Diagnostic de deal marketplace bloqué¶
Workflow pour diagnostiquer un deal marketplace bloqué (statut reserved, P2P en échec), en utilisant le MCP prod readonly.
Lifecycle d'un deal¶
pending → reserved (achat par le buyer, P2P créé)
reserved → confirmed (P2P réussi, bricks transférées)
reserved → pending (P2P échoué, deal remis en vente)
pending → canceled (annulé par le seller)
Seul le buying_deal WT porte le P2P Lemonway (debitAccountId = buyer, creditAccountId = seller). Le selling_deal WT du vendeur n'a pas de P2P propre. Pas de timeout automatique sur les deals reserved.
Étape 1 : Trouver le deal¶
À partir d'un email, customer ID, ou deal ID.
Par email du vendeur ou acheteur :
SELECT md.id as deal_id, md.status, md."createdAt", md."updatedAt",
md."sellerId", md."buyerId",
md."priceWithoutCommission", md."brickCount", md."sellingBrickPriceValue",
md."propertyId",
seller.email as seller_email,
buyer.email as buyer_email
FROM "market-deal" md
LEFT JOIN customers seller ON seller.id = md."sellerId"
LEFT JOIN customers buyer ON buyer.id = md."buyerId"
WHERE (seller.email = '{EMAIL}' OR buyer.email = '{EMAIL}')
AND md.status = 'reserved'
ORDER BY md."createdAt" DESC
LIMIT 10
Par customer ID (vendeur ou acheteur) :
SELECT md.id as deal_id, md.status, md."createdAt", md."updatedAt",
md."sellerId", md."buyerId",
md."priceWithoutCommission", md."brickCount",
md."purchaseWalletTransactionId", md."saleWalletTransactionId"
FROM "market-deal" md
WHERE (md."sellerId" = '{CUSTOMER_ID}' OR md."buyerId" = '{CUSTOMER_ID}')
AND md.status = 'reserved'
ORDER BY md."createdAt" DESC
LIMIT 10
Étape 2 : Wallet transactions liées¶
Récupérer les WT du deal. Le purchaseWalletTransactionId est le WT porteur du P2P.
SELECT wt.id, wt."customerId", wt.kind, wt.value, wt.status,
wt."createdAt", wt."updatedAt",
wt."lemonwayP2P"->>'status' as p2p_status,
wt."lemonwayP2P"->>'attemptNumber' as attempt_count,
wt."lemonwayP2P"->>'attemptAfter' as next_attempt,
wt."lemonwayP2P"->>'debitAccountId' as debit_account,
wt."lemonwayP2P"->>'creditAccountId' as credit_account,
wt."lemonwayTransactionId",
wt."index"
FROM wallet_transactions wt
WHERE wt.id IN ('{PURCHASE_WT_ID}', '{SALE_WT_ID}')
Si status = 'waiting' et p2p_status = 'pending' sur le buying_deal : le P2P n'a pas abouti.
Étape 3 : Diagnostic P2P (attempts)¶
Distinguer erreurs POST vs GET¶
La présence de rawError.context indique une erreur du GET (findExistingP2PReferenceAtLw), pas du POST (playP2P). Le GET peut masquer la vraie erreur (code 1 au lieu de code 110).
SELECT attempt->>'attemptedAt' as attempted_at,
attempt->'rawError'->'error'->>'code' as error_code,
attempt->'rawError'->'error'->>'message' as error_msg,
CASE WHEN attempt->'rawError' ? 'context'
THEN 'GET (masked)'
ELSE 'POST (real)'
END as error_source
FROM wallet_transactions wt,
jsonb_array_elements(wt."lemonwayP2P"->'attempts') as attempt
WHERE wt.id = '{PURCHASE_WT_ID}'
ORDER BY (attempt->>'attemptedAt')::timestamptz ASC
Synthèse des erreurs¶
Pour un résumé rapide sans lister toutes les attempts :
SELECT attempt->'rawError'->'error'->>'code' as error_code,
attempt->'rawError'->'error'->>'message' as error_msg,
CASE WHEN attempt->'rawError' ? 'context' THEN 'GET' ELSE 'POST' END as source,
COUNT(*) as occurrences,
MIN(attempt->>'attemptedAt') as first_seen,
MAX(attempt->>'attemptedAt') as last_seen
FROM wallet_transactions wt,
jsonb_array_elements(wt."lemonwayP2P"->'attempts') as attempt
WHERE wt.id = '{PURCHASE_WT_ID}'
GROUP BY error_code, error_msg, source
ORDER BY occurrences DESC
Classification des erreurs¶
| Code | Type | Retry | Signification |
|---|---|---|---|
| 110 | Business | 1h | Solde insuffisant (wallet débiteur) |
| 187 | Business | 1h | Solde insuffisant (variante) |
| 111 | Business | fail | Compte débiteur bloqué |
| 167 | Business | fail | Blocage réglementaire débiteur |
| 168 | Business | fail | Compte créditeur bloqué |
| 146 | Business | fail | L'un des deux comptes bloqué |
| 1 | Technique | backoff exp. | Erreur générique (souvent GET masquant le POST) |
| 348 | Technique | backoff exp. | Référence P2P dupliquée |
| 101 | Technique | backoff exp. | Limite P2P atteinte pour le receveur |
Pour les erreurs business de type "insufficient balance" sur un débit : le worker retente toutes les heures s'il existe des pendingCredit, sinon il fail le P2P (alerte Slack).
Étape 4 : Balance de l'acheteur¶
SELECT c.id, c.email,
c."withdrawableBalances", c."giftBalances",
c."lemonwayStatus", c."transactionRights", c."blockedTemporarilyAtLemonway",
c.lemonway->'wallet'->>'isBlocked' as lw_wallet_blocked
FROM customers c
WHERE c.id = '{BUYER_ID}'
Solde réel du wallet Lemonway ~ withdrawableBalances.current (les WT confirmed ont eu leur P2P joué, les waiting non). Pour savoir si le P2P buying_deal peut passer : current >= montant du P2P.
La colonne lemonwayBalance est un alias legacy de availableWithdrawableBalance_view, ce n'est PAS le solde réel Lemonway.
Étape 5 : Pending credits de l'acheteur¶
Identifier pourquoi le solde Lemonway est insuffisant et quand il sera suffisant.
SELECT wt.id, wt.kind, wt.value, wt.status, wt."createdAt",
wt."lemonwayP2P"->>'status' as p2p_status,
wt.context as wt_context
FROM wallet_transactions wt
WHERE wt."customerId" = '{BUYER_ID}'
AND wt.status = 'waiting'
AND wt.value > 0
ORDER BY wt."createdAt" ASC
Interprétation des pending credits :
| Kind | lemonwayP2P |
Signification |
|---|---|---|
topup_checkout |
null |
Wire-in Checkout pas encore arrivé chez LW (jusqu'à 3 jours ouvrés pour Apple Pay / carte via Checkout.com) |
topup_checkout |
pending |
Wire-in arrivé, P2P Checkout -> customer en cours |
topup_card |
n/a | Exclu du pendingCredit. Ne pas compter |
revenue_*, obligation_* |
pending |
P2P credit bloqué (vérifier les attempts) |
Si le seul pending credit est un topup_checkout avec lemonwayP2P = null : le paiement Checkout est en transit. Résolution automatique sous 3 jours ouvrés.
Étape 6 : Ordering des débits (race condition)¶
Vérifier si un débit plus récent a été exécuté avant le buying_deal bloqué.
SELECT wt.id, wt.kind, wt.value, wt.status, wt."createdAt",
wt."lemonwayP2P"->>'status' as p2p_status,
wt."lemonwayTransactionId",
wt."index"
FROM wallet_transactions wt
WHERE wt."customerId" = '{BUYER_ID}'
AND wt.value < 0
AND wt."index" > {BUYING_DEAL_INDEX}
AND wt.status = 'confirmed'
ORDER BY wt."index" ASC
Si des résultats existent avec un lemonwayTransactionId non null : un débit plus récent a consommé le solde Lemonway avant le buying_deal. C'est le symptôme de la race condition d'ordering corrigée par PR #4679 (déployée 26/02/2026). Avant ce fix, les P2P en backoff étaient exclus du ranking CTE, permettant aux débits plus récents de prendre rn=1.
Étape 7 : Comptes vendeur et acheteur¶
SELECT c.id, c.email,
c."transactionRights",
c."blockedTemporarilyAtLemonway",
c."lemonwayStatus",
c.lemonway->'wallet'->>'isBlocked' as lw_wallet_blocked,
c.lemonway->'wallet'->>'status' as lw_wallet_status,
c."withdrawableBalances", c."giftBalances"
FROM customers c
WHERE c.id IN ('{BUYER_ID}', '{SELLER_ID}')
Le P2P est bloqué si pour le buyer OU le seller :
- transactionRights != 'all'
- blockedTemporarilyAtLemonway = true
- lemonwayStatus != '6'
Causes racines courantes¶
| Situation | Diagnostic | Résolution |
|---|---|---|
Erreur 110 + topup_checkout pending (lemonwayP2P null) |
Wire-in Apple Pay / carte en transit | Attendre 3 jours ouvrés. Pas de bug |
| Erreur 110 + aucun pending credit | Le worker aurait dû fail le P2P (alerte Slack envoyée) | Vérifier l'alerte Slack. Annulation manuelle si nécessaire |
Erreurs code 1 en boucle (avec context dans rawError) |
GET masque la vraie erreur POST | Corrigé par PR #4679. Si antérieur au deploy (26/02/2026), c'est attendu |
| Débit plus récent confirmé avant le buying_deal | Race condition ordering dans le worker | Corrigé par PR #4679. Si antérieur au deploy, c'est la cause |
| Compte buyer ou seller bloqué | transactionRights, blockedTemporarilyAtLemonway, ou lemonwayStatus |
Débloquer le compte ou contacter Lemonway |
Deal reserved depuis très longtemps |
Pas de timeout automatique sur les deals reserved | Le deal reste réservé tant que le P2P n'a pas abouti ou échoué |
| Balance OK mais P2P en backoff long | Backoff exponentiel technique (5min * 2^n, cap ~10.67h) | Attendre le prochain attempt (voir attemptAfter) |