-
Notifications
You must be signed in to change notification settings - Fork 2
/
app.rb
236 lines (204 loc) · 7.27 KB
/
app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
require 'logger'
require 'sinatra'
require 'nokogiri'
require 'json'
require 'time'
require_relative 'lib/converter'
set :server, :puma
set :bind, '0.0.0.0'
# Need this for health check
get '/' do
200
end
get '/validate/:filename' do
content_type :json
begin
response = BusinessRulesValidator.new(params).validate_document
[200, response.to_json]
rescue => e
error = { message: e.message }
[500, error.to_json]
end
end
class BusinessRulesValidator
VALID_SCHEMAS = [
"http://www.xbrl-ie.net/public/ci/2012-12-01/gaap/core/2012-12-01/ie-gaap-full-2012-12-01.xsd",
"http://www.xbrl-ie.net/public/ci/2012-12-01/ifrs/core/2012-12-01/ie-ifrs-full-2012-12-01.xsd",
"http://www.xbrl-ie.net/public/ci/2016-08-01/FRS-101/2016-08-01/FRS-101-ie-2016-08-01.xsd",
"http://www.xbrl-ie.net/public/ci/2016-08-01/FRS-102/2016-08-01/FRS-102-ie-2016-08-01.xsd",
"http://www.xbrl-ie.net/public/ci/2016-08-01/IFRS/2016-08-01/IFRS-ie-2016-08-01.xsd"
]
MANDATORY_TAGS = [
'uk-bus:EntityCurrentLegalOrRegisteredName',
'uk-bus:StartDateForPeriodCoveredByReport',
'uk-bus:EndDateForPeriodCoveredByReport',
'uk-gaap:ProfitLossOnOrdinaryActivitiesBeforeTax'
]
VALID_CONTEXT_ENTITY_IDENTIFIERS = [
"http://www.revenue.ie/",
"http://www.cro.ie/",
]
def initialize(params)
@messages = {}
file = File.open(File.join('/ixbrl', params["filename"]))
@doc = Nokogiri::XML(file)
@log = Logger.new(File.new(File.join(__dir__, "dev.log"), 'w'))
end
def validate_document
@messages[:validate_schema] = validate_schema
@messages[:schema_ref] = schema_ref
@messages[:mandatory_tags_present] = mandatory_tags_present
@messages[:period_dates] = period_dates
@messages[:duplicate_facts] = duplicate_facts
@messages[:context_scheme_consistent] = context_scheme_consistent
@messages[:context_scheme_allowed] = context_scheme_allowed
@messages[:context_identifiers] = context_identifiers
@messages[:cro_number_required] = cro_number_required
@messages
end
def validate_schema
errors = []
validate_ixbrl_schemas(errors)
validate_xbrl_schemas(errors)
message = errors.any? ? "invalid" : "valid"
{message: message, errors: errors}
end
def validate_ixbrl_schemas(errors)
begin
file = "./xsd/xhtml-inlinexbrl-1_0.xsd"
xsd = Nokogiri::XML::Schema(File.open(file))
xsd.validate(@doc).each do |error|
errors << error
end
rescue Nokogiri::XML::SyntaxError => e
errors << "caught exception: #{e}"
@log.error("caught exception: #{e}")
end
end
def validate_xbrl_schemas(errors)
begin
xbrl_xml = Converter.new(@doc).to_xbrl
xbrl_doc = Nokogiri::XML(xbrl_xml)
file = "./xsd/ie/ie-gaap-full-2012-12-01.xsd"
xsd = Nokogiri::XML::Schema(File.open(file))
xsd.validate(xbrl_doc).each do |error|
errors << error
end
rescue Nokogiri::XML::SyntaxError => e
errors << "caught exception: #{e}"
@log.error("caught exception: #{e}")
end
end
def schema_ref
schemaRef = @doc.xpath('//link:schemaRef').first.attributes["href"].value
if VALID_SCHEMAS.include? schemaRef
"valid"
else
"invalid: #{schemaRef} is not a valid schema"
end
end
def mandatory_tags_present
missing_tags = check_for_missing_tags
if missing_tags.any?
"invalid: the following tags are missing or empty #{missing_tags.join('; ')}"
else
"valid"
end
end
# The following checks require data on ROS services and therefore are n/a:
# - Report period start date cannot be later than the selected Revenue accounting period start date (<value>).
# - Report period end date cannot be before the selected Revenue accounting period end date (<value>).
# - Report period end date must fall within the selected Revenue accounting period.
def period_dates
end_date = @doc.xpath("//*[@name='uk-bus:EndDateForPeriodCoveredByReport']")&.first&.text&.strip
if end_date && (Time.parse(end_date) > Time.parse(" 2011-12-31"))
"valid"
else
"invalid: Period End Date is #{end_date} but must be 2011-12-31 or later"
end
end
def duplicate_facts
# exclude facts where tupleRef attr is present as they can have duplicate values
facts = @doc.xpath("//ix:nonFraction[not(@tupleRef)] | //ix:nonNumeric[not(@tupleRef)]")
dictionary = {}
duplicate_facts = []
facts.each do |fact|
value = fact_value(fact) # need to transform currency values before comparing
name = fact.attributes["name"].value
context = fact.attributes["contextRef"].value
dictionary[name] ||= {}
if dictionary[name][context].nil?
dictionary[name][context] = { value: value, line_number: fact.line }
elsif dictionary[name][context][:value] != value
duplicate_fact = add_duplicate_fact(fact, dictionary[name][context])
duplicate_facts << duplicate_fact
end
end
@log.debug("dictionary is #{dictionary.inspect}")
message = duplicate_facts.any? ? "invalid" : "valid"
{ message: message, duplicate_facts: duplicate_facts }
end
def context_scheme_consistent
context_schemes = @doc.xpath("//*[@scheme]").map { |c| c.attributes["scheme"].value }.uniq
if context_schemes.length > 1
"invalid: Contexts do not all use the same identifier and the same scheme"
else
"valid"
end
end
def context_scheme_allowed
context_scheme = @doc.xpath("//*[@scheme]").first.attributes["scheme"].value
if VALID_CONTEXT_ENTITY_IDENTIFIERS.include? context_scheme
"valid"
else
"invalid: #{context_scheme} is not a valid scheme"
end
end
def context_identifiers
tax_numbers = @doc.xpath("//*[@scheme='http://www.revenue.ie/']").map { |c| c.text }.uniq
cro_numbers = @doc.xpath("//*[@scheme='http://www.cro.ie/']").map { |c| c.text }.uniq
if tax_numbers.all? { |id| id =~ /^\d{7}\D{1,2}$/ }
# a seven digit number with one or two check characters
"valid"
elsif cro_numbers.all? { |id| id =~ /^\d{5,6}$/ }
# up to six digits with no alpha characters
"valid"
else
"invalid: One or more context_identifer numbers malformed or missing"
end
end
def cro_number_required
context_schemes = @doc.xpath("//*[@scheme]").map { |c| c.attributes["scheme"].value }.uniq
return "valid" unless context_schemes.include? "http://www.cro.ie/"
tag_content = @doc.xpath("//ix:nonNumeric[@name='ie-common:CompaniesRegistrationOfficeNumber']")&.first&.text
if tag_content.to_s.strip.empty?
"invalid: Must tag CRO number if using http://www.cro.ie/ identifier scheme"
else
"valid"
end
end
private
def add_duplicate_fact(fact, entry)
{
name: fact.attributes["name"].value,
context: fact.attributes["contextRef"].value,
value: fact.text,
line_number: fact.line,
conflicting_fact: entry
}
end
def check_for_missing_tags
MANDATORY_TAGS.map do |tag|
tag_content = @doc.xpath("//*[@name='#{tag}']")&.first&.text
tag if tag_content.to_s.strip.empty?
end.compact
end
def fact_value(fact)
if fact.name == "nonFraction"
currency_value = fact.text.gsub(/\D/, '').to_i
fact.attributes["sign"]&.value == '-' ? (currency_value * -1) : currency_value
else
fact.text
end
end
end