Aller au contenu

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