Skip to content

PASS IAE: La date de début du contrat doit être avant la date de fin du PASS [GEN-2225]#5248

Merged
calummackervoy merged 4 commits into
masterfrom
calum/pass-validity-leverages-job-start-date
Mar 13, 2025
Merged

PASS IAE: La date de début du contrat doit être avant la date de fin du PASS [GEN-2225]#5248
calummackervoy merged 4 commits into
masterfrom
calum/pass-validity-leverages-job-start-date

Conversation

@calummackervoy
Copy link
Copy Markdown
Contributor

@calummackervoy calummackervoy commented Dec 11, 2024

🤔 Pourquoi ?

En ce moment c'est possible d'embaucher un candidat après son PASS IAE est expiré.

🍰 Comment ?

  • Mettre le champ de date de début de contrat en erreur si la date est postérieure à celle de fin du PASS IAE (dans AcceptForm).
  • Ajouter un contrôle sur JobApplication.accept qui empêche qu'une candidature dans cette situation serait acceptée.

🚨 À vérifier

  • Mettre à jour le CHANGELOG_breaking_changes.md ?

🏝️ Comment tester

  • Créer une candidature vers une SIAE.
  • Se connecter avec le compte employeur, et tenter accepter la candidature avec une date d'embauche après le fin du PASS IAE.

💻 Captures d'écran

Screenshot 2025-01-14 at 17 35 39 Screenshot 2025-01-14 at 17 36 42

@calummackervoy calummackervoy added the modifié Modifié dans le changelog. label Dec 11, 2024
@calummackervoy calummackervoy self-assigned this Dec 11, 2024
Comment thread itou/eligibility/models/iae.py
Comment thread tests/www/apply/test_list_for_siae.py Outdated
@francoisfreitag francoisfreitag changed the title PASS IAE: La date de début du contrat doit être est après la date de fin du PASS [GEN-2225] PASS IAE: La date de début du contrat doit être avant la date de fin du PASS [GEN-2225] Dec 16, 2024
@francoisfreitag francoisfreitag self-requested a review December 16, 2024 06:49
Comment thread itou/utils/date.py Outdated
Copy link
Copy Markdown
Contributor

@tonial tonial left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je rejoins le commentaire de @xavfernandez sur le make_date_timezone_aware.

Pour le reste, par contre, c'est top 👍

Comment thread itou/eligibility/models/iae.py Outdated
Comment thread itou/eligibility/models/iae.py
Comment thread itou/job_applications/models.py Outdated
Comment thread itou/users/models.py Outdated
Comment thread tests/eligibility/test_iae.py Outdated
Comment thread tests/www/apply/__snapshots__/test_process.ambr Outdated
Copy link
Copy Markdown
Contributor

@rsebille rsebille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Désolé je fait l'hélicoptère mais vu qu'on en a discuté (longuement) en atelier métier bah j'ai dû y mettre mon nez plus que je le voulais et le changement me semble bizarre par rapport au problème de base.

On se retrouve à faire des modifications profondes au niveau de la logique de validité des diagnostiques et des PASS IAE (et donc de si on en délivre un nouveau ou pas, toussa toussa) alors que le problème c'est de bloquer les employeurs d'accepter une candidature avec une date de début après la date de fin d'un PASS IAE et qui j'ai l'impression (mais je suis peut-être naif) pourrais se régler avec quelques lignes dans JobApplication.accept() :

if self.hiring_start_at > self.approval.end_at:
    raise xwf_models.AbortTransition(...)

et de faire grosso modo la même chose dans le formulaire pour afficher une erreur à l'utilisateur avant même d'arriver dans JobApplication.accept()

Comment thread itou/www/apply/views/process_views.py Outdated
Comment thread itou/job_applications/models.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment thread itou/eligibility/models/iae.py Outdated
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from d48a7e0 to 0ef8cd8 Compare January 14, 2025 16:19
@calummackervoy
Copy link
Copy Markdown
Contributor Author

calummackervoy commented Feb 4, 2025

@rsebille j'ai implementé ta solution, sont ces changements ceux que t'avais en tête ?

J'aime que ça impacte moins le code. J'ai eu besoin d'enlever ce test que j'avais fais pour la filtration des PASS IAEs expirés le moment d'embauche :

