Aller au contenu

Property Funding

Endpoints d'administration pour piloter le cycle de funding d'une property : suivi de l'étape, clôture, transferts de frais (collecte, fiducie) et virement des fonds au porteur. Source : property-funding.controller.ts.

Tous les endpoints requièrent une session admin BetterAuth valide (AdminAuthGuard). Les endpoints muteurs (endPropertyFunding, transferFundingFees, transferFiducieFees, transferFundsToProjectOwner) sont tracés via CreateAdminActionInterceptor.

GET /administration/property/:propertyId/funding

Retourne l'état courant du funding (étape admin) : informations de financement, dates clés, montants achetés et bricks encore disponibles.

Décorateur : L51.

Path param

  • propertyId — UUIDv4

Response

AdminPropertyFundingStep (cf. property-funding.admin-step.model.ts).

POST /administration/property/:propertyId/funding/end/:endType

Clôture le funding d'une property. Seul endType=success est supporté. Met à jour funding.ended, calcule investorCount / purchasedBrickCount, désactive la disponibilité des bricks sur le marché primaire, passe le projet en reception-by-project-owner-pending et invalide les caches Redis. Une AdminAction PROPERTY_END_FUNDING est créée via interceptor.

Décorateur : L61.

Path param

  • propertyId — UUIDv4
  • endType — littéral success

Erreurs

Code Statut Cause
property-funding.end.property-not-found 404 Property introuvable
<canCloseFundingWithSuccess error> 400 Conditions de clôture non remplies (montant min non atteint, date de remboursement non écoulée…)

POST /administration/property/:propertyId/funding/transfer-funding-fees

Effectue le P2P Lemonway des frais de structuring (HT + TVA) du SPV vers le compte d'origination, génère la facture associée et met à jour companyFees.fundingFees.transfer. Une AdminAction PROPERTY_TRANSFER_FUNDING_FEES est créée via interceptor.

Décorateur : L82.

Path param

  • propertyId — UUIDv4

Erreurs

Code Statut Cause
property.funding-fees.not-set 400 companyFees.fundingFees non configuré
property.funding-fees.already-transfered 400 Transfert déjà effectué
p2p-failed-at-provider 500 Échec du P2P Lemonway

POST /administration/property/:propertyId/funding/transfer-fiducie-fees

Effectue le P2P Lemonway des frais de fiducie (HT + TVA) du SPV vers le compte principal, génère la facture associée et met à jour companyFees.fiducieFees.transfer. Une AdminAction PROPERTY_TRANSFER_FIDUCIE_FEES est créée via interceptor.

Décorateur : L107.

Path param

  • propertyId — UUIDv4

Erreurs

Code Statut Cause
property.fiducie-fees.not-set 400 companyFees.fiducieFees non configuré
property.fiducie-fees.already-transfered 400 Transfert déjà effectué
p2p-failed-at-provider 500 Échec du P2P Lemonway

POST /administration/property/:propertyId/funding/transfer-funds-to-project-owner

Vire les fonds collectés au porteur de projet (MoneyOut Lemonway) ou enregistre un transfert externe (bypass manuel). Le montant transférable est plafonné à amountToFund - fundingFeesTotalTtc - fiducieFeesTotalTtc - totalFundEscrow - totalConstructionBudget cumulé sur tous les transferts. Une AdminAction PROPERTY_TRANSFER_FUNDS_TO_PROJECT_OWNER est créée via interceptor.

Décorateur : L132.

Path param

  • propertyId — UUIDv4

Request body

Discriminated union sur action :

{
  "action": "withdraw",
  "amountToTransfer": 1000000,
  "lemonwayWithdrawBankAccountId": 12345
}
{
  "action": "no-withdraw",
  "amountToTransfer": 1000000
}

amountToTransfer en cents (positif). withdraw : flux nominal, MoneyOut exécuté avec l'IBAN du porteur. no-withdraw : bypass manuel (virement effectué hors API, par exemple depuis un compte technique Lemonway), enregistré pour la compta (lemonwayWithdrawBypassed + bypassedBy) et tracé via AdminAction.

Erreurs

Code Statut Cause
property.transfer-exceeds-funding-amount 400 Total des transferts dépasserait le montant transférable
transfer-failed-at-provider 500 Échec du MoneyOut Lemonway

POST /:propertyId/funding/trigger-automatic-investment

Déclenche manuellement une vague d'investissement automatique pour un bien publié dont le funding est encore ouvert. Trace une PROPERTY_TRIGGER_AUTOMATIC_INVESTMENT. Décorateur : L197.

