invoicehn 0.1.0 → 0.2.0

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.
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "invoicehn" unless defined?(Invoicehn::Issuance)
4
+ require "invoicehn/testing"
5
+
6
+ module Invoicehn
7
+ # Minitest modules a host application includes to run the store, sequence
8
+ # and ledger contracts against its own adapters:
9
+ #
10
+ # require "invoicehn/test_support/contracts"
11
+ #
12
+ # class PostgresStoreContractTest < ActiveSupport::TestCase
13
+ # include Invoicehn::TestSupport::StoreContract
14
+ #
15
+ # # A store holding a complete issuer and a current authorization for
16
+ # # contract_identifier ("000-001-01" unless overridden).
17
+ # def contract_store = Facturacion::InvoicehnStore.new
18
+ # end
19
+ #
20
+ # class PostgresSequenceContractTest < ActiveSupport::TestCase
21
+ # include Invoicehn::TestSupport::SequenceContract
22
+ # def contract_sequence = Facturacion::InvoicehnSequence.new
23
+ # end
24
+ #
25
+ # class LedgerContractTest < ActiveSupport::TestCase
26
+ # include Invoicehn::TestSupport::LedgerContract
27
+ # def contract_ledger = Facturacion::InvoicehnLedger.new
28
+ # end
29
+ #
30
+ # class IssuanceContractTest < ActiveSupport::TestCase
31
+ # include Invoicehn::TestSupport::IssuanceContract
32
+ # def contract_store = Facturacion::InvoicehnStore.new
33
+ # def contract_sequence = Facturacion::InvoicehnSequence.new
34
+ # def contract_ledger = Facturacion::InvoicehnLedger.new
35
+ # end
36
+ #
37
+ # The modules write documents; run them against a test database. They use
38
+ # only Minitest's assertions (assert, assert_equal, assert_raises, ...).
39
+ module TestSupport
40
+ # Fixed sample data, independent of the host's records.
41
+ module Samples
42
+ def contract_identifier = Issuance::DEFAULT_IDENTIFIER
43
+
44
+ def contract_sample_issuer
45
+ Issuer.new(rtn: "08011990123456", legal_name: "Contrato, S. de R.L.", trade_name: "Contrato",
46
+ headquarters_address: "Tegucigalpa, M.D.C.", phone: "2222-3333", email: "contrato@ejemplo.hn")
47
+ end
48
+
49
+ def contract_sample_lines
50
+ [LineItem.new(description: "Prueba de contrato", quantity: 2, unit_price: Money.new("50.00"),
51
+ treatment: :gravado_15)]
52
+ end
53
+
54
+ # A document numbered at the last number of `authorization`'s range, so
55
+ # it does not collide with the next number the host would issue.
56
+ def contract_sample_document(authorization:, issuer: contract_sample_issuer, **extra)
57
+ Invoice.new(correlative: authorization.range_end, issuer: issuer,
58
+ customer: Customer::Taxpayer.new(name: "Cliente de Contrato, S.A.", rtn: "05019005123456"),
59
+ authorization: authorization, line_items: contract_sample_lines,
60
+ issue_date: Invoicehn.today, **extra)
61
+ end
62
+ end
63
+
64
+ # Requires #contract_store.
65
+ module StoreContract
66
+ include Samples
67
+
68
+ def contract_authorization(store = contract_store)
69
+ auth = store.active_authorization(contract_identifier, next_sequence: nil, on: Invoicehn.today)
70
+ assert auth, "contract_store debe tener una autorización vigente para #{contract_identifier}"
71
+ auth
72
+ end
73
+
74
+ def test_store_contract_interface
75
+ assert Testing.assert_store_contract(contract_store)
76
+ end
77
+
78
+ def test_store_contract_issuer_is_complete
79
+ issuer = contract_store.issuer
80
+
81
+ assert_kind_of Issuer, issuer
82
+ assert_empty issuer.missing_fields
83
+ end
84
+
85
+ def test_store_contract_active_authorization_covers_the_number
86
+ store = contract_store
87
+ auth = contract_authorization(store)
88
+
89
+ first = store.active_authorization(contract_identifier, next_sequence: auth.range_start.sequence,
90
+ on: Invoicehn.today)
91
+
92
+ assert_equal auth.to_h, first.to_h
93
+ beyond = auth.range_end.sequence + 1
94
+ covering = store.active_authorization(contract_identifier, next_sequence: beyond, on: Invoicehn.today)
95
+
96
+ assert(covering.nil? || covering.covers?(auth.range_end.with_sequence(beyond)),
97
+ "active_authorization devolvió una autorización que no cubre #{beyond}")
98
+ assert_nil store.active_authorization(contract_identifier, next_sequence: auth.range_start.sequence,
99
+ on: auth.limit_date + 1)
100
+ end
101
+
102
+ def test_store_contract_round_trips_save_find_delete
103
+ store = contract_store
104
+ document = contract_sample_document(authorization: contract_authorization(store))
105
+
106
+ refute store.exists?(document.correlative)
107
+ store.save_document(document)
108
+
109
+ assert store.exists?(document.correlative)
110
+ assert_equal document.to_h, store.find(document.correlative).to_h
111
+ assert_raises(ImmutableDocument) { store.save_document(document) }
112
+
113
+ store.delete_document(document)
114
+
115
+ refute store.exists?(document.correlative)
116
+ assert_raises(DocumentNotFound) { store.find(document.correlative) }
117
+ ensure
118
+ # A failure above must not leave the sample behind for the next test.
119
+ store&.delete_document(document) if document
120
+ end
121
+
122
+ # Art. 41 — the annulled form replaces the record; the number stays.
123
+ def test_store_contract_keeps_the_annulled_form
124
+ store = contract_store
125
+ document = contract_sample_document(authorization: contract_authorization(store))
126
+ store.save_document(document)
127
+ store.save_document(document.annul(reason: "Prueba de contrato"))
128
+
129
+ found = store.find(document.correlative)
130
+
131
+ assert_predicate found, :annulled?
132
+ assert_equal "Prueba de contrato", found.annulment_reason
133
+ ensure
134
+ store&.delete_document(document) if document
135
+ end
136
+
137
+ # A note's reference and motivo (Arts. 26 and 28 num. 3-4) must survive
138
+ # the store. Runs only when the store holds an authorization for
139
+ # contract_note_identifier.
140
+ def test_store_contract_keeps_a_notes_reference_and_motivo
141
+ store = contract_store
142
+ auth = store.active_authorization(contract_note_identifier, next_sequence: nil, on: Invoicehn.today)
143
+ skip "sin autorización para #{contract_note_identifier}" unless auth
144
+
145
+ reference = DocumentReference.new(correlative: "#{contract_identifier}-00000001", cai: "CAI-CONTRATO",
146
+ issue_date: Invoicehn.today)
147
+ note = contract_sample_document(authorization: auth, reference: reference, motivo: "Devolución")
148
+ store.save_document(note)
149
+ found = store.find(note.correlative)
150
+
151
+ assert_equal reference, found.reference
152
+ assert_equal "Devolución", found.motivo
153
+ assert_equal note.to_h, found.to_h
154
+ ensure
155
+ store&.delete_document(note) if note
156
+ end
157
+
158
+ def contract_note_identifier = "#{contract_identifier.split("-")[0, 2].join("-")}-06"
159
+ end
160
+
161
+ # Requires #contract_sequence.
162
+ module SequenceContract
163
+ include Samples
164
+
165
+ def contract_other_identifier = "#{contract_identifier.split("-")[0, 2].join("-")}-07"
166
+
167
+ def test_sequence_contract_interface
168
+ assert Testing.assert_sequence_contract(contract_sequence)
169
+ end
170
+
171
+ def test_sequence_contract_peek_does_not_consume
172
+ sequence = contract_sequence
173
+ before = sequence.issued_count(contract_identifier)
174
+
175
+ assert_equal sequence.peek(contract_identifier), sequence.peek(contract_identifier)
176
+ assert_equal before + 1, sequence.peek(contract_identifier).sequence
177
+ assert_equal before, sequence.issued_count(contract_identifier)
178
+ end
179
+
180
+ def test_sequence_contract_allocate_yields_the_next_number_and_advances
181
+ sequence = contract_sequence
182
+ expected = sequence.peek(contract_identifier)
183
+ yielded = nil
184
+
185
+ result = sequence.allocate(contract_identifier) do |correlative|
186
+ yielded = correlative
187
+ :resultado
188
+ end
189
+
190
+ assert_equal expected, yielded
191
+ assert_equal :resultado, result
192
+ assert_equal expected.sequence, sequence.issued_count(contract_identifier)
193
+ assert_equal expected.succ, sequence.peek(contract_identifier)
194
+ end
195
+
196
+ # The property Issuance's all-or-nothing guarantee rests on.
197
+ def test_sequence_contract_a_failing_block_consumes_nothing
198
+ sequence = contract_sequence
199
+ before = sequence.issued_count(contract_identifier)
200
+
201
+ assert_raises(ArgumentError) { sequence.allocate(contract_identifier) { raise ArgumentError, "fallo" } }
202
+ assert_equal before, sequence.issued_count(contract_identifier)
203
+ end
204
+
205
+ # Each identificador del documento runs its own series (Art. 10 num. 7).
206
+ def test_sequence_contract_identifiers_are_independent
207
+ sequence = contract_sequence
208
+ other = sequence.issued_count(contract_other_identifier)
209
+ sequence.allocate(contract_identifier) { |c| c }
210
+
211
+ assert_equal other, sequence.issued_count(contract_other_identifier)
212
+ end
213
+ end
214
+
215
+ # Requires #contract_ledger.
216
+ module LedgerContract
217
+ include Samples
218
+
219
+ def contract_ledger_document
220
+ auth = Authorization.new(cai: "CAI-CONTRATO", range_start: "#{contract_identifier}-00000001",
221
+ range_end: "#{contract_identifier}-00000010", limit_date: Invoicehn.today + 30)
222
+ contract_sample_document(authorization: auth)
223
+ end
224
+
225
+ def test_ledger_contract_interface
226
+ assert Testing.assert_ledger_contract(contract_ledger)
227
+ end
228
+
229
+ def test_ledger_contract_records_issue_and_annulment
230
+ ledger = contract_ledger
231
+ document = contract_ledger_document
232
+
233
+ ledger.record(document, event: :emision)
234
+ ledger.record(document.annul(reason: "Prueba de contrato"), event: :anulacion)
235
+
236
+ pass
237
+ end
238
+ end
239
+
240
+ # Requires #contract_store, #contract_sequence and #contract_ledger,
241
+ # wired to each other. Issues one real Factura.
242
+ module IssuanceContract
243
+ include Samples
244
+
245
+ def test_issuance_contract_round_trip_and_rollback
246
+ document = Testing.assert_issuance_round_trip(store: contract_store, sequence: contract_sequence,
247
+ ledger: contract_ledger, identifier: contract_identifier)
248
+
249
+ assert_equal contract_identifier, document.correlative.identifier
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+ require "invoicehn" unless defined?(Invoicehn::Issuance)
5
+
6
+ module Invoicehn
7
+ module TestSupport
8
+ # In-memory store, sequence and ledger. They exist for tests, and as the
9
+ # shortest complete example of the contract a host adapter (Postgres,
10
+ # say) has to meet — each method here is one the README lists. They hold
11
+ # nothing across processes and must never back real fiscal documents.
12
+
13
+ # The store: issuer, authorizations and documents.
14
+ class MemoryStore
15
+ attr_reader :documents, :issuer
16
+
17
+ def initialize(issuer: nil, authorizations: [])
18
+ @issuer = issuer
19
+ @authorizations = authorizations.dup
20
+ @documents = {}
21
+ end
22
+
23
+ def save_issuer(issuer)
24
+ @issuer = issuer
25
+ end
26
+
27
+ def add_authorization(authorization)
28
+ @authorizations << authorization
29
+ authorization
30
+ end
31
+
32
+ def authorizations_for(identifier) = @authorizations.select { |a| a.identifier == identifier }
33
+
34
+ # Unexpired, and — when next_sequence is given — covering that number.
35
+ def active_authorization(identifier, next_sequence: nil, on: Invoicehn.today)
36
+ candidates = authorizations_for(identifier).reject { |a| a.expired?(on) }
37
+ if next_sequence
38
+ correlative = Correlative.parse(format("%<id>s-%<n>08d", id: identifier, n: next_sequence))
39
+ candidates = candidates.select { |a| a.covers?(correlative) }
40
+ end
41
+ candidates.min_by { |a| a.range_start.sequence }
42
+ end
43
+
44
+ # An issued document is immutable; only its annulled form may replace it
45
+ # (Art. 41).
46
+ def save_document(document)
47
+ key = document.correlative.to_s
48
+ if @documents.key?(key) && !document.annulled?
49
+ raise ImmutableDocument, "el documento #{key} ya está registrado y no puede modificarse (Art. 41)"
50
+ end
51
+
52
+ @documents[key] = document.to_h
53
+ document
54
+ end
55
+
56
+ def delete_document(document)
57
+ @documents.delete(document.correlative.to_s)
58
+ document
59
+ end
60
+
61
+ def find(correlative)
62
+ hash = @documents[correlative.to_s]
63
+ raise DocumentNotFound, "no existe el documento #{correlative}" unless hash
64
+
65
+ Invoice.from_h(hash)
66
+ end
67
+
68
+ def exists?(correlative) = @documents.key?(correlative.to_s)
69
+ end
70
+
71
+ # The counter per identifier. The block runs inside the lock and the
72
+ # counter advances only after it returns — the property that keeps a
73
+ # failed issue from burning a number.
74
+ class MemorySequence
75
+ def initialize
76
+ @counters = Hash.new(0)
77
+ @lock = Monitor.new
78
+ end
79
+
80
+ def peek(identifier) = @lock.synchronize { build(identifier, @counters[identifier] + 1) }
81
+
82
+ def issued_count(identifier) = @lock.synchronize { @counters[identifier] }
83
+
84
+ def allocate(identifier)
85
+ @lock.synchronize do
86
+ correlative = build(identifier, @counters[identifier] + 1)
87
+ result = block_given? ? yield(correlative) : correlative
88
+ @counters[identifier] = correlative.sequence
89
+ result
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def build(identifier, sequence) = Correlative.parse(format("%<id>s-%<n>08d", id: identifier, n: sequence))
96
+ end
97
+
98
+ # Keeps the entries it is handed.
99
+ class MemoryLedger
100
+ attr_reader :records
101
+
102
+ def initialize
103
+ @records = []
104
+ end
105
+
106
+ def record(document, event: :emision)
107
+ @records << [event, document.correlative.to_s]
108
+ document
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "invoicehn" unless defined?(Invoicehn::Issuance)
4
+
5
+ module Invoicehn
6
+ # Checks a host application's store, sequence and ledger against the
7
+ # contract Invoicehn::Issuance relies on. Not loaded by `require
8
+ # "invoicehn"`; a host's test suite opts in:
9
+ #
10
+ # require "invoicehn/testing"
11
+ #
12
+ # Invoicehn::Testing.assert_store_contract(PostgresStore.new)
13
+ # Invoicehn::Testing.assert_sequence_contract(PostgresSequence.new)
14
+ # Invoicehn::Testing.assert_ledger_contract(AccountingLedger.new)
15
+ # Invoicehn::Testing.assert_issuance_round_trip(store: ..., sequence: ..., ledger: ...)
16
+ #
17
+ # Every check raises Invoicehn::Testing::ContractError listing all the
18
+ # problems it found, and returns the checked object (or, for the round trip,
19
+ # the issued document) otherwise, so it works under minitest, RSpec or a
20
+ # plain script.
21
+ module Testing
22
+ # Raised when an implementation does not satisfy the contract.
23
+ class ContractError < Error; end
24
+
25
+ # name => [positional parameters, keyword parameters]
26
+ STORE_CONTRACT = {
27
+ issuer: [0, []],
28
+ authorizations_for: [1, []],
29
+ active_authorization: [1, %i[next_sequence on]],
30
+ save_document: [1, []],
31
+ delete_document: [1, []],
32
+ find: [1, []],
33
+ exists?: [1, []]
34
+ }.freeze
35
+
36
+ SEQUENCE_CONTRACT = {
37
+ allocate: [1, []],
38
+ peek: [1, []],
39
+ issued_count: [1, []]
40
+ }.freeze
41
+
42
+ LEDGER_CONTRACT = {
43
+ record: [1, %i[event]]
44
+ }.freeze
45
+
46
+ # A ledger that refuses every entry, used to prove the rollback.
47
+ class RefusingLedger
48
+ class Refused < StandardError; end
49
+
50
+ def record(_document, event: :emision)
51
+ raise Refused, "asiento rechazado a propósito (#{event})"
52
+ end
53
+ end
54
+
55
+ class << self
56
+ def assert_store_contract(store) = assert_contract(store, STORE_CONTRACT, "store")
57
+ def assert_sequence_contract(sequence) = assert_contract(sequence, SEQUENCE_CONTRACT, "sequence")
58
+ def assert_ledger_contract(ledger) = assert_contract(ledger, LEDGER_CONTRACT, "ledger")
59
+
60
+ # Behavioural check, against a store that already holds a complete
61
+ # issuer and a current authorization for `identifier`. Issues ONE real
62
+ # Factura, so run it against a test database.
63
+ #
64
+ # 1. With a ledger that refuses the entry, issuing must raise and leave
65
+ # nothing behind: no document at the next number, counter unchanged.
66
+ # This is what a Postgres adapter gets wrong when allocate advances
67
+ # the counter outside the block, or delete_document is a no-op.
68
+ # 2. With the real ledger, the document must come back from find equal
69
+ # to what was issued, and the counter must have advanced by one.
70
+ #
71
+ # @return [Invoice] the document issued in step 2
72
+ def assert_issuance_round_trip(store:, sequence:, ledger:, identifier: Issuance::DEFAULT_IDENTIFIER,
73
+ customer: Customer::ConsumidorFinal.new, line_items: nil)
74
+ line_items ||= [LineItem.new(description: "Prueba de contrato", quantity: 1,
75
+ unit_price: Money.new("1.00"), treatment: :exento)]
76
+ before = sequence.issued_count(identifier)
77
+ upcoming = sequence.peek(identifier)
78
+ problems = []
79
+
80
+ refusing = Issuance.new(store: store, sequence: sequence, ledger: RefusingLedger.new)
81
+ begin
82
+ refusing.issue(customer: customer, line_items: line_items, identifier: identifier)
83
+ problems << "issue no propagó el error del ledger"
84
+ rescue RefusingLedger::Refused
85
+ nil
86
+ end
87
+ problems << "el documento #{upcoming} quedó guardado aunque el ledger lo rechazó" if store.exists?(upcoming)
88
+ if sequence.issued_count(identifier) != before
89
+ problems << "el contador avanzó aunque la emisión falló (allocate debe avanzar sólo si el bloque termina)"
90
+ end
91
+ raise ContractError, problems.join("\n") if problems.any?
92
+
93
+ document = Issuance.new(store: store, sequence: sequence, ledger: ledger)
94
+ .issue(customer: customer, line_items: line_items, identifier: identifier)
95
+
96
+ problems << "se emitió #{document.correlative}, se esperaba #{upcoming}" unless document.correlative == upcoming
97
+ problems << "issued_count no avanzó en uno" unless sequence.issued_count(identifier) == before + 1
98
+ problems << "exists? no encuentra #{document.correlative}" unless store.exists?(document.correlative)
99
+ unless store.find(document.correlative).to_h == document.to_h
100
+ problems << "find(#{document.correlative}) no devuelve el documento tal como se emitió"
101
+ end
102
+ raise ContractError, problems.join("\n") if problems.any?
103
+
104
+ document
105
+ end
106
+
107
+ private
108
+
109
+ def assert_contract(object, contract, role)
110
+ problems = contract.flat_map do |name, (positional, keywords)|
111
+ next ["#{role} debe responder a ##{name}"] unless object.respond_to?(name)
112
+
113
+ signature_problems(object.method(name), role, name, positional, keywords)
114
+ end
115
+ raise ContractError, problems.join("\n") if problems.any?
116
+
117
+ object
118
+ end
119
+
120
+ def signature_problems(method, role, name, positional, keywords)
121
+ params = method.parameters
122
+ problems = []
123
+
124
+ required = params.count { |type, _| type == :req }
125
+ accepts = params.count { |type, _| %i[req opt].include?(type) }
126
+ rest = params.any? { |type, _| type == :rest }
127
+ unless required <= positional && (rest || accepts >= positional)
128
+ problems << "#{role}##{name} debe aceptar #{positional} argumento(s) posicional(es)"
129
+ end
130
+
131
+ keyrest = params.any? { |type, _| type == :keyrest }
132
+ declared = params.filter_map { |type, param| param if %i[key keyreq].include?(type) }
133
+ (keywords - declared).each do |kw|
134
+ problems << "#{role}##{name} debe aceptar la palabra clave #{kw}:" unless keyrest
135
+ end
136
+ problems
137
+ end
138
+ end
139
+ end
140
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Invoicehn
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/invoicehn.rb CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative "invoicehn/version"
4
4
  require_relative "invoicehn/errors"