def test_list_for_siae_filtered_by_eligibility_validated(self, client):
        company = CompanyFactory(with_membership=True)
        employer = company.members.first()

        job_app = JobApplicationFactory(to_company=company, eligibility_diagnosis=None)
        _another_job_app = JobApplicationFactory(to_company=company, eligibility_diagnosis=None)

        client.force_login(employer)
        response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
        assert response.context["job_applications_page"].object_list == []

        # Authorized prescriber diagnosis
        diagnosis = IAEEligibilityDiagnosisFactory(from_prescriber=True, job_seeker=job_app.job_seeker)
        response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
        assert response.context["job_applications_page"].object_list == [job_app]

        # Make sure the diagnostic expired - it should be ignored
        diagnosis.expires_at = timezone.localdate() - datetime.timedelta(
            days=diagnosis.EXPIRATION_DELAY_MONTHS * 31 + 1
        )
        diagnosis.save(update_fields=("expires_at",))
        response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
        assert response.context["job_applications_page"].object_list == []

        # Not expired, but will be when the job starts
        diagnosis.expires_at = timezone.localdate() + datetime.timedelta(days=1)
        diagnosis.save(update_fields=("expires_at",))
        job_app.hiring_start_at = timezone.localdate() + datetime.timedelta(days=2)
        job_app.save(update_fields=("hiring_start_at",))
        response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
        assert response.context["job_applications_page"].object_list == []

Mais vu que le début d'embauche est quelque chose qui peut changer, c'est peut-être le comportement attendu en fait ?

Copy link
Copy Markdown
Contributor

@rsebille rsebille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍, c'est quand même moins menaçant maintenant que avant :D.

2-3 trucs au niveau des tests, mais c'est de la forme donc tu vois ce que tu prend ou pas.


Mais vu que le début d'embauche est quelque chose qui peut changer, c'est peut-être le comportement attendu en fait ?

Oui, ça me choque pas que le filtre teste l'éligibilité actuelle et pas futur. Mais tu peux confirmer avec le métier pour être sûr.

Comment thread tests/job_applications/tests.py Outdated
Comment thread tests/job_applications/tests.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment on lines +2130 to +2135
# jobseeker has a PASS IAE, but it ends before the start date of the job
assert EligibilityDiagnosis.objects.filter(job_seeker=self.job_seeker).count() == 1
expired_date = timezone.now() + datetime.timedelta(days=1)
EligibilityDiagnosis.objects.filter(job_seeker=self.job_seeker).update(expires_at=expired_date)
self.job_seeker.approvals.update(end_at=expired_date)
assert job_application.hiring_start_at > self.job_seeker.latest_approval.end_at
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai la forte impression que tout ceci devrais être fait directement au niveau de l'appel à .create_job_application() et pas en plusieurs étapes avec des vérifications au milieu, tout l'intérêt de pouvoir faire du déclaratif comme nous le permettent les factory est justement de donner un état de départ connu et fixe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assez simple 🙂 self.create_job_application(..., approval=ApprovalFactory(end_at=(timezone.now() + datetime.timedelta(days=1))))

J'aime tester les assumptions dans mes tests mais ce n'est pas un avis fort (comme par example assert job_application.hiring_start_at > self.job_seeker.latest_approval.end_at)

Comment thread tests/www/apply/test_process.py Outdated
Comment on lines +2163 to +2245
# employer amends the situation by submitting a different hiring start date
post_data["hiring_start_at"] = timezone.localdate().strftime(DuetDatePickerWidget.INPUT_DATE_FORMAT)
response, _ = self.accept_job_application(client, job_application, post_data=post_data, assert_successful=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pareil, ce comportement est très certainement déjà testé ailleurs, et sûrement en long et en travers.
Les tests sont aussi du code donc faut le traiter de la même manière, et avoir une fonction pas trop longue et qui ne fait qu'une chose c'est toujours mieux, surtout quand on revient dessus des mois après ;).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je veux garder celui-ci je pense, les assertions sont reutilisées de accept_job_application et je pense que c'est un plus fort test quand il preuve que l'employeur peut rectifier la situation en changeant le hiring_start_date

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je trouve également ce test pertinent. Dommage d’avoir des helpers «compliqués» comme accept_job_application et _accept_view_post_data, mais ce n’est que l’état actuel des choses, rien de nouveau avec cette PR.

@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from 0ef8cd8 to b1ce34d Compare February 13, 2025 16:12
@calummackervoy calummackervoy added the 1-recette-jetable [Payé à l’heure] Crée une recette jetable sur CC label Feb 13, 2025
@github-actions
Copy link
Copy Markdown

🥁 La recette jetable est prête ! 👉 Je veux tester cette PR !

@notion-workspace
Copy link
Copy Markdown

@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from b1ce34d to b35fb42 Compare February 18, 2025 14:44
Copy link
Copy Markdown
Member

@francoisfreitag francoisfreitag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ça m’a l’air très bien. Je ferai quelques tests dans le navigateur demain pour m’assurer que tout fonctionne, et après ces quelques commentaires, on devrait être proches de pouvoir corriger ce bug. Merci !

Comment thread itou/www/apply/forms.py
Comment thread itou/www/apply/forms.py Outdated
Comment thread tests/job_applications/tests.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_process.py
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_submit.py Outdated
Comment thread tests/www/apply/test_submit.py Outdated
"hiring_end_at": "",
"pole_emploi_id": self.job_seeker.jobseeker_profile.pole_emploi_id,
"lack_of_pole_emploi_id_reason": self.job_seeker.jobseeker_profile.lack_of_pole_emploi_id_reason,
"birthdate": self.job_seeker.jobseeker_profile.birthdate.isoformat(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je taquine, mais lui n’a pas droit à .strftime(DuetDatePickerWidget.INPUT_DATE_FORMAT)?

(peu importe, Django accepte bien les deux)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Petit gagne d'uniformité quand même 👍

Copié-collé des autres tests du fichier, je les ai tous reglé dans un commit séparé

@notion-workspace
Copy link
Copy Markdown

@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from b35fb42 to 620cace Compare February 19, 2025 11:38
Comment thread itou/www/apply/forms.py
not self.instance.hiring_without_approval
and self.company.is_subject_to_eligibility_rules
and self.job_seeker.has_valid_approval
and hiring_start_at > self.job_seeker.latest_approval.end_at
Copy link
Copy Markdown
Contributor Author

@calummackervoy calummackervoy Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai tentativement décidé ignorer la présence du champ JobApplication.approval. Je ne pense pas que son expiration sera jamais différent que l'approbation la plus récente sur le candidat ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.approval = self.job_seeker.latest_approval

C’est en effet la même valeur que latest_approval. On ne peut effectivement pas utiliser JobApplication.approval ici, car la valeur est définie dans la transition accept qui sera appelée après que le formulaire soit instancié et validé.

Comment thread itou/www/apply/urls.py
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from 620cace to 311223f Compare February 24, 2025 11:23
@calummackervoy calummackervoy marked this pull request as draft February 27, 2025 15:50
@calummackervoy calummackervoy removed the 1-recette-jetable [Payé à l’heure] Crée une recette jetable sur CC label Mar 4, 2025
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from 311223f to 1501525 Compare March 6, 2025 15:03
@calummackervoy calummackervoy marked this pull request as ready for review March 6, 2025 15:03
Copy link
Copy Markdown
Member

@francoisfreitag francoisfreitag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L’ensemble me semble bien 👍

Les commits ne passent pas les tests individuellement. Le premier commit a une erreur :

____________________________________________________ TestProcessAcceptViews.test_accept_hiring_date_after_approval _____________________________________________________
[gw8] linux -- Python 3.13.2 /home/freitafr/dev/itou-review/.venv/bin/python3

self = <tests.www.apply.test_process.TestProcessAcceptViews object at 0x7e32045eac50>, client = <tests.utils.test.ItouClient object at 0x7e320358f890>
mocker = <pytest_mock.plugin.MockerFixture object at 0x7e32036ed400>

    def test_accept_hiring_date_after_approval(self, client, mocker):
        # jobseeker has a PASS IAE, but it ends before the start date of the job
        job_application = self.create_job_application(
            job_seeker=self.job_seeker,
            to_company=self.company,
            sent_by_authorized_prescriber_organisation=True,
>           approval=ApprovalFactory(end_at=(timezone.now() + datetime.timedelta(days=1))),
            hiring_start_at=timezone.localdate() + datetime.timedelta(days=2),
        )

tests/www/apply/test_process.py:2218:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.13/site-packages/factory/base.py:43: in __call__
    return cls.create(**kwargs)
.venv/lib/python3.13/site-packages/factory/base.py:539: in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
.venv/lib/python3.13/site-packages/factory/django.py:122: in _generate
    return super()._generate(strategy, params)
.venv/lib/python3.13/site-packages/factory/base.py:468: in _generate
    return step.build()
.venv/lib/python3.13/site-packages/factory/builder.py:274: in build
    instance = self.factory_meta.instantiate(
.venv/lib/python3.13/site-packages/factory/base.py:320: in instantiate
    return self.factory._create(model, *args, **kwargs)
tests/utils/factory_boy.py:12: in _create
    return super()._create(model_class, *args, **kwargs)
.venv/lib/python3.13/site-packages/factory/django.py:175: in _create
    return manager.create(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/manager.py:87: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:679: in create
    obj.save(force_insert=True, using=self.db)
itou/approvals/models.py:648: in save
    super().save(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/base.py:892: in save
    self.save_base(
.venv/lib/python3.13/site-packages/django/db/models/base.py:998: in save_base
    updated = self._save_table(
.venv/lib/python3.13/site-packages/django/db/models/base.py:1161: in _save_table
    results = self._do_insert(
.venv/lib/python3.13/site-packages/django/db/models/base.py:1202: in _do_insert
    return manager._insert(
.venv/lib/python3.13/site-packages/django/db/models/manager.py:87: in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:1847: in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1835: in execute_sql
    for sql, params in self.as_sql():
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1760: in as_sql
    self.prepare_value(field, self.pre_save_val(field, obj))
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1708: in pre_save_val
    return field.pre_save(obj, add=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.db.models.fields.DateField: end_at>, model_instance = <Approval: XXXXX5565710>, add = True

    def strict_pre_save(self, model_instance, add):
        # DateTimeField inherits DateField, hence the check on self.__class__
        if self.__class__ is DateField and isinstance(getattr(model_instance, self.attname), datetime.datetime):
>           raise ValueError(
                f"<{model_instance}>.{self.attname}={getattr(model_instance, self.attname)} needs to be a date"
            )
E           ValueError: <XXXXX5565710>.end_at=2025-03-11 10:18:27.247316+00:00 needs to be a date

tests/conftest.py:675: ValueError
======================================================================= short test summary info ========================================================================
FAILED tests/www/apply/test_process.py::TestProcessAcceptViews::test_accept_hiring_date_after_approval - ValueError: <XXXXX5565710>.end_at=2025-03-11 10:18:27.247316+00:00 needs to be a date

Comment thread tests/job_applications/tests.py Outdated
Comment on lines +1538 to +1539
hiring_start_at=timezone.localdate() + relativedelta(days=2),
approval__end_at=timezone.localdate() + relativedelta(days=1),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On pourrait stocker et réutiliser today = timezone.localdate() pour éviter un petit raté vers minuit. 🤷

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'ai ajouté un commit pour cibler les autres tests dans test_process.py : 1f5c984

Comment thread tests/www/apply/test_process.py Outdated
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from 1501525 to 1f5c984 Compare March 10, 2025 16:56
Comment thread tests/www/apply/test_process.py Outdated
Comment thread tests/www/apply/test_submit.py Outdated
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch 2 times, most recently from 3c49a54 to 271e80f Compare March 13, 2025 14:18
Adding this improves the validation of hiring_start_at (with regards to the job seeker's PASS IAE), for hiring declarations made by employers
Tweaks a number of instances in test_process.py where localdate() is called twice in two lines. If ran at midnight exactly it's possible the test will run with bad dates
@calummackervoy calummackervoy force-pushed the calum/pass-validity-leverages-job-start-date branch from 271e80f to a06acf8 Compare March 13, 2025 14:32
@calummackervoy calummackervoy added this pull request to the merge queue Mar 13, 2025
Merged via the queue into master with commit ee5fdf7 Mar 13, 2025
@calummackervoy calummackervoy deleted the calum/pass-validity-leverages-job-start-date branch March 13, 2025 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug modifié Modifié dans le changelog.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants