Diagnostic de clôture de collecte¶
Workflow pour identifier ce qui bloque la clôture d'une collecte sur une property, en utilisant le MCP prod readonly.
Étape 1 : Trouver la property¶
Le champ name est jsonb, il faut caster en text pour chercher.
SELECT id, name, status, "publishStatus", funding
FROM properties
WHERE name::text ILIKE '%NOM_RECHERCHE%'
LIMIT 10
Colonnes utiles de funding (jsonb) :
- amountToFundCents : objectif en centimes
- startedAt, maxEndDate : dates de la collecte
- brickPrice : prix unitaire d'une brick
- ended : présent uniquement si la collecte est terminée (ended.type = success ou failure)
Étape 2 : État de la collecte (bricks)¶
SELECT status, "isAvailableOnPrimary", COUNT(*) as count
FROM bricks
WHERE "propertyId" = '{PROPERTY_ID}'
GROUP BY status, "isAvailableOnPrimary"
ORDER BY count DESC
Comparer le total avec amountToFundCents / brickPrice pour calculer le pourcentage de collecte.
Étape 3 : Achats primaires par statut¶
SELECT "status_view",
SUM((json->>'brickCount')::int) as total_bricks,
COUNT(*) as purchase_count
FROM primary_purchase
WHERE "propertyId_view" = '{PROPERTY_ID}'
GROUP BY "status_view"
ORDER BY total_bricks DESC
Statuts clés :
- confirmed : achats finalisés
- waiting_for_contract_creation : paiement fait, contrat pas encore créé (bloquant)
- refunded : remboursés
- declined : refusés
Étape 4 : Détail des achats bloqués¶
Si des achats sont en waiting_for_contract_creation :
SELECT pp.json->>'brickCount' as brick_count,
pp.json->>'investorId' as investor_id,
pp."createdAt_view",
pp.json as purchase_json,
c.email,
cp."firstName", cp."lastName"
FROM primary_purchase pp
LEFT JOIN customers c ON c.id = (pp.json->>'investorId')::uuid
LEFT JOIN customer_profile cp ON cp."customerId" = (pp.json->>'investorId')::uuid
WHERE pp."propertyId_view" = '{PROPERTY_ID}'
AND pp."status_view" = 'waiting_for_contract_creation'
ORDER BY pp."createdAt_view" DESC
Étape 5 : Vérifier les wallet transactions¶
Récupérer les purchaseWtId depuis le JSON des achats bloqués, puis :
SELECT wt.id,
wt."customerId",
wt.status,
wt."lemonwayP2P"->>'status' as p2p_status,
jsonb_array_length(wt."lemonwayP2P"->'attempts') as attempt_count,
wt."lemonwayP2P"->'attempts'->-1 as last_attempt
FROM wallet_transactions wt
WHERE wt.id IN ('{WT_ID_1}', '{WT_ID_2}', ...)
Si status = 'waiting' et p2p_status = 'pending' : le P2P Lemonway n'a pas abouti.
Étape 6 : Diagnostic P2P¶
Distinguer erreurs POST vs GET¶
Dans les attempts, la présence de rawError.context indique une erreur provenant du GET (findExistingP2PReferenceAtLw), pas du POST (playP2P). Le GET peut masquer la vraie erreur (code 1 "Unknown error" au lieu de code 110 "insufficient balance").
SELECT wt.id,
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 (findExistingP2PReferenceAtLw)'
ELSE 'POST (playP2P)'
END as error_source
FROM wallet_transactions wt,
jsonb_array_elements(wt."lemonwayP2P"->'attempts') as attempt
WHERE wt.id = '{WT_ID}'
ORDER BY (attempt->>'attemptedAt')::timestamptz ASC
Système de balances investisseur¶
Le solde d'un investisseur est stocké dans customers.withdrawableBalances (jsonb) et customers.giftBalances (jsonb), calculés par computeAllBalances dans customer-balance.service.ts :
| Champ | Calcul | Signification |
|---|---|---|
current |
SUM(value) WHERE status IN ('confirmed', 'canceled') | Somme des transactions finalisées |
pendingCredit |
SUM(value) WHERE status = 'waiting' AND value > 0 AND kind != 'topup_card' | Crédits en attente |
pendingDebit |
SUM(ABS(value)) WHERE status = 'waiting' AND value < 0 | Débits en attente |
Le solde disponible côté Bricks : available = current + pendingCredit - pendingDebit.
IMPORTANT : la colonne lemonwayBalance sur customers est un alias legacy de availableWithdrawableBalance_view. Elle ne représente PAS le solde réel du wallet Lemonway.
Solde réel du wallet Lemonway ~ current, car les transactions confirmed ont eu leur P2P joué chez LW, les waiting non. Pour savoir si un P2P bloqué peut passer, vérifier current >= montant du P2P (pas available).
SELECT c.id, cp."firstName", cp."lastName",
c."withdrawableBalances", c."giftBalances",
c."lemonwayStatus", c."transactionRights", c."blockedTemporarilyAtLemonway"
FROM customers c
LEFT JOIN customer_profile cp ON cp."customerId" = c.id
WHERE c.id IN ('{INVESTOR_ID_1}', '{INVESTOR_ID_2}', ...)
Pour vérifier la cohérence entre les valeurs stockées et le calcul depuis les transactions :
SELECT c.id, cp."firstName",
(c."withdrawableBalances"->>'current')::int as stored_current,
(c."withdrawableBalances"->>'pendingDebit')::int as stored_pending_debit,
(c."withdrawableBalances"->>'pendingCredit')::int as stored_pending_credit,
COALESCE(SUM(wt.value) FILTER (WHERE wt.status IN ('confirmed', 'canceled')), 0)::INT AS computed_current,
COALESCE(SUM(ABS(wt.value)) FILTER (WHERE wt.status = 'waiting' AND wt.value < 0), 0)::INT AS computed_pending_debit,
COALESCE(SUM(wt.value) FILTER (WHERE wt.status = 'waiting' AND wt.value > 0 AND wt.kind != 'topup_card'), 0)::INT AS computed_pending_credit
FROM customers c
LEFT JOIN customer_profile cp ON cp."customerId" = c.id
LEFT JOIN wallet_transactions wt ON wt."customerId" = c.id
WHERE c.id IN ('{INVESTOR_ID_1}', '{INVESTOR_ID_2}', ...)
GROUP BY c.id, cp."firstName"
Vérifier les crédits en attente (pendingCredit)¶
Les pendingCredit peuvent provenir de :
- topup_checkout : paiement carte via Checkout.com. Si lemonwayP2P IS NULL, le paiement n'est pas encore confirmé par Checkout.com (webhook pas reçu). Le P2P Bricks-Checkout.com -> customer n'est créé qu'après confirmation.
- revenue_obligation_coupon, obligation_principal_repayment : revenus/remboursements depuis un SPV. Si lemonwayP2P->>'status' = 'pending', le P2P credit est bloqué (vérifier les attempts).
SELECT wt.id, wt.kind, wt.value, wt.status, wt."createdAt",
wt."lemonwayP2P"->>'status' as p2p_status,
wt."lemonwayP2P"->>'debitAccountId' as debit_account
FROM wallet_transactions wt
WHERE wt."customerId" = '{INVESTOR_ID}'
AND wt.status = 'waiting'
AND wt.value > 0
ORDER BY wt."createdAt" ASC
Vérifier si d'autres débits ont drainé le wallet¶
SELECT wt.id, wt.kind, wt.value, wt.status, wt."createdAt",
wt."lemonwayP2P"->>'status' as p2p_status
FROM wallet_transactions wt
WHERE wt."customerId" = '{INVESTOR_ID}'
AND wt.status = 'waiting'
AND wt.value < 0
ORDER BY wt."index" ASC
Si un investisseur a des P2P confirmés plus récents que le P2P bloqué, c'est un symptôme de la race condition sur le ranking des débits.
Étape 7 : Vérifier le SPV¶
SELECT spv.json
FROM special_purpose_vehicule spv
JOIN properties p ON p."spvId" = spv.id
WHERE p.id = '{PROPERTY_ID}'
Vérifier dans json.lemonway :
- wallet.status = registeredKYC2
- wallet.isBlocked = false
- account.status = ACCEPTED
Étape 8 : Contexte global du SPV¶
Comparer les P2P réussis vs bloqués vers le même compte crédit :
SELECT DATE(wt."createdAt") as day,
wt."lemonwayP2P"->>'status' as p2p_status,
COUNT(*) as count
FROM wallet_transactions wt
WHERE wt."lemonwayP2P"->>'creditAccountId' = '{SPV_LW_EXTERNAL_ID}'
GROUP BY DATE(wt."createdAt"), wt."lemonwayP2P"->>'status'
ORDER BY day DESC, p2p_status
Si des milliers de P2P passent vers le même SPV mais quelques-uns sont bloqués, le problème est spécifique à ces transactions (pas un problème global du SPV).
Codes d'erreur Lemonway courants¶
| Code | Message | Signification |
|---|---|---|
| 1 | Unknown error / Internal server error | Erreur générique (souvent masquée par le GET) |
| 110 | Amount higher than your account balance | Solde insuffisant sur le wallet débiteur |
| 111 | Debit account blocked | Compte débiteur bloqué |
| 143 | P2P not found | P2P inexistant (utilisé par le GET de vérification) |
| 146 | Debit or credit account blocked | L'un des deux comptes est bloqué |
| 167 | Debit account blocked (regulatory) | Blocage réglementaire |
| 168 | Credit account blocked | Compte créditeur bloqué |
| 187 | Insufficient balance | Solde insuffisant (variante) |
| 348 | Duplicate reference | Référence P2P déjà utilisée |
Flow des achats primaires¶
Achat créé
→ waiting_for_bricks_assignation (bricks assignées par le watcher)
→ waiting_for_p2p (P2P Lemonway créé par le watcher)
→ waiting_for_contract_creation (P2P joué, en attente de confirmation)
→ confirmed (P2P confirmé par PlayedLemonwayP2PWorker, contrat créé)
Le passage de waiting_for_contract_creation à confirmed dépend de :
1. Le P2P Lemonway passe de pending à succeeded
2. Le PlayedLemonwayP2PWorker détecte le changement et appelle confirmPrimaryPurchase