From 858dfdc2a8a0561653da3419eeaa30d1b963aaf3 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 6 May 2015 13:08:48 -0400 Subject: [PATCH] contrib: introduce `refund` calculator MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This project was used to the correction of the course INF2015: Développement de logiciels dans un environnement Agile" Signed-off-by: Alexandre Terrasa --- contrib/refund/src/refund.nit | 132 +++++++++++ contrib/refund/src/refund_base.nit | 421 ++++++++++++++++++++++++++++++++++++ contrib/refund/src/refund_json.nit | 326 ++++++++++++++++++++++++++++ 3 files changed, 879 insertions(+) create mode 100644 contrib/refund/src/refund.nit create mode 100644 contrib/refund/src/refund_base.nit create mode 100644 contrib/refund/src/refund_json.nit diff --git a/contrib/refund/src/refund.nit b/contrib/refund/src/refund.nit new file mode 100644 index 0000000..02e0af1 --- /dev/null +++ b/contrib/refund/src/refund.nit @@ -0,0 +1,132 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2015 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Insurance refunds calculation tool. +# +# `refund` computes automatically the allowed refund for a reclamation according +# to an insurrance policy. +# +# Usage: +# +# ~~~sh +# > refund ( | [OPTIONS]) +# ~~~ +# +# Input file: +# +# `refund` expects a JSON input file on the form: +# +# ~~~json +# { +# "dossier": "A100323", +# "mois": "2015-01", +# "reclamations": [ +# { +# "soin": 100, +# "date": "2015-01-11", +# "montant": "234.00$" +# }, { +# "soin": 200, +# "date": "2015-01-13", +# "montant": "128.00$" +# }, { +# "soin": 334, +# "date": "2015-01-23", +# "montant": "50.00$" +# } +# ] +# } +# ~~~ +# +# Output file: +# +# You have to specify the path where `refund` should output the result file. +# +# Results are formatted as JSON: +# +# ~~~json +# { +# "client": "100323", +# "mois": "2015-01", +# "remboursements": [ +# { +# "soin": 100, +# "date": "2015-01-11", +# "montant": "58.50$" +# }, { +# "soin": 200, +# "date": "2015-01-13", +# "montant": "22.50$" +# }, { +# "soin": 334, +# "date": "2015-01-23", +# "montant": "0.00$" +# } +# ] +# } +# ~~~ +# +# Options: +# +# `refund` can generate statistics about reclamations and refunds computed. +# +# * `-S`: display statistics +# * `-SR`: reset statistics +# +# Error handling: +# +# In case of error, a JSON object is generated in place of the output file: +# +# ~~~json +# { "message": "Invalid input data" } +# ~~~ +module refund + + +import refund_json + +# Display usage in console then leave. +fun usage do + print "" + print "Usage:" + print "refund " + print "" + print "options" + print " -S\tShow stats in console" + print " -RS\tClear stats" + exit 1 +end + +var proc = new RefundProcessor + +if args.length == 1 then + var flag = args.first + if flag == "-RS" then + proc.clear_stats + exit 0 + else if flag == "-S" then + proc.show_stats + exit 0 + else + print "Error: Unknown flag {flag}." + usage + end +else if args.length != 2 then + print "Error: Incorrect number of arguments. Got {args.length}, expected 2." + usage +end + +proc.process(args[0], args[1]) diff --git a/contrib/refund/src/refund_base.nit b/contrib/refund/src/refund_base.nit new file mode 100644 index 0000000..fbd5fee --- /dev/null +++ b/contrib/refund/src/refund_base.nit @@ -0,0 +1,421 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2015 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Insurance refunds calculation base classes. +module refund_base + +import counter + +# `RefundProcessor` manages the calculation of the refunds. +# +# See `process`. +class RefundProcessor + + # Where to generate output file. + var output_file: String is noinit, writable + + # Where to save usage statistics. + var stats_file = "stats.json" + + # Processes the `input_file` and write the output in `output_file`. + # + # Steps: + # + # 1. Parses the input_file and check json validity (see `load_input`). + # 2. Instantiates and checks the reclamation sheet against client rules + # (see `ReclamationSheet.from_json`). + # 3. Processes refunds (see `proces_refunds`). + # 4. Writes the output file (see `write_output`). + fun process(input_file, output_file: String) is abstract + + # Refunds allowed for the current reclamation sheet. + var current_refunds = new HashMap[Care, Dollar] + + # Computes allowed refunds for a given `Reclamation` found in a `ReclamationSheet`. + fun process_refund(sheet: ReclamationSheet, recl: Reclamation): Dollar is abstract + + # Shows stats values in console + fun show_stats do print load_stats + + # Loads stats from file as a RefundStats instance. + fun load_stats: RefundStats is abstract + + # Saves stats in file. + fun save_stats(stats: RefundStats) is abstract + + # Outputs error object then exit. + fun die(msg: String) is abstract + + # Clears stats. + # + # Basically delete the stats file. + fun clear_stats do if stats_file.file_exists then stats_file.file_delete +end + +# Stats representation using a `Counter`. +class RefundStats + super Counter[String] +end + +# A `Client` can ask for refunds from the insurance company. +class Client + + # Client number. + var number: String + + redef fun to_s do return "#{number}" +end + +# A `ReclamationSheet` is filled by the `Client` to obtain a `RefundSheet`. +class ReclamationSheet + + # File used for this refund. + var file: ReclFile is writable + + # Month concerned by the refund. + var month: ReclMonth is writable + + # Array of reclamations. + var recls = new Array[Reclamation] is writable + + redef fun to_s do + return "Refund (file: {file}, month: {month}, recls: {recls.length})" + end +end + +# A File found in a `ReclamationSheet`. +# +# A File points to a `Contract` and a `Client`. +# +# Allowed format is: `X12345` where `X` is the contract kind and `12345` is the +# client number. +class ReclFile + + # File string id. + var id: String is writable + + # Contract instance linked to this file. + var contract: Contract is noinit, writable + + # Client instance linked to this file. + var client: Client is noinit, writable + + # Returns the contract instance corresponding to `kind`. + fun contract_factory(proc: RefundProcessor, kind: String): Contract do + if kind == "A" then return new ContractA + if kind == "B" then return new ContractB + if kind == "C" then return new ContractC + if kind == "D" then return new ContractD + if kind == "E" then return new ContractE + proc.die("Unknown contract {kind}") + abort + end + + redef fun to_s do return "{contract.kind}{client.number}" +end + +# Month date formatted for contracts. +# +# Mainly used to factorize treatments on date calculation. +class ReclMonth + + # Internal date used to store the month. + var date: ReclDate is writable + + # Is `date` in this month? + fun has(date: ReclDate): Bool do return self.date.month == date.month + + redef fun to_s do + if date.month < 10 then + return "{date.year}-0{date.month}" + end + return "{date.year}-0{date.month}" + end +end + +# The date on which a `Care` occured. +class ReclDate + # Year of the month. + var year: Int is writable + + # Month number (`1` is January). + var month: Int is writable + + # Day number. + var day: Int is writable + + redef fun to_s do + var res = new FlatBuffer + res.append "{year}-" + if month < 10 then + res.append "0{month}-" + else + res.append "{month}-" + end + if day < 10 then + res.append "0{day}" + else + res.append day.to_s + end + return res.write_to_string + end +end + +# `RefundRecl` are parts of the `RefundReclamation`. +class Reclamation + # `Care` id concerned by this reclamation. + var care_id: Int is writable + + # Date this care was applied. + var date: ReclDate is writable + + # Amount of money given by the `Client` in exchange of this care. + var fees: Dollar is writable + + redef fun to_s do return "Entry (care: {care_id}, date: {date}, fees: {fees})" +end + +# A `Contract` specifies the refund applicable on care. +class Contract + + # Kind of the contract (specified by a letter). + var kind: String is noinit, writable + + # Covered cares for this kind of contract. + var cares = new Array[Care] is writable + + # Adds a care to this contract. + fun add_care(care: Care) do cares.add care + + # Gets a `Care` instance by its id. + # + # Returns `null` if no `Care` found. + fun care_by_id(id: Int): nullable Care do + for care in cares do + if care.match_id(id) then return care + end + return null + end + + redef fun to_s do return "{kind} ({cares.length} cares)" +end + +# Contracts +# FIXME move contracts to a JSON configuration file. + +private class ContractA + super Contract + + init do + kind = "A" + add_care(new UniqCare.with_vals(0, 25.0, null, null)) + add_care(new UniqCare.with_vals(100, 35.0, null, 250.0.to_dollar)) + add_care(new UniqCare.with_vals(150, 0.0, null, null)) + add_care(new UniqCare.with_vals(175, 50.0, null, 200.0.to_dollar)) + add_care(new UniqCare.with_vals(200, 25.0, null, 250.0.to_dollar)) + add_care(new RangeCare.with_vals([300..399], 0.0, null, null)) + add_care(new UniqCare.with_vals(400, 0.0, null, null)) + add_care(new UniqCare.with_vals(500, 25.0, null, 150.0.to_dollar)) + add_care(new UniqCare.with_vals(600, 40.0, null, 300.0.to_dollar)) + add_care(new UniqCare.with_vals(700, 0.0, null, null)) + end +end + +private class ContractB + super Contract + + init do + kind = "B" + add_care(new UniqCare.with_vals(0, 50.0, 40.0.to_dollar, null)) + add_care(new UniqCare.with_vals(100, 50.0, 50.0.to_dollar, 250.0.to_dollar)) + add_care(new UniqCare.with_vals(150, 0.0, null, null)) + add_care(new UniqCare.with_vals(175, 75.0, null, 200.0.to_dollar)) + add_care(new UniqCare.with_vals(200, 100.0,null, 250.0.to_dollar)) + add_care(new RangeCare.with_vals([300..399], 50.0, null, null)) + add_care(new UniqCare.with_vals(400, 0.0, null, null)) + add_care(new UniqCare.with_vals(500, 50.0, 50.0.to_dollar, 150.0.to_dollar)) + add_care(new UniqCare.with_vals(600, 100.0,null, 300.0.to_dollar)) + add_care(new UniqCare.with_vals(700, 70.0, null, null)) + end +end + +private class ContractC + super Contract + + init do + kind = "C" + add_care(new UniqCare.with_vals(0, 90.0, null, null)) + add_care(new UniqCare.with_vals(100, 95.0, null, 250.0.to_dollar)) + add_care(new UniqCare.with_vals(150, 85.0, null, null)) + add_care(new UniqCare.with_vals(175, 90.0, null, 200.0.to_dollar)) + add_care(new UniqCare.with_vals(200, 90.0, null, 250.0.to_dollar)) + add_care(new RangeCare.with_vals([300..399], 90.0, null, null)) + add_care(new UniqCare.with_vals(400, 90.0, null, null)) + add_care(new UniqCare.with_vals(500, 90.0, null, 150.0.to_dollar)) + add_care(new UniqCare.with_vals(600, 75.0, null, 300.0.to_dollar)) + add_care(new UniqCare.with_vals(700, 90.0, null, null)) + end +end + +private class ContractD + super Contract + + init do + kind = "D" + add_care(new UniqCare.with_vals(0, 100.0, 85.0.to_dollar, null)) + add_care(new UniqCare.with_vals(100, 100.0, 75.0.to_dollar, 250.0.to_dollar)) + add_care(new UniqCare.with_vals(150, 100.0, 150.0.to_dollar, null)) + add_care(new UniqCare.with_vals(175, 95.0, null, 200.0.to_dollar)) + add_care(new UniqCare.with_vals(200, 100.0, 100.0.to_dollar, 250.0.to_dollar)) + add_care(new RangeCare.with_vals([300..399],100.0, null, null)) + add_care(new UniqCare.with_vals(400, 100.0, 65.0.to_dollar, null)) + add_care(new UniqCare.with_vals(500, 100.0, null, 150.0.to_dollar)) + add_care(new UniqCare.with_vals(600, 100.0, 100.0.to_dollar, 300.0.to_dollar)) + add_care(new UniqCare.with_vals(700, 100.0, 90.0.to_dollar, null)) + end +end + +private class ContractE + super Contract + + init do + kind = "E" + add_care(new UniqCare.with_vals(0, 15.0, null, null)) + add_care(new UniqCare.with_vals(100, 25.0, null, 250.0.to_dollar)) + add_care(new UniqCare.with_vals(150, 15.0, null, null)) + add_care(new UniqCare.with_vals(175, 25.0, 20.0.to_dollar, 200.0.to_dollar)) + add_care(new UniqCare.with_vals(200, 12.0, null, 250.0.to_dollar)) + add_care(new RangeCare.with_vals([300..399], 60.0, null, null)) + add_care(new UniqCare.with_vals(400, 25.0, 15.0.to_dollar, null)) + add_care(new UniqCare.with_vals(500, 30.0, 20.0.to_dollar, 150.0.to_dollar)) + add_care(new UniqCare.with_vals(600, 15.0, null, 300.0.to_dollar)) + add_care(new UniqCare.with_vals(700, 22.0, null, null)) + end +end + +# A `Care` is payed by the `Client` and can raises a `Refund`. +interface Care + + # Does `id` is acceptable for this care? + fun match_id(id: Int): Bool is abstract + + # Percent covered for this kind of care. + fun cover: Float is abstract + + # Max amount covered for this kind of care by reclamation. + fun max: nullable Dollar is abstract + + # Max amount covered for this kind of care by month. + fun month_max: nullable Dollar is abstract + + # Computes the refund for this care. + fun process_refund(fees: Dollar): Dollar do + var max = self.max + var val = ((fees.value.to_f * (cover / 100.0)) / 100.0).to_dollar + if max != null and val > max then val = max + return val + end +end + +# A `UniqCare` refers to one and only one kind of `Care`. +# +# For example, the care `Ostéopathie` as the uniq id `200`. +class UniqCare + super Care + + # Care id. + var id: Int + + redef fun match_id(id) do return self.id == id + + redef var cover = 0.0 + redef var max = null + redef var month_max = null + + # Inits this `Care` with values. + # + # * `id`: the `Care` id. + # * `cover`: refund percentage covered for this `Care`. + # * `max`: max amount refunded for this `Care` in a reclamation sheet. + # * `month_max`: max amount refunded by month. + init with_vals(id: Int, cover: Float, max, month_max: nullable Dollar) do + self.id = id + self.cover = cover + self.max = max + self.month_max = month_max + end + + redef fun to_s do return id.to_s +end + +# A `RangeCare` refers to a set of id corresponding to the same `Care`. +# +# For example, the care `Soins Dentaires` is refered by the ids 300 to 399. +class RangeCare + super Care + + # Care id range. + var id: Range[Int] + + redef fun match_id(id) do return self.id.has(id) + redef var cover = 0.0 + redef var max = null + redef var month_max = null + + # Inits this `Care` with values. + # + # * `id`: the `Care` id. + # * `cover`: refund percentage covered for this `Care`. + # * `max`: max amount refunded for this `Care` in a reclamation sheet. + # * `month_max`: max amount refunded by month. + init with_vals(id: Range[Int], cover: Float, max, month_max: nullable Dollar) do + self.id = id + self.cover = cover + self.max = max + self.month_max = month_max + end + + redef fun to_s do return id.first.to_s +end + +# Used to represent currencies values. +class Dollar + super Comparable + + redef type OTHER: Dollar + + # Amount of cents. + var value: Int + + # Inits `self` from a float `value`. + init from_float(value: Float) do + self.value = (value * 100.0).to_i + end + + redef fun to_s do return "{value / 100}.{value % 100}$" + redef fun <(o) do return value < o.value + + # Dollars addition. + fun +(o: Dollar): Dollar do return new Dollar(value + o.value) + + # Dollars substraction. + fun -(o: Dollar): Dollar do return new Dollar(value - o.value) +end + +redef class Float + # Returns `self` as a Dollar instance. + fun to_dollar: Dollar do return new Dollar.from_float(self) +end diff --git a/contrib/refund/src/refund_json.nit b/contrib/refund/src/refund_json.nit new file mode 100644 index 0000000..33032b3 --- /dev/null +++ b/contrib/refund/src/refund_json.nit @@ -0,0 +1,326 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2015 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# JSON handling for `refund`. +module refund_json + +import refund_base +import json::static + +redef class RefundProcessor + + redef fun process(input_file, output_file) do + self.output_file = output_file + var json = load_input(input_file) + var sheet = new ReclamationSheet.from_json(self, json) + var res = process_refunds(sheet) + write_output(res.to_pretty_json, output_file) + end + + # Computes allowed refunds for a given `ReclamationSheet`. + fun process_refunds(sheet: ReclamationSheet): JsonObject do + # update stats + var stats = load_stats + stats.inc("total_treatments") + # compute refunds + current_refunds.clear + var json = new JsonObject + json["dossier"] = sheet.file.to_s + json["mois"] = sheet.month.to_s + var arr = new JsonArray + var sum = 0.0.to_dollar + for recl in sheet.recls do + var refund = process_refund(sheet, recl) + var obj = new JsonObject + obj["soin"] = recl.care_id + obj["date"] = recl.date.to_s + obj["montant"] = refund.to_s + arr.add obj + sum += refund + # update stats for care + stats.inc("total_{recl.care_id}") + end + save_stats(stats) + json["remboursements"] = arr + json["total"] = sum.to_s + return json + end + + # Loads the input string and returns its content as a JsonObject. + # + # Dies if the file cannot be read or does not contain a valid JSONObject. + fun load_input(file: String): JsonObject do + if not file.file_exists then + die("File `{file}` not found.") + abort + end + var ptr = new FileReader.open(file) + var json = ptr.read_all.parse_json + if json isa JsonParseError then + die("Wrong input file ({json.message})") + abort + else if not json isa JsonObject then + die("Wrong input type (expected JsonObject got {json.class_name})") + abort + end + ptr.close + return json + end + + # Writes `str` in path specified by `file`. + # + # Used to produce output and stats. + fun write_output(str: String, file: String) do + var ofs = new FileWriter.open(file) + ofs.write(str) + ofs.close + end + + # UTILS + + # Does `json` contains `key`? Dies otherwise. + private fun check_key(json: JsonObject, key: String) do + if json.has_key(key) then return + die("Malformed input (missing key {key})") + end + + # Does `str` match the regex `re`. + private fun check_format(str, re: String): Bool do + return str.has(re.to_re) + end + + redef fun die(msg) do + # save error + var obj = new JsonObject + obj["message"] = msg + write_output(obj.to_pretty_json, output_file) + # update stats + var stats = load_stats + stats.inc("total_reject") + save_stats(stats) + # leave + exit 1 + end + + redef fun show_stats do print load_stats.to_json.to_pretty_json + + redef fun load_stats do + # If no stats found, return a new object + if not stats_file.file_exists then return new RefundStats + # Try to read from file + var ifs = new FileReader.open(stats_file) + var content = ifs.read_all.parse_json + ifs.close + # If file is corrupted, return a new object + if not content isa JsonObject then return new RefundStats + # Return file contained stats + return new RefundStats.from_json(content) + end + + redef fun save_stats(stats: RefundStats) do + write_output(stats.to_json.to_pretty_json, stats_file) + end +end + +redef class RefundStats + + # Inits `self` from the content of a JsonObject + init from_json(json: JsonObject) do + for k, v in json do self[k] = v.as(Int) + end + + # Outputs `self` as a JSON string. + fun to_json: JsonObject do + var obj = new JsonObject + for k, v in self do obj[k] = v + return obj + end +end + +redef class ReclamationSheet + + # Inits `self` from the content of a `JsonObject`. + init from_json(proc: RefundProcessor, json: JsonObject) do + file = new ReclFile.from_json(proc, json) + month = new ReclMonth.from_json(proc, json) + recls = parse_recls(proc, json) + init(file, month) + end + + # Parses and checks the given `json` then returns an array of `Reclamation` instances. + private fun parse_recls(proc: RefundProcessor, json: JsonObject): Array[Reclamation] do + proc.check_key(json, "reclamations") + var res = new Array[Reclamation] + var recls = json["reclamations"] + if not recls isa JsonArray then + proc.die("Wrong type for `number` (expected JsonArray got {recls.class_name})") + abort + end + var i = 0 + for obj in recls do + if not obj isa JsonObject then + proc.die("Wrong type for `reclamations#{i}` " + + "(expected JsonObject got {obj.class_name})") + abort + end + var recl = new Reclamation.from_json(proc, obj) + if not month.has(recl.date) then + proc.die("Wrong `mois` for `soin` with id `{recl.care_id}`") + abort + end + if file.contract.care_by_id(recl.care_id) == null then + proc.die("Unknown `soin` with id `{recl.care_id}`") + abort + end + res.add recl + i += 1 + end + return res + end +end + +redef class ReclFile + # Inits `self` from the content of a JsonObject. + init from_json(proc: RefundProcessor, json: JsonObject) do + proc.check_key(json, "dossier") + var id = json["dossier"] + if not id isa String then + proc.die("Wrong type for `dossier` (expected String got {id.class_name})") + abort + end + # Check format + parse_contract(proc, id) + parse_client(proc, id) + init(id) + end + + # Tries to parse the contract from `file_id` string. + private fun parse_contract(proc: RefundProcessor, file_id: String) do + var kind = file_id.first.to_s + if not proc.check_format(kind, "^[A-E]\{1\}$") then + proc.die("Wrong contract (expected A, B, C, D or E got {kind})") + end + contract = contract_factory(proc, kind) + end + + # Tries to parse the client number from the `file_id` string. + private fun parse_client(proc: RefundProcessor, file_id: String) do + var num = file_id.substring_from(1) + if not proc.check_format(num, "^[0-9]\{6\}$") then + proc.die("Wrong format for `number` (expected XXXXXX got {num})") + abort + end + client = new Client(num) + end +end + +redef class ReclMonth + # Inits `self` from a `JsonObject`. + init from_json(proc: RefundProcessor, json: JsonObject) do + proc.check_key(json, "mois") + var month = json["mois"] + if not month isa String then + proc.die("Wrong type for `mois` (expected String got {month.class_name})") + return + end + if not proc.check_format(month, "^[0-9]\{4\}-[0-9]\{2\}$") then + proc.die("Wrong format for `mois` (expected AAAA-MM got {month})") + return + end + from_string(proc, month) + end + + # Inits `self` from a string representation formatted as `AAAA-MM`. + init from_string(proc: RefundProcessor, str: String) do + var parts = str.split("-") + var year = parts[0].to_i + var month = parts[1].to_i + if month < 1 or month > 12 then + proc.die("Wrong format for `mois` (expected AAAA-MM got {str})") + return + end + date = new ReclDate(year, month, 1) + init(date) + end +end + +redef class ReclDate + # Inits `self` from a `JsonObject`. + # + # Dies if the `json` input is invalid. + init from_json(proc: RefundProcessor, json: JsonObject) do + proc.check_key(json, "date") + var date = json["date"] + if not date isa String then + proc.die("Wrong type for `date` (expected String got {date.class_name})") + abort + end + if not proc.check_format(date, "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}$") then + proc.die("Wrong format for `date` (expected AAAA-MM-DD got {date})") + abort + end + from_string(proc, date) + end + + # Inits `self` from its string representation formatted as `AAAA-MM`. + init from_string(proc: RefundProcessor, str: String) do + var parts = str.split("-") + year = parts[0].to_i + month = parts[1].to_i + day = parts[2].to_i + if month < 1 or month > 12 or day < 1 or day > 31 then + proc.die("Wrong format for `mois` (expected AAAA-MM got {str})") + abort + end + init(year, month, day) + end +end + +redef class Reclamation + # Inits `self` from a `JsonObject`. + init from_json(proc: RefundProcessor, json: JsonObject) do + care_id = parse_care_id(proc, json) + date = new ReclDate.from_json(proc, json) + fees = parse_fees(proc, json) + init(care_id, date, fees) + end + + # Inits `self` from its string representation formatted as `Int`. + private fun parse_care_id(proc: RefundProcessor, json: JsonObject): Int do + proc.check_key(json, "soin") + var id = json["soin"] + if not id isa Int then + proc.die("Wrong type for `soin` (expected Int got {id.class_name})") + abort + end + return id + end + + # Inits `self` from its string representation formatted as `0.00$`. + private fun parse_fees(proc: RefundProcessor, json: JsonObject): Dollar do + proc.check_key(json, "montant") + var fees = json["montant"] + if not fees isa String then + proc.die("Wrong type for `fees` (expected String got {fees.class_name})") + abort + end + if not proc.check_format(fees, "^[0-9]+((\\.|\\,)[0-9]+)?\\$$") then + proc.die("Wrong format for `montant` (expected XX.XX$ got {fees})") + abort + end + return new Dollar.from_float(fees.basename("$").to_f) + end +end -- 1.7.9.5