gobl 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 +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
|