Deal¶
Contrôleur HTTP de gestion des annonces du marché secondaire (création, achat, annulation, vérification du quota quotidien). Pour la consultation des annonces, voir Marketplace. Source : deal.controller.ts.
Tous les endpoints requièrent un JWT investisseur valide (JwtAuthGuard) et le rôle CUSTOMER (UserRoleGuard). Le flag customer.canSeeRoyalty = true est exigé (sinon 403 marketplace-forbidden) sur la création, l'achat et l'annulation, mais pas sur GET /marketplace/deal/can-sell-today.
Quota quotidien de vente : businessRules.secondaryMarket.dailyDealCreationMax = 30 annonces par jour et par customer (compteur Redis avec expiration à minuit, cf. clé customer-daily-sale-count:<customerId>). Le compte company.customerId est exempté.
GET /marketplace/deal/can-sell-today¶
Indique si l'investisseur peut encore créer une annonce aujourd'hui (sous le quota). Le compte company retourne toujours true.
Pré-conditions¶
- JWT investisseur valide
- Rôle
CUSTOMER
Response¶
POST /marketplace/deal¶
Crée une annonce de revente de bricks (statut PENDING). Verrouille pessimistiquement les bricks de l'investisseur sur la property (status null → SELLING) et incrémente le compteur quotidien. En cas d'erreur dans la transaction, le compteur est décrémenté.
Pré-conditions¶
- JWT investisseur valide
- Rôle
CUSTOMER customer.canSeeRoyalty === true- Droits transactionnels actifs (
Customer.assertTransactionRights('all')) - Property éligible :
investorContractType === 'royalty', achetée (purchasedDate != null), pas en revente clôturée (resale.completedAt == null) — cf.isMarketplaceAllowedForProperty - Quota quotidien non dépassé (≤ 30 annonces / jour)
- L'investisseur possède au moins
amountbricks libres (status = null) sur cette property - Prix de vente dans
[businessRules.secondaryMarket.minimumBrickPrice, officialBrickPrice](en cents) — cf.brickPriceIsSalable
Request body¶
Validé via dealCreationValidator (deal.controller.ts#L42). amount est un entier positif (nombre de bricks à vendre), price est en cents par brick.
Response¶
L'entité MarketDeal créée :
{
"id": "<uuid>",
"propertyId": "<uuid>",
"status": "pending",
"bricksIds": ["<uuid>", "..."],
"sellerId": "<customerId>",
"brickCount": 10,
"officialBrickPriceValue": 1000,
"sellingBrickPriceValue": 950,
"priceWithoutCommission": 9500,
"profitability": 5.50,
"revenueYield": 4.20,
"brickPriceVariation": -5.0
}
Erreurs¶
| Code | Statut | Cause |
|---|---|---|
marketplace-forbidden |
403 | customer.canSeeRoyalty === false |
customer.rights.account-is-being-verified |
403 | Compte en cours de vérification (Lemonway ou interne) |
customer.rights.account-not-completed |
403 | Identité investisseur non vérifiée |
marketplace.sell.MAX_SELLING_QUOTA_REACHED |
403 | Quota quotidien atteint |
marketplace.sell.PROPERTY_NOT_FOUND |
404 | Property inexistante |
marketplace.sell.property-forbidden |
403 | Property non éligible (non-royalty, pas achetée, ou revente clôturée) |
marketplace.sell.PROPERTY_BRICKS_PRICE_NOT_FOUND |
404 | Aucun BrickPrice officiel pour cette property |
marketplace.sell.PRICE_NOT_WITHIN_RANGE |
400 | Prix demandé hors [minimumBrickPrice, officialBrickPrice] |
marketplace.sell.NOT_ENOUGH_BRICKS |
400 | Moins de bricks disponibles (status = null) que demandé |
POST /marketplace/deal/:dealId/purchase¶
Achète une annonce existante. Réserve le deal (status PENDING → RESERVED), crée deux WalletTransaction (achat côté acheteur en BUYING_DEAL, vente côté vendeur en SELLING_DEAL, statut WAITING). Le solde n'est débité/crédité qu'à la confirmation Lemonway via MarketDealService.confirmDealPurchase (déclenchée par le worker wallet-transactions).
Pré-conditions¶
- JWT investisseur valide
- Rôle
CUSTOMER customer.canSeeRoyalty === true- Onboarding investisseur complété (
Customer.assertCompletedInvestorOnboarding) - Droits transactionnels actifs (
Customer.assertTransactionRights('all')) - Le deal est en statut
PENDINGet n'appartient pas à l'acheteur (sellerId != buyerId) - Property toujours éligible (
isMarketplaceAllowedForProperty) - Solde Lemonway ≥
priceWithoutCommission
Path param¶
dealId— UUIDv4
Response¶
undefined (pas de body retourné). L'effet est asynchrone : le deal passe en RESERVED, deux WTs sont créées en WAITING ; la finalisation (passage à CONFIRMED, transfert des bricks, email de confirmation Customer.io) est faite par le worker.
Erreurs¶
| Code | Statut | Cause |
|---|---|---|
marketplace-forbidden |
403 | customer.canSeeRoyalty === false |
customer.investor-onboarding-not-completed |
403 | Onboarding investisseur incomplet |
customer.rights.account-is-being-verified |
403 | Compte en cours de vérification |
customer.rights.account-not-completed |
403 | Identité non vérifiée |
marketplace.buy.DEAL_NOT_FOUND |
404 | Deal introuvable, non PENDING, ou possédé par l'acheteur |
marketplace.sell.property-forbidden |
403 | Property non éligible au moment de l'achat |
marketplace.buy.BUYER_NOT_FOUND |
404 | Acheteur introuvable au lock (cas dégradé) |
marketplace.buy.SELLER_NOT_FOUND |
404 | Vendeur introuvable au lock (cas dégradé) |
marketplace.buy.USER_NOT_ENOUGH_MONEY |
400 | Solde Lemonway insuffisant |
DELETE /marketplace/deal/:dealId¶
Annule une annonce en cours appartenant à l'investisseur. Supprime le MarketDeal et libère les bricks (status = SELLING → null). Lock pessimiste sur le deal pendant la transaction.
Pré-conditions¶
- JWT investisseur valide
- Rôle
CUSTOMER customer.canSeeRoyalty === true- Le deal est en statut
PENDINGet appartient au customer (sellerId === customer.id)
Path param¶
dealId— UUIDv4
Response¶
undefined (pas de body).
Erreurs¶
| Code | Statut | Cause |
|---|---|---|
marketplace-forbidden |
403 | customer.canSeeRoyalty === false |
marketplace.deletion.not-found |
404 | Deal introuvable, non PENDING, ou n'appartenant pas au customer |
Liens¶
- Service deal :
deal.service.ts - Service marketplace (annulation) :
marketplace.service.ts - Règles métier :
businessRules.secondaryMarket - Consultation des annonces : Marketplace