contrib: introduce `refund` calculator
authorAlexandre Terrasa <alexandre@moz-code.org>
Wed, 6 May 2015 17:08:48 +0000 (13:08 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Mon, 11 May 2015 00:48:32 +0000 (20:48 -0400)
This project was used to the correction of the course
INF2015: Développement de logiciels dans un environnement Agile"

Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

contrib/refund/src/refund.nit [new file with mode: 0644]
contrib/refund/src/refund_base.nit [new file with mode: 0644]
contrib/refund/src/refund_json.nit [new file with mode: 0644]

diff --git a/contrib/refund/src/refund.nit b/contrib/refund/src/refund.nit
new file mode 100644 (file)
index 0000000..02e0af1
--- /dev/null
@@ -0,0 +1,132 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# 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 (<input_file> <output_file> | [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 <input.json> <output.json>"
+       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 (file)
index 0000000..fbd5fee
--- /dev/null
@@ -0,0 +1,421 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# 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 (file)
index 0000000..33032b3
--- /dev/null
@@ -0,0 +1,326 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# 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