Path param

  • propertyId — UUIDv4

Request body

{ "includeInvestorsWithCanceledAutoInvest": true }

Quand true, les investisseurs avec un achat auto refunded ou declined restent éligibles ; seuls les investisseurs avec un achat confirmed ou en cours (waiting_* sur primary_purchase) sont exclus. Quand false, tout investisseur ayant déjà une ligne primary_purchase sur le bien est exclu.

Pré-conditions

  • Feature flag ENABLE_INVESTMENT_PLAN actif (sinon investment-plan-disabled)
  • Bien published, funding non clôturé
  • Pas de job Graphile en attente pour ce bien (clé project-automatic-funding-manual-{propertyId} — sinon automatic-funding-job-already-pending)

Pas de garde early-access côté endpoint ni worker : décision ops.

Response

Corps vide (204 implicite — le controller matche { ok: true } sans valeur de retour).

Erreurs

Code Statut Cause
property-not-found 400 Bien introuvable
property-not-published 400 Bien non publié
funding-already-ended 400 Funding déjà clôturé
investment-plan-disabled 400 Feature flag ENABLE_INVESTMENT_PLAN désactivé
automatic-funding-job-already-pending 400 Job Graphile déjà en queue pour ce bien

GET /administration/property/simulate-automatic-investment

Lance en arrière-plan une simulation du financement automatique pour un horizon et un taux donnés. La requête répond immédiatement ('Simulation en cours...'), les résultats sont postés sur Slack #financement-automatique-simulation. Le montant à financer est codé en dur (5 M€) et le propertyId simulé est 00000000-0000-0000-0000-000000000000.

Décorateur : L182.

Query params

?investmentHorizonInMonths=24&returnOnInvestment=10
  • investmentHorizonInMonths — entier positif (numberFromString.then(positiveInteger))
  • returnOnInvestment — pourcentage (numberFromString.then(positiveNumberTagged))

Response

Texte brut : 'Simulation en cours... Les résultats seront postés sur Slack. #financement-automatique-simulation'.

Cron funding-auto-close (clôture automatique)

Clôture automatique des collectes éligibles et récap quotidien Slack pour l'équipe financement. Source : funding-auto-close.cron-task.ts · funding-auto-close.service.ts.

Schedule

30 7 * * * UTC (08:30 Paris) — déclaré dans graphile-cron.worker.ts.

Comportement

Pour chaque funding actif (ClosePropertyFundingRepository.getActiveFundings) :

  1. Clôture auto — si status === totallyFinanced et fundingCanBeSuccessfulyEnded : appelle PropertyFundingService.endPropertyFunding({ endType: 'success' }) (même logique que l'endpoint admin POST .../funding/end/success)
  2. Stuck — 100 % financé mais fenêtre de rétractation du dernier achat pas encore expirée → listé en alerte (non clôturable)
  3. Approaching max end date — collecte inProgress ou minToFundReached à J-7 de maxEndDate (ou dépassée) → alerte avec % confirmé et % confirmé+waiting
  4. To watch — ≥ 100 % avec waiting, date de clôture possible dépassée, mais pas encore totallyFinanced
  5. Active summaries — toutes les autres collectes en cours, triées par date de clôture possible

Slack

Canal prod : #suivi-projet-financement-en-cours (dev/staging : canal dev mappé par env). Un seul message récap par run, sections :

Section Emoji Contenu
Clôtures réussies Projets auto-clôturés ce run
Stuck ⚠️ 100 % mais rétractation en cours
Expiration proche J-7 ou date max dépassée
Échecs Erreur à la clôture (message PG/métier)
À surveiller 👀 100 % waiting, date clôture passée
En cours Liste des autres collectes actives

Si aucune collecte active : message Aucune collecte active en cours.

Dry run

FundingAutoCloseService.closeEligibleFundings({ dryRun: true }) simule les clôtures (logs [DRY RUN]) sans muter la DB — utile pour valider l'éligibilité avant intervention manuelle.

Pièges courants

  • Stuck vs to watch : stuck = totallyFinanced + rétractation ; to watch = pas encore totallyFinanced mais 100 % avec achats waiting
  • % waiting : fundingPercentageWithWaiting inclut les achats primary_purchase en attente de settlement — peut dépasser 100 % affiché confirmé seul
  • Échec de clôture : vérifier la section ❌ du Slack puis les conditions canCloseFundingWithSuccess (dernier achat refundable, montant min, etc.) — clôture manuelle via POST .../funding/end/success si besoin

Liens