gobl 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/data/tax/ES.json +841 -0
- data/data/tax/FR.json +47 -0
- data/data/tax/GB.json +56 -0
- data/data/tax/NL.json +60 -0
- data/home/cavalle/workspace/invopop/gobl.ruby/gobl.gemspec +30 -0
- data/lib/extensions.rb +12 -0
- data/lib/gobl/bill/advances.rb +90 -0
- data/lib/gobl/bill/charge.rb +121 -0
- data/lib/gobl/bill/delivery.rb +79 -0
- data/lib/gobl/bill/discount.rb +121 -0
- data/lib/gobl/bill/exchange_rates.rb +90 -0
- data/lib/gobl/bill/invoice.rb +209 -0
- data/lib/gobl/bill/invoice_type.rb +163 -0
- data/lib/gobl/bill/line.rb +121 -0
- data/lib/gobl/bill/line_charge.rb +79 -0
- data/lib/gobl/bill/line_discount.rb +79 -0
- data/lib/gobl/bill/ordering.rb +58 -0
- data/lib/gobl/bill/outlay.rb +107 -0
- data/lib/gobl/bill/payment.rb +79 -0
- data/lib/gobl/bill/preceding.rb +114 -0
- data/lib/gobl/bill/scheme_keys.rb +90 -0
- data/lib/gobl/bill/tax.rb +72 -0
- data/lib/gobl/bill/totals.rb +135 -0
- data/lib/gobl/cal/date.rb +106 -0
- data/lib/gobl/cal/period.rb +63 -0
- data/lib/gobl/config.rb +14 -0
- data/lib/gobl/currency/code.rb +322 -0
- data/lib/gobl/currency/exchange_rate.rb +65 -0
- data/lib/gobl/document.rb +70 -0
- data/lib/gobl/dsig/digest.rb +65 -0
- data/lib/gobl/dsig/signature.rb +106 -0
- data/lib/gobl/envelope.rb +77 -0
- data/lib/gobl/header.rb +98 -0
- data/lib/gobl/i18n/string.rb +72 -0
- data/lib/gobl/id.rb +80 -0
- data/lib/gobl/l10n/code.rb +106 -0
- data/lib/gobl/l10n/country_code.rb +405 -0
- data/lib/gobl/note/message.rb +72 -0
- data/lib/gobl/num/amount.rb +248 -0
- data/lib/gobl/num/percentage.rb +84 -0
- data/lib/gobl/operations/service_error.rb +10 -0
- data/lib/gobl/operations/validation_result.rb +46 -0
- data/lib/gobl/operations.rb +170 -0
- data/lib/gobl/org/address.rb +156 -0
- data/lib/gobl/org/code.rb +106 -0
- data/lib/gobl/org/coordinates.rb +79 -0
- data/lib/gobl/org/email.rb +79 -0
- data/lib/gobl/org/inbox.rb +86 -0
- data/lib/gobl/org/item.rb +121 -0
- data/lib/gobl/org/item_code.rb +65 -0
- data/lib/gobl/org/key.rb +106 -0
- data/lib/gobl/org/meta.rb +72 -0
- data/lib/gobl/org/name.rb +114 -0
- data/lib/gobl/org/note.rb +79 -0
- data/lib/gobl/org/note_key.rb +181 -0
- data/lib/gobl/org/party.rb +135 -0
- data/lib/gobl/org/person.rb +100 -0
- data/lib/gobl/org/registration.rb +99 -0
- data/lib/gobl/org/source_key.rb +161 -0
- data/lib/gobl/org/tax_identity.rb +93 -0
- data/lib/gobl/org/telephone.rb +72 -0
- data/lib/gobl/org/unit.rb +204 -0
- data/lib/gobl/pay/advance.rb +107 -0
- data/lib/gobl/pay/card.rb +65 -0
- data/lib/gobl/pay/credit_transfer.rb +86 -0
- data/lib/gobl/pay/direct_debit.rb +72 -0
- data/lib/gobl/pay/due_date.rb +86 -0
- data/lib/gobl/pay/instructions.rb +114 -0
- data/lib/gobl/pay/method_key.rb +163 -0
- data/lib/gobl/pay/online.rb +65 -0
- data/lib/gobl/pay/term_key.rb +166 -0
- data/lib/gobl/pay/terms.rb +79 -0
- data/lib/gobl/stamp.rb +63 -0
- data/lib/gobl/struct.rb +48 -0
- data/lib/gobl/tax/category.rb +83 -0
- data/lib/gobl/tax/category_total.rb +87 -0
- data/lib/gobl/tax/combo.rb +79 -0
- data/lib/gobl/tax/localities.rb +90 -0
- data/lib/gobl/tax/locality.rb +72 -0
- data/lib/gobl/tax/rate.rb +77 -0
- data/lib/gobl/tax/rate_total.rb +82 -0
- data/lib/gobl/tax/rate_total_surcharge.rb +63 -0
- data/lib/gobl/tax/rate_value.rb +79 -0
- data/lib/gobl/tax/region.rb +100 -0
- data/lib/gobl/tax/scheme.rb +86 -0
- data/lib/gobl/tax/schemes.rb +90 -0
- data/lib/gobl/tax/set.rb +90 -0
- data/lib/gobl/tax/total.rb +65 -0
- data/lib/gobl/types.rb +17 -0
- data/lib/gobl/uuid/uuid.rb +106 -0
- data/lib/gobl/version.rb +5 -0
- data/lib/gobl.rb +41 -0
- data/lib/gobl_extensions/document_helper.rb +45 -0
- data/lib/gobl_extensions/envelope_helper.rb +15 -0
- data/lib/gobl_extensions/i18n/value_keys_helper.rb +27 -0
- data/lib/gobl_extensions/tax/region_helper.rb +41 -0
- 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,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
|