Skip to content

Payout withdrawal copy — in-flight, completed, failed (draft for review)

Owner: terminales-liquidaciones · Date: 2026-09-12 · Status: DRAFT — not shipped (W76/W77 re-enable held pending Alfredo). es-MX. Same voice as #907.

The PO asked for the hardest copy in the feature — what a merchant sees while a withdrawal is in flight and when it fails — drafted now because it’s needed whichever way D2 is ruled. This is a screen about their money.


⚠️ Finding first: the in-flight copy already exists — and already over-promises

Section titled “⚠️ Finding first: the in-flight copy already exists — and already over-promises”

This isn’t greenfield. settlement.i18n.ts already carries in-flight copy, and it promises a timeframe we cannot keep under ops-manual disbursement:

  • disposeModal.processingNote (:342): “Tu solicitud la procesa el equipo de terminales; normalmente en 1 día hábil. Te notificaremos al completarse.”
  • disposeModal.successBody (:356): “Tu liquidación {folio} por {amount} quedó en proceso. Te notificaremos cuando esté completada (normalmente 1 día hábil).”

The PO’s own operational point makes this the exact trap: “ops-manual means someone must actually do it, promptly, every time… if nobody is watching the queue on a Saturday, the copy we just shipped becomes a lie.” “Normalmente en 1 día hábil” is that promise. Until there is an owner + a cadence for the disbursement queue that makes “1 día hábil” true, we should not state an interval. This is the same honesty discipline as the empty-state date copy (#907, #900) — say what’s true, don’t promise a time we don’t control.

Recommendation: drop the interval from both existing lines; state the mechanism (a person on the terminales team completes it) and that we notify on completion, without a duration. If Alfredo commits to a queue-watch SLA (owner + cadence, incl. weekends), we can add a truthful interval later — but the copy should not assert it before the operation guarantees it.


1. Corrected — submit-time / in-flight (disposeModal)

Section titled “1. Corrected — submit-time / in-flight (disposeModal)”
Key Current Proposed
processingNote “…el equipo de terminales; normalmente en 1 día hábil. Te notificaremos al completarse.” “Tu solicitud la procesa el equipo de terminales. Te notificaremos aquí y por notificación en cuanto tu retiro se complete.”
successBody(folio, amount) “…quedó en proceso. Te notificaremos cuando esté completada (normalmente 1 día hábil).” “Tu liquidación {folio} por {amount} quedó en proceso. Te notificaremos en cuanto se complete.”

2. New — explanatory bodies on the request detail (request)

Section titled “2. New — explanatory bodies on the request detail (request)”

The request.status labels (“En proceso” / “Liquidada” / “Rechazada” / “Revertida”) exist but carry no explanation. Add one honest body per state:

Proposed key Copy Why
request.inProgressNote “Tu retiro está en proceso. El monto ya se apartó de tu saldo disponible y el equipo de terminales lo está enviando a tu cuenta bancaria. Te avisaremos en cuanto se complete.” Truthful on all three facts the code guarantees: money is reserved (debited from available), a person completes the transfer (not “en línea”/instant), no timeframe.
request.settledNote “Tu retiro se completó. El monto se envió a tu cuenta bancaria. Conserva el número de referencia para tu conciliación.” The reference + completedAt are shown as fields; the body points to them.
request.rejectedNote(reason) “Este retiro fue rechazado y no se envió. Te devolvimos el monto completo a tu saldo disponible, así que puedes intentarlo de nuevo. Motivo: {motivo}.” The hardest line. The reversal is real (reject() → compensating credit restores available), so the copy MUST say the money came back — otherwise the merchant thinks it vanished. States: not sent, fully refunded, retryable, why.

Verification of the rejected line (the only line asserting a fact about money) — done 2026-09-12

Section titled “Verification of the rejected line (the only line asserting a fact about money) — done 2026-09-12”
  • “completo” holds — money is restored net of nothing. reject() calls enqueueReversal(movementId); the engine (wallet-engine.service.ts:315-337) loads the original WITHDRAWAL and appends a compensating CREDIT of original.amount (the full requested amount — the original debit carried no fee; commissions are taken upstream at rollup, not at withdrawal). Balance apply (:834-835): availableBalance + signedAmount(CREDIT, amount) = + full amount. No fee, no rounding, no partial. Σ(movements) nets the original to zero.
    • One honest nuance: the wallet balance is always fully restored. The settlement report’s “Disponible” figure is restored best-effort (reject() skips it only if that report was re-rolled/missing) — in that edge case the money is still fully back in the wallet, it just may not re-appear on that one report’s line. Does not undermine “completo” for the money; flagged for completeness.
  • {motivo} is free-text ops input, NOT an internal code. It is UpdateSettlementStatusDto.reason (@IsString, 1–512 chars), typed by the platform admin in the reject dialog. There is no enum/slug, so the verification-page reason-code→es-MX mapping lesson does not apply (nothing to map). The different real risk: it renders to the merchant verbatim, so an ops note written for us (“dup”, “cuenta rara”, English, curt) would reach the merchant as-is.
    • RULED (PO 2026-09-12): the reject-dialog hint is MANDATORY and ships in the SAME PR as the rejected body. The copy and the guardrail are one change — the honest message depends on the field being written honestly, and nobody writes carelessly on purpose; they write carelessly because nothing told them someone else would read it. Add to ops.reasonPlaceholder/hint: “Este motivo lo verá el comercio.” (es-MX, on the reject dialog).
    • Curated es-MX reason presets: follow-up, not a blocker (PO). They need thought about which rejection reasons are legitimate to show a merchant at all — separate change.

withdrawNote (#907: “El retiro a tu cuenta aún no está disponible en línea.”) is the absent-CTA copy; it is removed when DISPOSE_ENABLED goes true. Note this in the re-enable PR so nobody wonders where it went.


  • The copy above deliberately never says “inmediato”, “en línea”, or a duration. It is honest under manual disbursement today, and stays honest if a rail is automated later.
  • If Alfredo wants a stated turnaround, that is an operational commitment (queue owner + cadence, weekends included), not a copy tweak. The copy can then truthfully carry it. Until then, silence on timing is the honest choice.
  • REJECTED honesty has a sibling on the merchants side: the bank-account card currently collapses account-status REJECTED→‘pending’ (merchants item 3, riding with MERCH-BANK-WRITE). A merchant told “pendiente” when their account was actually rejected is the same class of lie as an unexplained withdrawal — worth fixing in the same wave.