5
+ require_relative "invoicehn/settings"
5
6
  require_relative "invoicehn/locale"
6
7
  require_relative "invoicehn/money"
7
8
  require_relative "invoicehn/spanish_numerals"
8
9
  require_relative "invoicehn/rtn"
9
10
  require_relative "invoicehn/correlative"
11
+ require_relative "invoicehn/document_reference"
10
12
  require_relative "invoicehn/tax_treatment"
11
13
  require_relative "invoicehn/line_item"
12
14
  require_relative "invoicehn/tax_summary"
@@ -37,4 +39,30 @@ require_relative "invoicehn/renderers/pdf"
37
39
  # límite de emisión (Arts. 59-61) all remain the operator's obligations before
38
40
  # any document produced here is valid.
39
41
  module Invoicehn
42
+ # Invoicehn.configure / Invoicehn.today — see Invoicehn::Settings.
43
+ class << self
44
+ def settings
45
+ @settings ||= Settings.new
46
+ end
47
+
48
+ def configure
49
+ yield settings
50
+ settings
51
+ end
52
+
53
+ # Restores the defaults. Meant for test suites.
54
+ def reset_settings!
55
+ @settings = Settings.new
56
+ end
57
+
58
+ # Today's date in Honduras, as the configured clock reports it. Raises if
59
+ # the clock returns something that is not a Date — a Time or a String here
60
+ # would silently shift fiscal dates.
61
+ def today
62
+ value = settings.today.call
63
+ return value if value.instance_of?(Date)
64
+
65
+ raise Error, "el reloj configurado devolvió #{value.inspect}; se espera un Date"
66
+ end
67
+ end
40
68
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: invoicehn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jorge Padilla
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-08-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bigdecimal
@@ -81,9 +80,10 @@ dependencies:
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0.23'
83
82
  description: |
84
- Genera Facturas que cumplen los requisitos de contenido de los Artículos 10 y 11 del
85
- Reglamento del Régimen de Facturación (Acuerdo 481-2017 y sus reformas), incluyendo el
86
- correlativo de 16 dígitos, control del CAI, rango autorizado y fecha límite de emisión,
83
+ Genera Facturas, Notas de Crédito y Notas de Débito que cumplen los requisitos de contenido
84
+ de los Artículos 10-11 y 25-28 del Reglamento del Régimen de Facturación (Acuerdo 481-2017
85
+ y sus reformas), incluyendo el correlativo de 16 dígitos con los códigos de tipo de
86
+ documento reformados, control del CAI, rango autorizado y fecha límite de emisión,
87
87
  discriminación del ISV por tarifa, y el redondeo estatutario del Artículo 9 de la Ley
88
88
  del Impuesto Sobre Ventas. Incluye biblioteca Ruby y una interfaz de terminal.
89
89
  email:
@@ -111,6 +111,7 @@ files:
111
111
  - lib/invoicehn/config.rb
112
112
  - lib/invoicehn/correlative.rb
113
113
  - lib/invoicehn/customer.rb
114
+ - lib/invoicehn/document_reference.rb
114
115
  - lib/invoicehn/errors.rb
115
116
  - lib/invoicehn/exchange_rate.rb
116
117
  - lib/invoicehn/invoice.rb
@@ -125,10 +126,14 @@ files:
125
126
  - lib/invoicehn/renderers/text.rb
126
127
  - lib/invoicehn/rtn.rb
127
128
  - lib/invoicehn/sequence.rb
129
+ - lib/invoicehn/settings.rb
128
130
  - lib/invoicehn/spanish_numerals.rb
129
131
  - lib/invoicehn/storage/json_store.rb
130
132
  - lib/invoicehn/tax_summary.rb
131
133
  - lib/invoicehn/tax_treatment.rb
134
+ - lib/invoicehn/test_support/contracts.rb
135
+ - lib/invoicehn/test_support/memory.rb
136
+ - lib/invoicehn/testing.rb
132
137
  - lib/invoicehn/version.rb
133
138
  homepage: https://github.com/JorgePadilla/invoicehn
134
139
  licenses:
@@ -139,7 +144,6 @@ metadata:
139
144
  changelog_uri: https://github.com/JorgePadilla/invoicehn/blob/main/CHANGELOG.md
140
145
  bug_tracker_uri: https://github.com/JorgePadilla/invoicehn/issues
141
146
  rubygems_mfa_required: 'true'
142
- post_install_message:
143
147
  rdoc_options: []
144
148
  require_paths:
145
149
  - lib
@@ -154,8 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
158
  - !ruby/object:Gem::Version
155
159
  version: '0'
156
160
  requirements: []
157
- rubygems_version: 3.5.22
158
- signing_key:
161
+ rubygems_version: 3.6.9
159
162
  specification_version: 4
160
163
  summary: Facturación para Honduras conforme al Régimen de Facturación (Acuerdo 481-2017)
161
164
  test_files: []