Commit 389af47c authored by David Foucher's avatar David Foucher
Browse files

Add remuneration api endpoint

parent 4101b858
Pipeline #1821 passed with stage
in 1 minute and 14 seconds
__pycache__/
.mypy_cache/
.pytest_cache/
docs/
tmp/
......
......@@ -12,6 +12,7 @@ from openapi_spec_validator import validate_spec
from trefle.config import FINANCEMENTS
from trefle.config import REMUNERATIONS
from trefle import VERSION
pytestmark = pytest.mark.asyncio
......@@ -55,6 +56,33 @@ async def test_simulate_endpoint(client):
result.raise_for_errors()
async def test_remuneration_endpoint(client):
resp = await client.get("/schema")
spec = create_spec(json.loads(resp.body))
resp = await client.post(
"/remuneration",
body={
"beneficiaire.age": 20,
"formation.region": 27,
"formation.codes_financeur": [2],
},
)
assert resp.status == HTTPStatus.OK
assert "remunerations" in json.loads(resp.body)
remunerations = json.loads(resp.body)["remunerations"]
assert remunerations
print(remunerations[0])
assert "remuneration" in remunerations[0]
assert "Version" in resp.headers
validator = ResponseValidator(spec)
request = MockRequest("http://trefle.pole-emploi.fr", "post", "/remuneration")
response = MockResponse(resp.body, resp.status.value)
result = validator.validate(request, response)
result.raise_for_errors()
async def test_simulate_endpoint_without_formation_prix_horaire(client):
resp = await client.get("/schema")
......
......@@ -37,3 +37,10 @@ def get_financements(tags=None):
for tag in tags or []:
financements = [f for f in financements if tag in f["tags"]]
return financements
def get_remunerations(tags=None):
remunerations = [config.Remuneration(f) for f in config.REMUNERATIONS]
for tag in tags or []:
remunerations = [f for f in remunerations if tag in f["tags"]]
return remunerations
......@@ -49,6 +49,77 @@ async def simulate_(request, response):
log_simulate(context, errors=error)
raise HttpError(HTTPStatus.UNPROCESSABLE_ENTITY, error)
eligible = request.query.bool("eligible", None)
if eligible is not None:
financements = [f for f in financements if f["eligible"] == eligible]
else:
financements = sorted(
financements, key=lambda value: value["eligible"], reverse=True
)
explain = request.query.bool("explain", False)
for financement in financements:
financement["explain"] = (
[s.json for s in financement["explain"]] if explain else None
)
body = {"financements": financements}
if request.query.bool("context", False):
body["context"] = {
k: v for k, v in context.items()
if k in SCHEMA and "label" in SCHEMA[k]}
if request.query.bool("scenario", False):
body["scenario"] = make_scenario(context, financements)
response.json = body
log_simulate(context, financements=financements)
@app.route("/remuneration", methods=["POST"])
async def remuneration_(request, response):
data = request.json
remunerations = get_remunerations(tags=request.query.list("tags", []))
try:
flatten(data)
context = Context(data.copy())
routine.extrapolate_context(context)
routine.preprocess(context)
for remuneration in remunerations:
copy = context.copy()
routine.check_remuneration(context, remuneration)
data.update(copy.cleaned_data)
# FIXME (limits of the single-store-all context object)
# Clean keys not meant to be exposed
for key in list(data.keys()):
if key.startswith("remuneration"):
del data[key]
except DataError as err:
error = {err.key: err.error}
log_simulate(context, errors=error)
raise HttpError(HTTPStatus.UNPROCESSABLE_ENTITY, error)
# TODO: explain only for financement see routine.py check_remuneration
# explain = request.query.bool("explain", False)
# for remuneration in remunerations:
# remuneration["explain"] = (
# [s.json for s in remunerations["explain"]] if explain else None
# )
body = {"remunerations": remunerations}
# if request.query.bool("context", False):
# body["context"] = {
# k: v for k, v in context.items()
# if k in SCHEMA and "label" in SCHEMA[k]}
response.json = body
"""
try:
await simulate(context, financements)
except DataError as err:
error = {err.key: err.error}
log_simulate(context, errors=error)
raise HttpError(HTTPStatus.UNPROCESSABLE_ENTITY, error)
eligible = request.query.bool("eligible", None)
if eligible is not None:
financements = [f for f in financements if f["eligible"] == eligible]
......@@ -71,6 +142,7 @@ async def simulate_(request, response):
response.json = body
log_simulate(context, financements=financements)
"""
app.route("/legacy", methods=["POST"])(simulate_legacy)
......
......@@ -14,6 +14,7 @@ from ..rules import Rule, SCHEMA, LABELS, RULES, IDCC
CONSTANTS = {}
FINANCEMENTS = []
REMUNERATIONS = []
ORGANISMES = {}
ROOT = Path(__file__).parent
RAW_RULES = {}
......@@ -67,6 +68,14 @@ def load_financements():
FINANCEMENTS.append(props)
def load_remunerations():
with (ROOT / "remunerations.yml").open() as f:
data = yaml.safe_load(f.read())
for id_, props in data.items():
props.setdefault("intitule", id_)
REMUNERATIONS.append(props)
def load_naf(data):
# Data from https://www.insee.fr/fr/information/2406147
from ..validators import format_naf
......@@ -143,6 +152,10 @@ class Organisme(SmartDict):
...
class Remuneration(SmartDict):
...
def init():
print("Initializing config")
with (ROOT / "schema.yml").open() as f:
......@@ -150,6 +163,7 @@ def init():
for id_, rules in load_dir_rules(ROOT / "rules"):
RULES[id_] = rules
load_financements()
load_remunerations()
with (ROOT / "organismes.yml").open() as f:
for name, data in yaml.safe_load(f.read()).items():
organisme = data
......
......@@ -86,8 +86,13 @@ def then_check_organisme(context, name):
@then(r"(?:le |la |l')(?P<label>.*) vaut (?P<value>.+)")
def then_check_output(context, label, value):
value = Pointer(value).get({})
key = LABELS[label][12:] # Remove "financement." namespace.
assert context.result[key] == value, (f'{context.result[key]} '
if(LABELS[label].startswith("financement")):
key = LABELS[label][12:] # Remove "financement." namespace.
if(LABELS[label].startswith("remuneration")):
key = LABELS[label][13:] # Remove "remuneration." namespace.
assert context.result[key] == value, (f'key = {key} '
f'label = {LABELS[label]}'
f'{context.result[key]} '
f'({type(context.result[key])}) != '
f'{value} ({type(value)})')
......
......@@ -61,6 +61,40 @@ paths:
schema:
$ref: '#/components/schemas/errors'
/api-moteur/0.7/financement: *baseendpoint
/remuneration: &baseremuendpoint
post:
summary: Lancer un calcul de remuneration
requestBody:
content:
'application/json':
schema:
$ref: '#/components/schemas/request'
examples:
compute-request:
$ref: '#/components/examples/compute-request'
responses:
200:
description: Calcul effectué avec succès
content:
application/json:
schema:
$ref: '#/components/schemas/collection'
examples:
compute-success:
$ref: '#/components/examples/compute-success'
422:
description: Les informations sont invalides
content:
application/json:
schema:
$ref: '#/components/schemas/errors'
400:
description: Le format de la requête est invalide
content:
application/json:
schema:
$ref: '#/components/schemas/errors'
/api-moteur/0.7/remuneration: *baseremuendpoint
components:
schemas:
request:
......
Rémunération Bourgogne Franche Comté:
tags: [remunuration, BFC]
racine: rémunération Bourgogne-Franche-Comté.rules
Si les codes financeur de la formation contiennent «Conseil régional»
Si l'allocation du bénéficiaire est définie
Alors la rémunération applicable est égale au montant de l'allocation du bénéficiaire
# Par défaut
Si l'âge du bénéficiaire est inférieur à 18
Et la rémunération applicable est inférieure à 455.01
......
......@@ -1043,6 +1043,81 @@ formation:
individuels:
type: boolean
label: formation ouverte aux bénéficiaires individuels
remuneration: &remuneration
intitule_remuneration: # add _remuneration to not overlap financement.intitule
description: le nom de la règle de rémuneration
type: string
public: true
label: intitulé de la régle de rémunération
remuneration:
description: rémunération applicable
type: number
format: float
public: true
nullable: true
label: rémunération applicable
remuneration_annee_2:
description: rémunération applicable la deuxième année
type: number
format: float
public: true
nullable: true
label: rémunération applicable la deuxième année
remuneration_annee_3:
description: rémunération applicable la troisième année
type: number
format: float
public: true
nullable: true
label: rémunération applicable la troisième année
remuneration_texte:
description: texte de la rémunération
type: string
public: true
nullable: true
label: texte de la rémunération
indemnite_conges_payes:
description: indemnité compensatrice de congés payés
type: integer
format: int32
public: true
nullable: true
label: indemnité compensatrice de congés payés
plafond_remuneration:
description: plafond de rémunération applicable
type: number
format: money
public: true
nullable: true
label: plafond de rémunération applicable
rff:
description: montant de la RFF si applicable
type: number
format: float
public: true
nullable: true
label: RFF applicable
debut_rff:
description: date de début de la RFF applicable
type: string
format: date
public: true
nullable: true
label: date de début de la RFF applicable
fin_rff:
description: date de fin de la RFF applicable
type: string
format: date
public: true
nullable: true
label: date de fin de la RFF applicable
fin_remuneration:
description: date de fin de la rémunération applicable
type: string
format: date
nullable: true
public: true
label: date de fin de la rémunération applicable
financement:
intitule:
description: le nom du financement
......@@ -1104,47 +1179,6 @@ financement:
type: string
public: true
label: texte des démarches
remuneration:
description: rémunération applicable
type: number
format: float
public: true
nullable: true
label: rémunération applicable
remuneration_annee_2:
description: rémunération applicable la deuxième année
type: number
format: float
public: true
nullable: true
label: rémunération applicable la deuxième année
remuneration_annee_3:
description: rémunération applicable la troisième année
type: number
format: float
public: true
nullable: true
label: rémunération applicable la troisième année
remuneration_texte:
description: texte de la rémunération
type: string
public: true
nullable: true
label: texte de la rémunération
indemnite_conges_payes:
description: indemnité compensatrice de congés payés
type: integer
format: int32
public: true
nullable: true
label: indemnité compensatrice de congés payés
plafond_remuneration:
description: plafond de rémunération applicable
type: number
format: money
public: true
nullable: true
label: plafond de rémunération applicable
prise_en_charge:
description: valeur définie seulement si le prix horaire de la formation est défini
type: number
......@@ -1198,34 +1232,7 @@ financement:
format: float
public: true
label: reste à charge applicable
rff:
description: montant de la RFF si applicable
type: number
format: float
public: true
nullable: true
label: RFF applicable
debut_rff:
description: date de début de la RFF applicable
type: string
format: date
public: true
nullable: true
label: date de début de la RFF applicable
fin_rff:
description: date de fin de la RFF applicable
type: string
format: date
public: true
nullable: true
label: date de fin de la RFF applicable
fin_remuneration:
description: date de fin de la rémunération applicable
type: string
format: date
nullable: true
public: true
label: date de fin de la rémunération applicable
<<: *remuneration
ressources:
description: liste d'adresses Internet pour en savoir plus sur le sujet
type: array
......
......@@ -53,3 +53,4 @@ def add_schema(name, data=None):
add_schema("beneficiaire")
add_schema("formation")
add_schema("financement")
add_schema("remuneration")
......@@ -204,7 +204,6 @@ def compute_modalites(context, financement):
plafond_financier = context.get("financement.plafond_financier")
reste_a_charge = context.get("financement.reste_a_charge", 0)
plafond_prix_horaire = context.get("financement.plafond_prix_horaire", 0)
indemnite_conges_payes = context.get("financement.indemnite_conges_payes", 0)
financement.reste_a_charge = reste_a_charge
prise_en_charge = context.get("financement.prise_en_charge", None)
if not prise_en_charge:
......@@ -225,20 +224,28 @@ def compute_modalites(context, financement):
plafond_financier = heures * plafond_prix_horaire
financement.plafond_prix_horaire = plafond_prix_horaire
financement.plafond_prise_en_charge = plafond_financier - reste_a_charge
financement.heures = heures
compute_remuneration(context, financement)
def compute_remuneration(context, facility, facility_name="financement"):
# FIXME: should we define default remuneration in common rules instead?
remuneration = context.get("financement.remuneration", 0)
plafond_remuneration = context.get("financement.plafond_remuneration", 0)
indemnite_conges_payes = context.get(facility_name + ".indemnite_conges_payes", 0)
remuneration = context.get(facility_name + ".remuneration", 0)
plafond_remuneration = context.get(facility_name + ".plafond_remuneration", 0)
if plafond_remuneration and plafond_remuneration < remuneration:
remuneration = plafond_remuneration
financement.remuneration = remuneration
financement.fin_remuneration = context.get("financement.fin_remuneration", None)
if not financement.fin_remuneration:
financement.fin_remuneration = context.get("beneficiaire.fin_allocation")
facility.remuneration = remuneration
facility.fin_remuneration = context.get(facility_name + ".fin_remuneration", None)
if not facility.fin_remuneration:
facility.fin_remuneration = context.get("beneficiaire.fin_allocation")
+datetime.timedelta(days=1)
financement.indemnite_conges_payes = indemnite_conges_payes
financement.heures = heures
facility.indemnite_conges_payes = indemnite_conges_payes
# TODO: some keys are not remuneration topic, move these part away
keys = [
"intitule",
"intitule_remuneration",
"remuneration_texte",
"prise_en_charge_texte",
"demarches",
......@@ -246,24 +253,23 @@ def compute_modalites(context, financement):
"description",
"remuneration_annee_2",
"remuneration_annee_3",
"intitule",
"en_savoir_plus",
]
for key in keys:
name = f"financement.{key}"
name = facility_name + f".{key}"
if name in context:
financement[key] = context[name]
if financement.get("demarches"):
financement.demarches = financement.demarches.replace("⏎", "\n")
if financement.get("rff"):
financement.debut_rff = financement.fin_remuneration + datetime.timedelta(
facility[key] = context[name]
if facility.get("demarches"):
facility.demarches = facility.demarches.replace("⏎", "\n")
if facility.get("rff"):
facility.debut_rff = facility.fin_remuneration + datetime.timedelta(
days=1
)
financement.fin_rff = context.get("formation.fin")
facility.fin_rff = context.get("formation.fin")
def get_root_rule(context, financement):
name = financement.racine
def get_root_rule(context, facility):
name = facility.racine
if name.endswith(".rules"):
return name
name = LABELS.get(name, name)
......@@ -303,6 +309,35 @@ def check_financement(context, financement):
del financement[key]
def check_remuneration(context, remuneration):
statuses = []
remuneration.explain = []
context["remuneration.intitule_remuneration"] = remuneration.intitule
context["remuneration.tags"] = remuneration.tags
context["financement.remuneration"] = 0 # not nullable for remuneration
rule_name = get_root_rule(context, remuneration)
if not rule_name:
return
for rule in RULES[rule_name]:
# TODO: status only available for conditions before action ??
statuses.extend(Rule.process(rule, context))
remuneration.explain = statuses
# get value from financement context
for key in SCHEMA:
if key.startswith("remuneration"):
if(context.get("financement." + key[13:])):
context[key] = context.get("financement." + key[13:])
compute_remuneration(context, remuneration, facility_name="remuneration")
# load_organisme_contact_details(context, remuneration)
# remuneration.format()
for key in list(remuneration.keys()):
name = f"remuneration.{key}"
if name not in SCHEMA or not SCHEMA[name].get("public"):
del remuneration[key]
def search_term(list_, term):
data = {}
for k in list_:
......
......@@ -331,7 +331,10 @@ class Condition(Step):
def compile(self):
super().compile()
# TODO add some context for knowing if financement or remuneration is the target
keys = [pointer.key for pointer in self.params.values() if pointer.key]
# print(self.params.value())
# print(keys)
for pointer in self.params.values():
try:
pointer.resolve_labels(*keys)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment