# Offre logement

## Recherche

Il existe deux routes pour effectuer une recherche d'offres :

- GET api/dmo/housing_offers
- POST api/dmo/housing_offers/search

Les deux routes sont indentiques mais la seconde permet d'utiliser les opérateurs de recheche qui peuvent poser problème lors de l'utilisation avec les params du verbe GET

Exemple de recherche d'offres a partir d'une liste d'id d'entité

    POST api/dmo/housing_offers/search
    {
        "entity_ids": {
            "$in": [
                "id_1",
                "id_2",
                "id_3"
            ]
        }
    }

## Affichage des offres de logements publiques

Le front-end peut lancer des requêtes afin de récupérer l'ensemble des offres de logements publiques (housing_offer avec le champ "is_public" à true)

Il n'est pas nécessaire d'être identifié pour y avoir accès (pas besoin de token).

    GET /api/dmo/public_housing_offers

Les paramètres de tri et de pagination possibles sont décris dans le swagger :

[https://als-hermes-scoring-rec.be-idys.com/api/swagger/index.html#operations-tag-API_DMO_PUBLIC_HOUSING_OFFERS](https://als-hermes-scoring-rec.be-idys.com/api/swagger/index.html#operations-tag-API_DMO_PUBLIC_HOUSING_OFFERS)

Une expliquation plus détaillée de l'utilisation des query params pour préciser la recherche se trouve au premier chapitre de cette documentation.

Il est aussi possible de réaliser une requête pour récupérer le détail d'une seule housing_offer dont le champ is_public est à true sans être identifié (pas besoin de token) :

    GET /api/dmo/public_housing_offers/{id}

**Attention, après sa création, le code est valable pendant 15 jours**


## Création/mise à jour d’une offre

    POST /api/dmo/housing_offers

Lors de la génération d'une offre, l'API effectue une recherche d'une offre existante utilisant les mêmes external_ref, lessor_siren, et lessor_code que ceux spécifiés dans les paramètres. 

Si une offre correspondante est trouvée et qu'elle est toujours "active", alors cette opération mettra à jour l'offre existante au lieu d'en créer une nouvelle. 

De plus, si l'offre existante comprend déjà des images (ordered_pictures_ids ou picture_urls) et que les entrées correspondantes dans les paramètres sont vides, alors ces champs ne seront pas mis à jour. Ceci est conçu pour prévenir la suppression accidentelle des images déjà en place.


## Filtrer les offres publiques par dates

    GET api/dmo/public_housing_offers

Ajouter les paramètres

    date_publication_start[$gte]=${date limite inférieure} // au format YYYY-MM-DD
    publication_end_date[$lte]=${date limite supérieure} // au format YYYY-MM-DD

La requête va retourner toutes les offres publiques dont les dates de publication sont comprises entre les 2 paramètres.

## Récupération de l'historique des décomptes bailleurs par n° d'offre

    GET api/dmo/housing_offers/{id de l'offre}/history

Le champ `created_at` est le timestamp du moment où un objectif a été réalisé.  
Les `target_by_typologies` et `realized_by_typologies` sont les copies des "attendus" et "réalisés" à ce moment.

```
  [
    {
      "id": "5e4fdc8b3604e9cfbf4d055e",
      "type": "lessor_objective_history",
      "attributes": {
        ...
        "created_at": "2020-02-21T14:35:07.613+01:00",
        "lessor_objective_id": "5dc3c9bfa70bf25732aaa55b",
        "lessor_id": "5dc2d6b6a70bf24cc5b33594",
        "housing_offer_id": "5dc30a5ba70bf23b53a26475",
        "target_by_typologies": {
          "T3": 25,
        },
        "realized_by_typologies": {
          "T3": 3,
        }
      }
    }
  ]
```

## Renseigner la date de visite pour une candidature

    PATCH/PUT api/dmo/housing_applications/{housing_application_id}

avec en body de la requête :

    {
      "data": {
        "attributes": {
          "visit_conducted_date": 'DD/MM/YYYY' // date de la visite
        }
      }
    }

## Fonctionnement de la création/update/auto_delete des offres

Lorsqu'on crée un offre, un recherche est effectuée sur le champs lessor_id et external_ref.
Si une offre correspondante existe, elle est mise a jour avec les infos transmises lors de l'update.

A chaque modifications de l'offre via l'api, si l'offre a le champs housing_offer.auto_destroy_next_day à true, un compte à rebour de 36h se déclenche.
A l'issue de ce compte à rebours, si aucune requête de création ou d'update n'a été faite sur l'offre par le bailleur, le offer_status passe à "RETIRE PAR LE BAILLEUR"
et les actions suivantes sont exécutées :

- envoi du flux 12 pour informer sopra de la reprise par le bailleur (status "RBA").
- à spécifier
- à spécifier

## Trier les offres par localisation

    GET api/dmo/housing_offers?longitude=44.55127&latitude=4.75386&distance=130

| fields     | type     | default |
| ---------- | -------- | ------- | ---- |
| :longitude | :float   | `null`  |
| :latitude  | :float   | `null`  |
| :distance  | :integer | `20`    |
| :unit      | `km      | meter`  | `km` |

Le filtre sur la localisation étant apliqué en dernier, les conditions de récupération des offres reste exactement les mêmes.

Si les paramètres `:longitude` et `:latitude` sont fournis, alors le filtre sur la localisation sera appliqué.

## Règles de dépublication d'une offre

### Pour administrateurs uniquement en CRUD complet

Exemple de création d'un nouveau réglage de dépublication :

    POST api/dmo/housing_offer_expiration_settings

Avec en body

    {
      "data": {
        "attributes": {
          "department": '34',
          "max_applicants": 10,          // seuil de candidatures max
          "minimal_publication_days": 3, // durée de vie minimale de l'offre en jour
          "expiry_days": 7,              // nombre de jours avant dépublication
          "lessor_id": {id du bailleur}
        }
      }
    }

### Automatisation de la dépublication

Une offre **publiée**, créée ou éditée, récupère automatiquement le `max_applicants` et le `minimal_publication_days` (par défaut 3) du réglage correspondant à son département et lessor_id.

La date de dépublication (`publication_end_date`) est alors automatiquement calculée et enregistrée à `date_start_publication + expiry_days` (par défaut 7).

Lorsqu'une offre dépasse son seuil de candidatures **et** sa durée minimale de vie, elle est automatiquement dépubliée (offer_status: 'COMMERCIALISATION NON PUBLIEE')

### Remarques

Si le `max_applicants` n'est pas renseigné, l'offre n'a pas de seuil de candidatures.

Si un `expiry_days` est set à la création ou à l'édition d'une offre, il prend le pas sur celui du réglage (`housing_offer_expiration_setting`)

- Exemple : si j'enregistre une offre avec `expiry_days = 20` et qu'il existe un réglage avec `expiry_days = 11`, ma date de dépublication sera de `publication_start_date + 20`

Si une date de dépublication (`publication_end_date`) est set à la création ou à l'édition d'une offre, c'est elle qui sera prise en compte quelque soit le réglage et/ou le `expiry_days` existant(s).

## Vérification si une référence existe

    GET /api/dmo/housing_offers/reference_exist?external_ref=reference

## Besoin front sprint 25

5411 -a Pour qu'on puisse récupérer les candidatures d'une offre il faut soit ajouter les ID de candidatures actives sur une offre, soit permettre de requêter des objets candidature par housing_offer_id + status "EN ATTENTE ou "TRANSMIS BAILLEUR" ou "RETENUE EN CAL"
pour récuperer les candidatures souhaités :

    GET /api/dmo/housing_applications&housing_offer_id=<id>&status[$in][]="RETENUE EN CAL"&status[$in][]="TRANSMIS BAILLEUR"&... etc

5411 -b Il nous manque sur l'objet candidature les infos suivantes : - l'ID de l'offre chez le bailleur (external_ref de l'offre) - nom et prénom du candidat (main_applicant de la demande) - nur/nud (nud de la demande) - numéro de la demande (als_id de la demande)

du coup au niveau de la vue json j'ai rajouté ces champs :

      housing_application: {
          ...
          "unique_numbers" => [
          [0] "AL056473357466383880"
          ],
          "offer_external_ref" => "EXTERNALREF01",
          "main_applicant_first_name" : "martin",
          "main_applicant_last_name" : "Matin",
          "request_als_id" : "0000000002",
          "rank" : 2,
          ...
      }

Pour trier via score, il faut utiliser le paramètre `offer_id`

GET /api/dmo/housing_applications?offer_id={housing_offer_id}&sort[applicated_position]=1

## Réservation d'offre

Pour créer une reservation d'une entreprise :

    POST api/dmo/entity_reservations
        data: {
          attributes: {
            entity_id: entity_id,
            housing_offer_id: housing_offer_id,
            from: date,
            to: date,
          }
        }

Possiblité de modifier et supprimer les reservations via PUT/PATCH et DELETE

Il ne peut y avoir qu'une seule réservation par entreprise/offre, si lors de la création ou mise à jour une autre reservation existe déja en base. On écrase les valeurs de celle-ci avec les valeurs fournies dans l'appel POST ou PUT/PATCH

Les réservations sont reportées sur l'offre dans le champs active `active_reservations_changed` qui contient les id des entreprises qui ont réservées

Tout les jours, un script se lance pour changer l'ativité des réservation qui ne sont plus active ou le deviennent

Les droits de lecture sur une offre sont ajoutés à toutes les entreprises qui ont une réservation active.

Appel pour trouver les offres reservées par une liste d'entreprise:

    GET /api/dmo/housing_offers?active_reservations[$in][]=:entity_id1&active_reservations[$in][]=:entity_id2

    ou

    POST GET /api/dmo/housing_offers
    {
        active_reservations: {
            "$in": [entity_id1, entity_id2]
        }
    }

Appel pour mettre à jour plusieurs reservations d'une offre

      POST /api/dmo/entity_reservations/update_offer_reservations/:offer_id
      {
        "entity_reservations": [
          { "entity_id": "1", "from": "2019-01-01", "to": "2025-01-02" },
          { "entity_id": "2" },
          { "entity_id": "3" },
        ]
      }

Les champs entity_reservations et entity_id sont obligatoires. Et les champs from et to sont facultatifs.
Si une des deux valeurs from ou to manque, alors la réservation sera active par défaut.

La méthode va vérifier si l’utilisateur a bien le rôle “Administrator” paw. Si oui, alors la méthode va vérifier si l’id de l’offre passé dans l’url correspond bien à une offre en base (erreur 404 sinon). S’il n’y a pas de champ entity_reservations dans le body, nous reverrons une erreur avec le code 422.
Ensuite pour chaque entity_reservation passée dans le tableau du body, la méthode va vérifier s’il y a bien une entreprise correspondant à l’entity_id (retour 207 sinon). S’il n’y a aucune erreur, alors la requête renverra un code 200 avec la liste des réservations crées.

Codes retours :
 * 200 : Succès pour toutes les réservations
 * 207 : Succès partiel (au moins une réservation en échec, voire tout en échec)
 * 404 : Offre non trouvée [housing_offer_not_found]
 * 422 : Si entity_reservations n’est pas dans le body [entity_reservations_cannot_be_missing]
Chaque retour en erreur créera un log équivalent (accessible un mois en production) visible depuis {host}/admin/logs.

Exemples :

  code 200 (toutes les reservations ont été mise à jour) :

    {
      "created_reservations": [
        {
          "id": "xxxxxx",
          "entity_id": "1",
          "response": {
            "return_code": "200",
            "details": "Opérations effectuées"
          },
          "attributes": {
            "from": "2019-01-01",
            "to": "2025-01-02",
            "housing_offer_id": "offer_id",
            "entity_id": "1",
            "is_active": true
          }
        }
      ],
      "not_created_reservations": []
    }

Code 207 ( il y a au moins 1 erreur):

    {
      "created_reservations": [
        {
          "id": "xxxxxx",
          "entity_id": "1",
          "response": {
            "return_code": "200",
            "details": "Opérations effectuées"
          },
          "attributes": {
            "from": "2019-01-01",
            "to": "2025-01-02",
            "housing_offer_id": "offer_id",
            "entity_id": "1",
            "is_active": true
          }
        }
      ],
      "not_created_reservations": [
        {
          "entity_id": "2",
          "response": {
            "return_code": "404",
            "details": "entity not found"
          }
        }
      ]
    }

Format de retour en cas d’échec (exemple avec l’erreur 404) :

    {
      "errors": [
        {
          "title": "translation missing: fr.error_messages.housing_offer_not_found.title",
          "status": 404,
          "code": "translation missing: fr.error_messages.housing_offer_not_found.code"
        }
      ]
    }

## Multireservataire

### Admin

L'admin des réservataires est accessible via `/admin/reservation_holders`

Chaque réservataire peut être accessible par tous le monde (public_read: true) ou aux utilisateurs ayant une habilitations sur une liste de bailleurs (lessor_ids)

### Récupération

Les réservataires accessibles à un utilisateur peuvent être récupérer via l'appel :

        GET /api/dmo/reservation_holders

Un réservataire en particulier est accessible via :

       GET /api/dmo/reservation_holders/:id

       ou

       GET /api/dmo/reservation_holders/:siren

### Affectation sur une offre

Pour affecter un réservataire sur une offre, il faut renseigner le champs `reserving_siren` sur l'offre.

### Accès

Pour récupérer la liste des offres d'un réservataire :

    GET /api/dmo/housing_offers?reservation_holder_id=id

    ou

    GET /api/dmo/housing_offers?reserving_siren[$in][]=siren1&reserving_siren[$in][]=siren2

### Récupération des demandes eligibles

    GET /api/dmo/housing_offers/:id/eligible_requests?reserving_siren=siren

Le paramètres reserving_siren est optionnel. Si fourni il permet de restreindre les résutalt au demande lié au entreprise de ce siren/reservataire

L'appel qui permet ne récupérer que des demandes qui sont sur des entreprises auquel l'utilisateur a accès.

      POST /api/dmo/housing_offers/:id/eligible_requests_rights_filtered
      {
        entity_ids: ['id']
      }

Le paramètres entity_ids est optionnel. Si fourni il permet de restreindre d'avantage la liste des demandes

### Récupération de la liste des candidatures active d'une offre

L'appel qui permet d'aller chercher la liste des candidatures actives sur une offre

      GET /api/dmo/housing_offers/:id/all_applications

L'appel qui permet ne récupérer que les candidatures qui ont sur des entreprises auquel l'utilisateur a accès.

      POST /api/dmo/housing_offers/:id/all_applications_rights_filtered
      {
        entity_ids: ['id']
      }

Le paramètres entity_ids est optionnel. Si fourni il permet de restreindre d'avantage la liste des demandes

### Récupération d'offres avec recalcul du eligible_requests_nb

    GET /api/dmo/housing_offers/with_recount_eligible_requests_nb?housing_offer_ids[]=id_offer&reserving_siren=siren

Si un reserving_siren est fourni :

- On va chercher la liste des sirens liés au réservataire (reservation_holder)
- Ensuite on recherche la liste des entreprises liées à ces sirens. Comme pour l'appel /api/dmo/housing_offers/:id/eligible_requests?reserving_siren=siren
- Et on recalcule le champ eligible_requests_nb de chaque offre seulement avec les demandes qui sont sur ces entreprises

Sinon on recalcule juste le eligible_requests_nb

Il existe également une option possible_eligible_requests_nb qui permet également de recalculer le champs possible_eligible_requests_nb.

GET /api/dmo/housing_offers/with_recount_eligible_requests_nb?housing_offer_ids[]=id_offer&reserving_siren=siren&options[]=possible_eligible_requests_nb

L'appel qui permet de restreindre les recalculs aux demandes/candidatures qui ont sur des entreprises auquel l'utilisateur a accès.

    GET /api/dmo/housing_offers/with_recount_eligible_requests_nb_rights_filtered
    {
      housing_offer_ids: ['id'],
      entity_ids: ['id']
    }

Cet appel recalcul eligible_requests_nb et applicated_nb.
Le paramètres entity_ids est optionnel. Si fourni il permet de restreindre d'avantage le recalcul

L'option possible_eligible_requests_nb possible sur cet appel également.

GET /api/dmo/housing_offers/with_recount_eligible_requests_nb_rights_filtered?options[]=possible_eligible_requests_nb

## Renvoyé les flux 5

Pour une offre

    POST /api/dmo/housing_offers/:id/resend_flux5

Pour toutes les offres non envoyées depuis une date (par défault une semaine)

    POST /api/dmo/housing_offers/resend_flux5_all_missing_offers
    {
        date: '2021-01-01'
    }

## Attache Communale

L'orientation `township_attachment` sur les `housing_channels_keys` des housing_offers permet de restreindre l'éligibilité des demandes.
Elle fonctionne en synergie avec le champs `applicants_attached_codes` sur la housing_request, qui est calculé pour être la somme des `actual_housing_details.address_insee_code` (remplit juste pour le main_applicant) et `workplace_insee_code` de tous les applicants de la demande.
Lorsque l'orientation est définie : un filtre est ajouté sur l'eligiblité classique : seules les demandes qui ont dans `applicants_attached_codes` l'`insee_code` de l'offre sont éligibles et inversement.
Nb: on supprime le filtre de departement lors que l'on filtre par attache communale. Et l'orientation `township_attachment` n'est pas présente directement dans le filtre d'éligiblité de l'offre ou demande. Mais l'orientation reste dans le housing_channel_keys en base.

## Savoir si une demande est eligible à une offre

    GET /api/dmo/housing_offers/:offer_id/is_eligible_request/:als_id

    Retour
    {
      is_eligible: true|false
    }

## Aide à la décision bailleur

Cette route qui permet de remplir les champs d'une offre non enregistrée et renvoyer deux totaux des demandes éligibles.
Avec les paramètres suivants qui sont tous obligatoires : threshold_type, threshold_subtype, rent_with_charges, insee_code, typology

    GET /api/dmo/housing_offers/simulate_eligible_requests_count?threshold_type=valeur&threshold_subtype=valeur&rent_with_charges=valeur&insee_code=valeur&typology=valeur

Retour

```json
  {
    “possible_eligible_requests_count”: Integer, // demandes éligibles avec commune demandée et communes limitrophes
    “complete_eligible_requests_count”: Integer // demandes éligibles complètes avec commune demandée et communes limitrophes
  }
```

## Totaux demandes éligibles

Ajout des champs suivant sur l'offre :

- async_eligible_requests_nb
- async_eligible_department_nb
- async_eligible_seeked_nb
- async_eligible_bordering_nb
- async_eligible_seeked_bordering_nb
- async_eligible_other_from_department_nb

Qui sont recalculés lors du changement des champs d'éligiblité d'une offre (offer_status, motivation, orientations, etc).

Ce recalcul est fait dans un job UpdateAsyncEligibleNbOfferJob sur la pile existante x_hermes_eligibility.

Ces champs sont utilisables pour trier/filtrer sur les routes qui retournent des offres Exemple :

    GET /api/dmo/housing_offers?async_eligible_requests_nb=0
    GET /api/dmo/housing_requests/eligible_offers?sort[async_eligible_seeked_nb]=-1

## Compte des demandes éligibles par entreprise sur les offres reservées

Ces totaux sont recalculé tous les jours via un batch pour toutes les offres reservées avec un status :

- COMMERCIALISATION NON PUBLIEE
- COMMERCIALISE SANS PUBLICATION
- EN ATTENTE COMMERCIALISATION
- EN COURS ATTRIBUTION
- EN COURS D'ATTRIBUTION
- EN COURS DE DESIGNATION
- LOGEMENT PUBLIE

Ce Batch utilise une collection interne qui n'est pas accessible via l'API : Eligible. 
Cette collection accelère des calculs. Elle établit une connexion entre les offres et les demandes éligibles ainsi que leurs catégories d'éligibilité. 
Pour économiser de l'espace dans la base de données et sur les index, elle est supprimée quotidiennement dans le batch.

La route permatant d'aller chercher les compte des demandes éligibles par entreprise sur les offres reservées est la suivante :

    GET /api/dmo/eligible_count_offers?updated_at[$gte]=2023-01-01

Avec le retour suivant

```json
{
  "data": [
    {
      "id": "64997ba6ab5aed0360b2647e",
      "type": "eligible_count_offer",
      "attributes": {
        "responsible": null,
        "readers": [],
        "writers": [],
        "read_level": "write",
        "created_at": "2023-06-26T13:51:02.407+02:00",
        "updated_at": "2023-06-26T13:51:02.407+02:00",
        "housing_offer_id": "64997ba6ab5aed0360b2647b",
        "offer_status": "PUBLIE",
        "reserved_entitites_eligible_request_nb": {
          "78997ba6ab5aed0360b2651a": {
            "department": 2,
            "seeked_bordering": 1,
            "total": 2
          },
          "65147ba6ab5aed0360b8712c": {
            "department": 3,
            "seeked_bordering": 3,
            "total": 3
          }
        }
      }
    }
  ],
  "links": {
    "self": "/api/dmo/eligible_count_offers?updated_at%5B%24gte%5D=2023-01-01",
    "first": "/api/dmo/eligible_count_offers?updated_at%5B%24gte%5D=2023-01-01&page%5Bnumber%5D=1&page%5Bsize%5D=10",
    "prev": null,
    "next": null,
    "last": "/api/dmo/eligible_count_offers?updated_at%5B%24gte%5D=2023-01-01&page%5Bnumber%5D=1&page%5Bsize%5D=10"
  },
  "meta": {
    "pagination": {
      "per_page": 15,
      "total_pages": 1,
      "total_objects": 1,
      "count": 1,
      "links": {
        "first": "/api/dmo/eligible_count_offers?page=1",
        "last": "/api/dmo/eligible_count_offers?page=1"
      }
    }
  }
}
```

## Accès à une offre avec un token de courte durée

Vous avez la possibilité de créer un token d'accès temporaire pour une offre spécifique en utilisant un compte avec le rôle `Administrator` sur un `reserving_siren` spécifique.

    POST /api/dmo/housing_offers/:id/generate_exchange_token
    {
        "reserving_siren": "siren",
    }

Voici les retours possibles :

| http_code | retour                             | raison                                                            |
| --------- | ---------------------------------- | ----------------------------------------------------------------- |
| 200       | { "token": token }                 | Succès                                                            |
| 401       | not_administrator                  | Utilisateur n'est pas un Administrator                            |
| 403       | reservation_holder_id_not_matching | Le reservation_holder_id et celui de l'offre ne correspondent pas |
| 404       | housing_offer_not_found            | Offre non trouvée                                                 |
| 404       | reservation_holder_id_not_found    | Aucun reservation_holder trouvé pour ce siren                     |
| 422       | missing_param                      | Paramètre reserving_siren ou id manquant                          |

Cette requête génère un token, qui peut ensuite être utilisé pour accéder à l'offre via l'API. Ce token reste valide pendant 90 secondes.

Pour l'utiliser, ajoutez-le dans le header de votre requête comme suit : Authorisation: Bearer {token} dans la requête suivante :

    GET /api/dmo/housing_offers/:id/with_exchange_token

Voici les retours possibles :

| http_code | retour                             | raison                                                               |
| --------- | ---------------------------------- | -------------------------------------------------------------------- |
| 200       | json de l'offre                    | Succès                                                               |
| 401       | token_not_found                    | Token non trouvé                                                     |
| 401       | unable_to_decode_token             | Impossible de décoder ce token                                       |
| 403       | reservation_holder_id_not_matching | Le reservation_holder_id du token et de l'offre ne correspondent pas |
| 403       | housing_offer_id_not_matching      | L'id du token et de l'offre ne correspondent pas                     |
| 404       | housing_offer_not_found            | Offre non trouvée                                                    |
| 422       | token_expired                      | Token invalide                                                       |

Format des erreurs :

```json
{
  "errors": [
    {
      "title": "translation missing: fr.error_messages.housing_offer_not_found.title",
      "status": 404,
      "code": "translation missing: fr.error_messages.housing_offer_not_found.code"
    }
  ]
}
```
