gobl 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/data/tax/ES.json +841 -0
  3. data/data/tax/FR.json +47 -0
  4. data/data/tax/GB.json +56 -0
  5. data/data/tax/NL.json +60 -0
  6. data/home/cavalle/workspace/invopop/gobl.ruby/gobl.gemspec +30 -0
  7. data/lib/extensions.rb +12 -0
  8. data/lib/gobl/bill/advances.rb +90 -0
  9. data/lib/gobl/bill/charge.rb +121 -0
  10. data/lib/gobl/bill/delivery.rb +79 -0
  11. data/lib/gobl/bill/discount.rb +121 -0
  12. data/lib/gobl/bill/exchange_rates.rb +90 -0
  13. data/lib/gobl/bill/invoice.rb +209 -0
  14. data/lib/gobl/bill/invoice_type.rb +163 -0
  15. data/lib/gobl/bill/line.rb +121 -0
  16. data/lib/gobl/bill/line_charge.rb +79 -0
  17. data/lib/gobl/bill/line_discount.rb +79 -0
  18. data/lib/gobl/bill/ordering.rb +58 -0
  19. data/lib/gobl/bill/outlay.rb +107 -0
  20. data/lib/gobl/bill/payment.rb +79 -0
  21. data/lib/gobl/bill/preceding.rb +114 -0
  22. data/lib/gobl/bill/scheme_keys.rb +90 -0
  23. data/lib/gobl/bill/tax.rb +72 -0
  24. data/lib/gobl/bill/totals.rb +135 -0
  25. data/lib/gobl/cal/date.rb +106 -0
  26. data/lib/gobl/cal/period.rb +63 -0
  27. data/lib/gobl/config.rb +14 -0
  28. data/lib/gobl/currency/code.rb +322 -0
  29. data/lib/gobl/currency/exchange_rate.rb +65 -0
  30. data/lib/gobl/document.rb +70 -0
  31. data/lib/gobl/dsig/digest.rb +65 -0
  32. data/lib/gobl/dsig/signature.rb +106 -0
  33. data/lib/gobl/envelope.rb +77 -0
  34. data/lib/gobl/header.rb +98 -0
  35. data/lib/gobl/i18n/string.rb +72 -0
  36. data/lib/gobl/id.rb +80 -0
  37. data/lib/gobl/l10n/code.rb +106 -0
  38. data/lib/gobl/l10n/country_code.rb +405 -0
  39. data/lib/gobl/note/message.rb +72 -0
  40. data/lib/gobl/num/amount.rb +248 -0
  41. data/lib/gobl/num/percentage.rb +84 -0
  42. data/lib/gobl/operations/service_error.rb +10 -0
  43. data/lib/gobl/operations/validation_result.rb +46 -0
  44. data/lib/gobl/operations.rb +170 -0
  45. data/lib/gobl/org/address.rb +156 -0
  46. data/lib/gobl/org/code.rb +106 -0
  47. data/lib/gobl/org/coordinates.rb +79 -0
  48. data/lib/gobl/org/email.rb +79 -0
  49. data/lib/gobl/org/inbox.rb +86 -0
  50. data/lib/gobl/org/item.rb +121 -0
  51. data/lib/gobl/org/item_code.rb +65 -0
  52. data/lib/gobl/org/key.rb +106 -0
  53. data/lib/gobl/org/meta.rb +72 -0
  54. data/lib/gobl/org/name.rb +114 -0
  55. data/lib/gobl/org/note.rb +79 -0
  56. data/lib/gobl/org/note_key.rb +181 -0
  57. data/lib/gobl/org/party.rb +135 -0
  58. data/lib/gobl/org/person.rb +100 -0
  59. data/lib/gobl/org/registration.rb +99 -0
  60. data/lib/gobl/org/source_key.rb +161 -0
  61. data/lib/gobl/org/tax_identity.rb +93 -0
  62. data/lib/gobl/org/telephone.rb +72 -0
  63. data/lib/gobl/org/unit.rb +204 -0
  64. data/lib/gobl/pay/advance.rb +107 -0
  65. data/lib/gobl/pay/card.rb +65 -0
  66. data/lib/gobl/pay/credit_transfer.rb +86 -0
  67. data/lib/gobl/pay/direct_debit.rb +72 -0
  68. data/lib/gobl/pay/due_date.rb +86 -0
  69. data/lib/gobl/pay/instructions.rb +114 -0
  70. data/lib/gobl/pay/method_key.rb +163 -0
  71. data/lib/gobl/pay/online.rb +65 -0
  72. data/lib/gobl/pay/term_key.rb +166 -0
  73. data/lib/gobl/pay/terms.rb +79 -0
  74. data/lib/gobl/stamp.rb +63 -0
  75. data/lib/gobl/struct.rb +48 -0
  76. data/lib/gobl/tax/category.rb +83 -0
  77. data/lib/gobl/tax/category_total.rb +87 -0
  78. data/lib/gobl/tax/combo.rb +79 -0
  79. data/lib/gobl/tax/localities.rb +90 -0
  80. data/lib/gobl/tax/locality.rb +72 -0
  81. data/lib/gobl/tax/rate.rb +77 -0
  82. data/lib/gobl/tax/rate_total.rb +82 -0
  83. data/lib/gobl/tax/rate_total_surcharge.rb +63 -0
  84. data/lib/gobl/tax/rate_value.rb +79 -0
  85. data/lib/gobl/tax/region.rb +100 -0
  86. data/lib/gobl/tax/scheme.rb +86 -0
  87. data/lib/gobl/tax/schemes.rb +90 -0
  88. data/lib/gobl/tax/set.rb +90 -0
  89. data/lib/gobl/tax/total.rb +65 -0
  90. data/lib/gobl/types.rb +17 -0
  91. data/lib/gobl/uuid/uuid.rb +106 -0
  92. data/lib/gobl/version.rb +5 -0
  93. data/lib/gobl.rb +41 -0
  94. data/lib/gobl_extensions/document_helper.rb +45 -0
  95. data/lib/gobl_extensions/envelope_helper.rb +15 -0
  96. data/lib/gobl_extensions/i18n/value_keys_helper.rb +27 -0
  97. data/lib/gobl_extensions/tax/region_helper.rb +41 -0
  98. metadata +225 -0
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GOBL
4
+ module Num
5
+ # Represents a numeric quantity with optional decimal places that determine accuracy
6
+ class Amount
7
+ # The integer value of the amount
8
+ attr_reader :value
9
+
10
+ # The exponent, or number of significant figures of the amount
11
+ attr_reader :exp
12
+
13
+ # Creates a new {Amount} from the given data object
14
+ #
15
+ # @param data [Hash, String, #to_s, Amount] the data object. Supported types:
16
+ #
17
+ # * A `Hash` with a `:value` key and, optionally, an `:exp` one,
18
+ # * A `String` or any other object that can be coerced into one via `#to_s`,
19
+ # * Another {Amount}
20
+ def initialize(data)
21
+ if data.is_a?(String)
22
+ parse(data)
23
+ elsif data.is_a?(Hash)
24
+ @value = data[:value]
25
+ @exp = data[:exp] || 0
26
+ elsif data.is_a?(self.class)
27
+ @value = data.value
28
+ @exp = data.exp
29
+ elsif data.respond_to?(:to_s)
30
+ parse(data.to_s)
31
+ else
32
+ raise 'Unsupported input amount'
33
+ end
34
+ end
35
+
36
+ # Creates a new {Amount} from a GOBL value
37
+ #
38
+ # @param data [Hash] a GOBL value
39
+ #
40
+ # @return [Amount] the object created from the given data
41
+ def self.from_gobl!(data)
42
+ new(data)
43
+ end
44
+
45
+ # Deserializes an {Amount} from a JSON string
46
+ #
47
+ # @param json [String] a JSON string
48
+ #
49
+ # @return [Amount] the deserialized {Amount}
50
+ def self.from_json!(json)
51
+ from_gobl!(JSON.parse(json))
52
+ end
53
+
54
+ # Returns a GOBL value representing the current amount
55
+ #
56
+ # @return [String] the GOBL value that represents the current amount
57
+ def to_gobl
58
+ to_s
59
+ end
60
+
61
+ # Serializes a GOBL struct into a JSON string
62
+ #
63
+ # @param options [#to_h] a Hash-like object to pass to `JSON.generate`
64
+ #
65
+ # @return [GOBL::Struct] the JSON string representing the GOBL struct
66
+ def to_json(options = nil)
67
+ JSON.generate(to_gobl, options)
68
+ end
69
+
70
+ # Returns the string representation of the current amount
71
+ #
72
+ # @return [String] the string representation of the current amount
73
+ #
74
+ # @see #as_s
75
+ def to_s
76
+ as_s
77
+ end
78
+
79
+ # Returns the string representation of the current amount
80
+ #
81
+ # @return [String] the string representation of the current amount
82
+ def as_s
83
+ return value.to_s if exp.zero?
84
+ raise 'exponent too high' if exp > 100
85
+
86
+ p = 10**exp
87
+ v1 = value / p
88
+ v2 = value - (v1 * p)
89
+ v2 = -v2 if v2.negative?
90
+ format('%d.%0*d', v1, exp, v2)
91
+ end
92
+
93
+ # Rescales each {Amount} in the pair ensuring both have the same exponent
94
+ #
95
+ # @param a1 [Amount] the first amount of the pair
96
+ # @param a2 [Amount] the second amount of the pair
97
+ #
98
+ # @return [Array(Amount, Amount)] the rescaled pair of amounts
99
+ def self.rescale_pair(a1, a2)
100
+ exp = a1.exp
101
+ exp = a2.exp if a2.exp > exp
102
+ [a1.rescale(exp), a2.rescale(exp)]
103
+ end
104
+
105
+ # Compares the current amount with another one
106
+ #
107
+ # @param other [Amount] the {Amount} to compare with
108
+ #
109
+ # @return [Integer] the result of the comparison:
110
+ #
111
+ # * `-1` if the current amount is lesser than the given one
112
+ # * `1` if current amount is greater than the given one
113
+ # * `0` if both amounts are equal
114
+ def compare(other)
115
+ a1, a2 = self.class.rescale_pair(self, other)
116
+ if a1.value < a2.value
117
+ -1
118
+ elsif a1.value > a2.value
119
+ 1
120
+ else
121
+ 0 # same
122
+ end
123
+ end
124
+
125
+ # Returns whether the current amount is equal to another one
126
+ #
127
+ # @param other [Amount] the {Amount} to compare with
128
+ #
129
+ # @return [Boolean] whether the two amounts are equal (`true`) or not (`false`)
130
+ def ==(other)
131
+ compare(other).zero?
132
+ end
133
+
134
+ # Changes the exponent of the {Amount} multipling or dividing its value as
135
+ # necessary. A lower exponent implies loosing accuracy.
136
+ #
137
+ # @param e [Integer] the new exponent
138
+ #
139
+ # @return [Amount] the rescaled {Amount}
140
+ def rescale(e)
141
+ if exp > e
142
+ # divide
143
+ x = exp - e
144
+ v = (value.to_f / (10**x)).round
145
+ self.class.new(value: v, exp: e)
146
+ elsif exp < e
147
+ # multiply
148
+ x = e - exp
149
+ v = value * (10**x)
150
+ self.class.new(value: v, exp: e)
151
+ else
152
+ # nothing
153
+ self
154
+ end
155
+ end
156
+
157
+ # Returns whether the current amount is equal to zero
158
+ #
159
+ # @return [Boolean] `true` if the current amount is equal to zero, `false`otherwise
160
+ def zero?
161
+ value.zero?
162
+ end
163
+
164
+ # Adds the current amount to a given one
165
+ #
166
+ # @param a2 [Amount] the amount to add
167
+ #
168
+ # @return [Amount] the amount resulted from the operation
169
+ def add(a2)
170
+ a2 = a2.rescale(exp)
171
+ self.class.new(value: value + a2.value, exp: exp)
172
+ end
173
+
174
+ # Subtracts a given {Amount} from the current one
175
+ #
176
+ # @param a2 [Amount] the amount to subtract
177
+ #
178
+ # @return [Amount] the amount resulted from the operation
179
+ def subtract(a2)
180
+ a2 = a2.rescale(exp)
181
+ self.class.new(value: value - a2.value, exp: exp)
182
+ end
183
+
184
+ # Multiplies a given {Amount} with the current one
185
+ #
186
+ # @param a2 [Amount] the amount to multiply with
187
+ #
188
+ # @return [Amount] the amount resulted from the operation
189
+ def multiply(a2)
190
+ v = (value * a2.value) / (10**a2.exp)
191
+ self.class.new(value: v, exp: exp)
192
+ end
193
+
194
+ # Divides the current amount by the given one
195
+ #
196
+ # @param a2 [Amount] the amount to divide by
197
+ #
198
+ # @return [Amount] the amount resulted from the operation
199
+ def divide(a2)
200
+ v = (value.to_f * (10**a2.exp)) / a2.value.to_f
201
+ self.class.new(value: v.round, exp: exp)
202
+ end
203
+
204
+ # Splits the current amount into equal `x` parts providing a second amount with
205
+ # the remainder. This is like {#divide}, but will correctly account for rounding
206
+ # errors.
207
+ #
208
+ # @param x [Integer] the number of parts to divide the amount into
209
+ #
210
+ # @return [Array(Amount, Amount)] the split amount and the remider amount
211
+ def split(x)
212
+ a2 = divide(self.class.new(value: x, exp: 0))
213
+ a3 = a2.multiply(self.class.new(value: x - 1, exp: 0))
214
+ a3 = subtract(a3)
215
+ [a2, a3]
216
+ end
217
+
218
+ # Inverts the sign of the current amount
219
+ #
220
+ # @return [Amount] the amount with the opposite sign
221
+ def invert
222
+ self.class.new(value: -value, exp: exp)
223
+ end
224
+
225
+ protected
226
+
227
+ # Extract the value and exponent from the string.
228
+ # Implementation copied from base GOBL library.
229
+ def parse(str)
230
+ x = str.split('.')
231
+ raise "invalid amount '#{str}'" if x.length > 2 # abort
232
+
233
+ v = x[0].to_i
234
+ e = 0
235
+
236
+ if x[1]
237
+ v2 = x[1].to_i
238
+ e = x[1].length
239
+ v *= 10**e
240
+ v += v2
241
+ end
242
+
243
+ @value = v
244
+ @exp = e
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GOBL
4
+ module Num
5
+ Factor1 = Amount.new(value: 1) # @api private
6
+ Factor100 = Amount.new(value: 100) # @api private
7
+
8
+ # Similar to {GOBL::Num::Amount}, but specialized in representing percentages
9
+ class Percentage < Amount
10
+ # Returns a string representation of the percentage with the percentage symbol
11
+ #
12
+ # @return [String] the string representing the percentage
13
+ def to_s
14
+ "#{to_s_without_symbol}%"
15
+ end
16
+
17
+ # Returns a string representation of the percentage with_out the percentage symbol
18
+ #
19
+ # @return [String] the string representing the percentage
20
+ def to_s_without_symbol
21
+ e = exp - 2
22
+ e = 0 if e.negative?
23
+ p = multiply(Factor100).rescale(e)
24
+ p.as_s
25
+ end
26
+
27
+ # Calculates the "percent of" a given amount
28
+ #
29
+ # @param a [GOBL::Num::Amount] the amount to calculate the percent of
30
+ #
31
+ # @return [GOBL::Num::Amount] the calculated amount
32
+ def of(a)
33
+ a.multiply(self)
34
+ end
35
+
36
+ # Calculates what "percent from" the given amount would result assuming the rate has
37
+ # already been applied. Put otherwise, for a given amount that was increased by the
38
+ # current percentage, this method returns the amount of that increment.
39
+ #
40
+ # @example
41
+ # original_amount = GOBL::Num::Amount.new("200")
42
+ # percentage = GOBL::Num::Percentage.new("25%")
43
+ # increment = percentage.of(original_amount) #=> 50
44
+ #
45
+ # increased_amount = original_amount.add(increment) #=> 250
46
+ # percentage.from(increased_amount) #=> 50
47
+ #
48
+ # @param a [GOBL::Num::Amount] the amount to calculate the percent from
49
+ #
50
+ # @return [GOBL::Num::Amount] the calculated amount
51
+ def from(a)
52
+ x = a.divide(factor)
53
+ a.subtract(x)
54
+ end
55
+
56
+ # Returns the percentage amount as a factor, adding 1 to the rate
57
+ #
58
+ # @return [GOBL::Num::Amount] the factor
59
+ def factor
60
+ Amount.new(value: value, exp: exp).add(Factor1)
61
+ end
62
+
63
+ protected
64
+
65
+ def parse(data)
66
+ return if data.length.zero?
67
+
68
+ rescale = false
69
+ if data[-1] == '%'
70
+ data = data.chomp('%')
71
+ rescale = true
72
+ end
73
+
74
+ super(data)
75
+
76
+ if rescale
77
+ p = self.rescale(exp + 2).divide(Factor100)
78
+ @value = p.value
79
+ @exp = p.exp
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GOBL
4
+ module Operations
5
+ # Wraps an error returned by the backend GOBL service when attempting to run an
6
+ # operation
7
+ class ServiceError < StandardError
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GOBL
4
+ module Operations
5
+ # The result of a GOBL validation over a GOBL structure
6
+ class ValidationResult
7
+ SERVICE_ERROR_REGEX = /^code=(?<code>\d+), message=(?<msg>.+)$/.freeze # @api private
8
+
9
+ def initialize(errors)
10
+ @errors = errors
11
+ end
12
+
13
+ # Whether the GOBL structure were valid or not
14
+ #
15
+ # @return [Boolean]
16
+ def valid?
17
+ @errors.empty?
18
+ end
19
+
20
+ # The list of errors found in the GOBL structure
21
+ #
22
+ # @return [Array]
23
+ def errors
24
+ @errors || []
25
+ end
26
+
27
+ # Builds a new `ValidationResult` object resulted from parsing the service
28
+ # error response given as a parameter
29
+ #
30
+ # @param service_error [String]
31
+ # @return [ValidationResult]
32
+ def self.from_service_error(service_error)
33
+ message = service_error.match(SERVICE_ERROR_REGEX)[:msg]
34
+ errors = message.split('; ')
35
+ new errors
36
+ end
37
+
38
+ # Builds a new `ValidationResult` object for a positive `validate` operation
39
+ #
40
+ # @return [ValidationResult]
41
+ def self.valid
42
+ new []
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GOBL
4
+ # Provides the API to execute operations over GOBL structures. It implements them by
5
+ # sending HTTP requests to the bulk endpoint of the service that the GOBL CLI makes
6
+ # available via the `gobl serve` command. The server's host and port must be configured
7
+ # using `GOBL.config.service_host` and `GOBL.config.service_port`.
8
+ #
9
+ # @example
10
+ # GOBL.config.service_host = 'localhost'
11
+ # GOBL.config.service_port = 8080
12
+ #
13
+ # doc = GOBL::Document.from_gobl!(
14
+ # '$schema' => 'https://gobl.org/draft-0/bill/invoice',
15
+ # 'code' => 'SAMPLE-001',
16
+ # 'currency' => 'EUR',
17
+ # 'issue_date' => '2022-02-01',
18
+ # 'supplier' => { 'tax_id' => { 'country' => 'ES', 'code' => '54387763P' }, 'name' => 'Provide One S.L.' },
19
+ # 'customer' => { 'tax_id' => { 'country' => 'ES', 'code' => '54387763P' }, 'name' => 'Sample Consumer' },
20
+ # 'lines' => [ { 'quantity' => '20', 'item' => { 'name' => 'Development services', 'price' => '90.00' } } ]
21
+ # )
22
+ #
23
+ # built_doc = GOBL.build(doc)
24
+ #
25
+ # invoice = built_doc.extract
26
+ # invoice.totals.total.to_s #=> "1800.00"
27
+ module Operations
28
+ # @api private
29
+ VALIDATABLE_TYPES = SIGNABLE_TYPES = BUILDABLE_TYPES = [
30
+ GOBL::Document,
31
+ GOBL::Envelope
32
+ ].freeze
33
+
34
+ # Calculates and validates an envelope or document, wrapping it in an envelope if
35
+ # requested.
36
+ #
37
+ # @param struct [GOBL::Document, GOBL::Envelope] the document or envelope to build.
38
+ # @param envelop [Boolean] whether the operation should envelop the document.
39
+ # @param draft [Boolean] whether the envelope should be flagged as a draft.
40
+ #
41
+ # @return [GOBL::Envelope, GOBL::Document] a built envelope or document.
42
+ #
43
+ # @raise [GOBL::Operations::ServiceError] if the service returns any errors.
44
+ #
45
+ # @example Build a document without enveloping it.
46
+ # invoice = GOBL::Document.from_json!(File.read('invoice.json'))
47
+ # GOBL.build(invoice) #=> A new, calculated `GOBL::Document`
48
+ #
49
+ # @example Build a document and wrap in a draft envelope.
50
+ # invoice = GOBL::Document.from_json!(File.read('invoice.json'))
51
+ # GOBL.build(invoice, envelop: true) #=> A calculated, draft `GOBL::Envelope`
52
+ #
53
+ # @example Build a document and wrap in a non-draft envelope.
54
+ # invoice = GOBL::Document.from_json!(File.read('invoice.json'))
55
+ # GOBL.build(invoice, envelop: true, draft: false) #=> A calculated, non-draft `GOBL::Envelope`
56
+ #
57
+ # @example Build an envelope.
58
+ # envelope = GOBL::Envelop.from_json!(File.read('envelope.json'))
59
+ # GOBL.build(envelope) #=> A new, calculated GOBL::Envelope
60
+ #
61
+ # @example Build an envelope forcing it to be a non-draft.
62
+ # envelope = GOBL::Envelop.from_json!(File.read('draft_envelope.json'))
63
+ # GOBL.build(envelope, draft: false) #=> A new, non-draft GOBL::Envelope
64
+ def build(struct, envelop: nil, draft: nil)
65
+ check_struct_type struct, BUILDABLE_TYPES
66
+
67
+ response = request_action(:build, struct: struct,
68
+ envelop: envelop,
69
+ draft: draft)
70
+
71
+ raise ServiceError, response['error'] if response['error'].present?
72
+
73
+ GOBL::Struct.from_gobl! response['payload']
74
+ end
75
+
76
+ # Checks whether or not a document or envelope is valid according to the GOBL schema
77
+ # and rules.
78
+ #
79
+ # @param struct [GOBL::Document, GOBL::Envelope] the document or the envelope to
80
+ # validate.
81
+ #
82
+ # @return [GOBL::ValidationResult] the result of the validations.
83
+ #
84
+ # @example Validate an invalid document.
85
+ # document = GOBL::Document.from_json!(File.read('invalid_invoice.json'))
86
+ # result = GOBL.validate(document)
87
+ # result.valid? #=> false
88
+ # result.errors #=> ['code: cannot be blank', 'totals: cannot be blank']
89
+ #
90
+ # @example Validate a valid envelope.
91
+ # envelope = GOBL::Envelop.from_json!(File.read('valid_envelope.json'))
92
+ # result = GOBL.validate(envelope)
93
+ # result.valid? #=> true
94
+ # result.errors #=> []
95
+ def validate(struct)
96
+ check_struct_type struct, VALIDATABLE_TYPES
97
+
98
+ response = request_action(:validate, struct: struct)
99
+
100
+ if response['error'].present?
101
+ ValidationResult.from_service_error(response['error'])
102
+ elsif response['payload']['ok']
103
+ ValidationResult.valid
104
+ else
105
+ raise 'Unexpected response from the service'
106
+ end
107
+ end
108
+
109
+ # Signs a document or envelope, calculating, enveloping and validating it first if
110
+ # needed. The signing key will be the one configured in the server.
111
+ #
112
+ # @param struct [GOBL::Document, GOBL::Envelope] the envelop or document to sign.
113
+ #
114
+ # @return [GOBL::Envelope] a signed envelope.
115
+ #
116
+ # @raise [GOBL::Operations::ServiceError] if the service returns any errors.
117
+ #
118
+ # @example Sign an envelope.
119
+ # envelope = GOBL::Envelop.from_json!(File.read('draft_envelope.json'))
120
+ # GOBL.sign(envelope) #=> A new signed GOBL::Envelope
121
+ def sign(struct)
122
+ check_struct_type struct, SIGNABLE_TYPES
123
+
124
+ response = request_action(:sign, struct: struct)
125
+
126
+ raise ServiceError, response['error'] if response['error'].present?
127
+
128
+ GOBL::Struct.from_gobl! response['payload']
129
+ end
130
+
131
+ private
132
+
133
+ # Sends a request action to the GOBL service using the bulk endpoint. This endpoint
134
+ # allows to send multiple actions in one request but we only use it with one action.
135
+ def request_action(action, struct:, **params)
136
+ request = build_bulk_request(action, struct, params)
137
+
138
+ response = Net::HTTP.post(bulk_endpoint, request)
139
+ response.error! unless response.is_a?(Net::HTTPSuccess)
140
+
141
+ parts = parse_bulk_response(response)
142
+ parts.first # We're only requesting one action, the response is the first part
143
+ end
144
+
145
+ def build_bulk_request(action, struct, params)
146
+ payload = params.merge(data: Base64.encode64(struct.to_json.to_s))
147
+
148
+ { action: action, payload: payload }.to_json
149
+ end
150
+
151
+ def bulk_endpoint
152
+ @bulk_endpoint ||= URI::HTTP.build(
153
+ host: GOBL.config.service_host,
154
+ port: GOBL.config.service_port,
155
+ path: '/bulk'
156
+ )
157
+ end
158
+
159
+ def parse_bulk_response(response)
160
+ response.body.split("\n").map { |row| JSON.parse(row) }
161
+ end
162
+
163
+ def check_struct_type(struct, allowed_types)
164
+ return if allowed_types.any? { |klass| struct.is_a?(klass) }
165
+
166
+ message = "This operation only supports #{allowed_types.join(', ')} structs"
167
+ raise ArgumentError, message
168
+ end
169
+ end
170
+ end