7c5e4d0bc7e61a8cd2688fd65a5924db3f205230
[nit.git] / contrib / refund / src / refund_json.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2015 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # JSON handling for `refund`.
18 module refund_json
19
20 import refund_base
21 import json::static
22
23 redef class RefundProcessor
24
25 redef fun process(input_file, output_file) do
26 self.output_file = output_file
27 var json = load_input(input_file)
28 var sheet = new ReclamationSheet.from_json(self, json)
29 var res = process_refunds(sheet)
30 write_output(res.to_pretty_json, output_file)
31 end
32
33 # Computes allowed refunds for a given `ReclamationSheet`.
34 fun process_refunds(sheet: ReclamationSheet): JsonObject do
35 # update stats
36 var stats = load_stats
37 stats.inc("total_treatments")
38 # compute refunds
39 current_refunds.clear
40 var json = new JsonObject
41 json["dossier"] = sheet.file.to_s
42 json["mois"] = sheet.month.to_s
43 var arr = new JsonArray
44 var sum = 0.0.to_dollar
45 for recl in sheet.recls do
46 var refund = process_refund(sheet, recl)
47 var obj = new JsonObject
48 obj["soin"] = recl.care_id
49 obj["date"] = recl.date.to_s
50 obj["montant"] = refund.to_s
51 arr.add obj
52 sum += refund
53 # update stats for care
54 stats.inc("total_{recl.care_id}")
55 end
56 save_stats(stats)
57 json["remboursements"] = arr
58 json["total"] = sum.to_s
59 return json
60 end
61
62 # Loads the input string and returns its content as a JsonObject.
63 #
64 # Dies if the file cannot be read or does not contain a valid JSONObject.
65 fun load_input(file: String): JsonObject do
66 if not file.file_exists then
67 die("File `{file}` not found.")
68 abort
69 end
70 var ptr = new FileReader.open(file)
71 var json = ptr.read_all.parse_json
72 if json isa JsonParseError then
73 die("Wrong input file ({json.message})")
74 abort
75 else if json == null then
76 die("Unable to parse input file as json (got null)")
77 abort
78 else if not json isa JsonObject then
79 die("Wrong input type (expected JsonObject got {json.class_name})")
80 abort
81 end
82 ptr.close
83 return json
84 end
85
86 # Writes `str` in path specified by `file`.
87 #
88 # Used to produce output and stats.
89 fun write_output(str: String, file: String) do
90 var ofs = new FileWriter.open(file)
91 ofs.write(str)
92 ofs.write("\n")
93 ofs.close
94 end
95
96 # UTILS
97
98 # Does `json` contains `key`? Dies otherwise.
99 private fun check_key(json: JsonObject, key: String) do
100 if json.has_key(key) then return
101 die("Malformed input (missing key {key})")
102 end
103
104 # Does `str` match the regex `re`.
105 private fun check_format(str, re: String): Bool do
106 return str.has(re.to_re)
107 end
108
109 redef fun die(msg) do
110 # save error
111 var obj = new JsonObject
112 obj["message"] = msg
113 write_output(obj.to_pretty_json, output_file)
114 # update stats
115 var stats = load_stats
116 stats.inc("total_reject")
117 save_stats(stats)
118 # leave
119 exit 1
120 end
121
122 redef fun show_stats do print load_stats.to_json_object.to_pretty_json
123
124 redef fun load_stats do
125 # If no stats found, return a new object
126 if not stats_file.file_exists then return new RefundStats
127 # Try to read from file
128 var ifs = new FileReader.open(stats_file)
129 var content = ifs.read_all.parse_json
130 ifs.close
131 # If file is corrupted, return a new object
132 if not content isa JsonObject then return new RefundStats
133 # Return file contained stats
134 return new RefundStats.from_json(content)
135 end
136
137 redef fun save_stats(stats) do
138 write_output(stats.to_json_object.to_pretty_json, stats_file)
139 end
140 end
141
142 redef class RefundStats
143
144 # Inits `self` from the content of a JsonObject
145 init from_json(json: JsonObject) do
146 for k, v in json do self[k] = v.as(Int)
147 end
148
149 # Outputs `self` as a JSON string.
150 fun to_json_object: JsonObject do
151 var obj = new JsonObject
152 for k, v in self do obj[k] = v
153 return obj
154 end
155 end
156
157 redef class ReclamationSheet
158
159 # Inits `self` from the content of a `JsonObject`.
160 init from_json(proc: RefundProcessor, json: JsonObject) do
161 file = new ReclFile.from_json(proc, json)
162 month = new ReclMonth.from_json(proc, json)
163 recls = parse_recls(proc, json)
164 init(file, month)
165 end
166
167 # Parses and checks the given `json` then returns an array of `Reclamation` instances.
168 private fun parse_recls(proc: RefundProcessor, json: JsonObject): Array[Reclamation] do
169 proc.check_key(json, "reclamations")
170 var res = new Array[Reclamation]
171 var recls = json["reclamations"]
172 if recls == null then
173 proc.die("Wrong type for `number` (expected JsonArray got null)")
174 abort
175 else if not recls isa JsonArray then
176 proc.die("Wrong type for `number` (expected JsonArray got {recls.class_name})")
177 abort
178 end
179 var i = 0
180 for obj in recls do
181 if obj == null then
182 proc.die("Wrong type for `reclamations#{i}` (expected JsonObject got null)")
183 abort
184 else if not obj isa JsonObject then
185 proc.die("Wrong type for `reclamations#{i}` " +
186 "(expected JsonObject got {obj.class_name})")
187 abort
188 end
189 var recl = new Reclamation.from_json(proc, obj)
190 if not month.has(recl.date) then
191 proc.die("Wrong `mois` for `soin` with id `{recl.care_id}`")
192 abort
193 end
194 if file.contract.care_by_id(recl.care_id) == null then
195 proc.die("Unknown `soin` with id `{recl.care_id}`")
196 abort
197 end
198 res.add recl
199 i += 1
200 end
201 return res
202 end
203 end
204
205 redef class ReclFile
206 # Inits `self` from the content of a JsonObject.
207 init from_json(proc: RefundProcessor, json: JsonObject) do
208 proc.check_key(json, "dossier")
209 var id = json["dossier"]
210 if id == null then
211 proc.die("Wrong type for `dossier` (expected String got null)")
212 abort
213 else if not id isa String then
214 proc.die("Wrong type for `dossier` (expected String got {id.class_name})")
215 abort
216 end
217 # Check format
218 parse_contract(proc, id)
219 parse_client(proc, id)
220 init(id)
221 end
222
223 # Tries to parse the contract from `file_id` string.
224 private fun parse_contract(proc: RefundProcessor, file_id: String) do
225 var kind = file_id.first.to_s
226 if not proc.check_format(kind, "^[A-E]\{1\}$") then
227 proc.die("Wrong contract (expected A, B, C, D or E got {kind})")
228 end
229 contract = contract_factory(proc, kind)
230 end
231
232 # Tries to parse the client number from the `file_id` string.
233 private fun parse_client(proc: RefundProcessor, file_id: String) do
234 var num = file_id.substring_from(1)
235 if not proc.check_format(num, "^[0-9]\{6\}$") then
236 proc.die("Wrong format for `number` (expected XXXXXX got {num})")
237 abort
238 end
239 client = new Client(num)
240 end
241 end
242
243 redef class ReclMonth
244 # Inits `self` from a `JsonObject`.
245 init from_json(proc: RefundProcessor, json: JsonObject) do
246 proc.check_key(json, "mois")
247 var month = json["mois"]
248 if month == null then
249 proc.die("Wrong type for `mois` (expected String got null)")
250 return
251 else if not month isa String then
252 proc.die("Wrong type for `mois` (expected String got {month.class_name})")
253 return
254 end
255 if not proc.check_format(month, "^[0-9]\{4\}-[0-9]\{2\}$") then
256 proc.die("Wrong format for `mois` (expected AAAA-MM got {month})")
257 return
258 end
259 from_string(proc, month)
260 end
261
262 # Inits `self` from a string representation formatted as `AAAA-MM`.
263 init from_string(proc: RefundProcessor, str: String) do
264 var parts = str.split("-")
265 var year = parts[0].to_i
266 var month = parts[1].to_i
267 if month < 1 or month > 12 then
268 proc.die("Wrong format for `mois` (expected AAAA-MM got {str})")
269 return
270 end
271 date = new ReclDate(year, month, 1)
272 init(date)
273 end
274 end
275
276 redef class ReclDate
277 # Inits `self` from a `JsonObject`.
278 #
279 # Dies if the `json` input is invalid.
280 init from_json(proc: RefundProcessor, json: JsonObject) do
281 proc.check_key(json, "date")
282 var date = json["date"]
283 if date == null then
284 proc.die("Wrong type for `date` (expected String got null)")
285 abort
286 else if not date isa String then
287 proc.die("Wrong type for `date` (expected String got {date.class_name})")
288 abort
289 end
290 if not proc.check_format(date, "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}$") then
291 proc.die("Wrong format for `date` (expected AAAA-MM-DD got {date})")
292 abort
293 end
294 from_string(proc, date)
295 end
296
297 # Inits `self` from its string representation formatted as `AAAA-MM`.
298 init from_string(proc: RefundProcessor, str: String) do
299 var parts = str.split("-")
300 year = parts[0].to_i
301 month = parts[1].to_i
302 day = parts[2].to_i
303 if month < 1 or month > 12 or day < 1 or day > 31 then
304 proc.die("Wrong format for `mois` (expected AAAA-MM got {str})")
305 abort
306 end
307 init(year, month, day)
308 end
309 end
310
311 redef class Reclamation
312 # Inits `self` from a `JsonObject`.
313 init from_json(proc: RefundProcessor, json: JsonObject) do
314 care_id = parse_care_id(proc, json)
315 date = new ReclDate.from_json(proc, json)
316 fees = parse_fees(proc, json)
317 init(care_id, date, fees)
318 end
319
320 # Inits `self` from its string representation formatted as `Int`.
321 private fun parse_care_id(proc: RefundProcessor, json: JsonObject): Int do
322 proc.check_key(json, "soin")
323 var id = json["soin"]
324 if id == null then
325 proc.die("Wrong type for `soin` (expected Int got null)")
326 abort
327 else if not id isa Int then
328 proc.die("Wrong type for `soin` (expected Int got {id.class_name})")
329 abort
330 end
331 return id
332 end
333
334 # Inits `self` from its string representation formatted as `0.00$`.
335 private fun parse_fees(proc: RefundProcessor, json: JsonObject): Dollar do
336 proc.check_key(json, "montant")
337 var fees = json["montant"]
338 if fees == null then
339 proc.die("Wrong type for `fees` (expected String got null)")
340 abort
341 else if not fees isa String then
342 proc.die("Wrong type for `fees` (expected String got {fees.class_name})")
343 abort
344 end
345 if not proc.check_format(fees, "^[0-9]+((\\.|\\,)[0-9]+)?\\$$") then
346 proc.die("Wrong format for `montant` (expected XX.XX$ got {fees})")
347 abort
348 end
349 return new Dollar.from_float(fees.basename("$").to_f)
350 end
351 end