Saltar al contenido principal

Pagos (Stripe) — Referencia técnica

← Volver a Pagos (Stripe)

Dónde vive esto

Backend

Frontend

El antiguo apps/frontend-nextjs/src/page-components/PaymentsPage.tsx ya no está conectado a ninguna ruta (/payments redirige en lugar de renderizarlo) — ha sido reemplazado por las dos páginas settings/ mencionadas arriba.

Checklist de implementación técnica

  • addPaymentMethod — resolver conectado en payment-methods.resolvers.js; se llama desde PaymentMethodsPage.tsx
  • myPaymentMethods — query conectada; PaymentMethodsPage.tsx lista las tarjetas mediante useQuery de Apollo
  • purchaseCoinsWithPayment — resolver conectado en coin-purchase.resolver.js, respaldado por el servicio de Stripe PaymentIntent; se llama desde StripeCheckout.tsx
  • myPaymentTransactions / paymentTransactionStats — resolvers conectados en payment-transaction.resolver.js; consultados por PaymentHistoryPage.tsx en /settings/payment-history, distinto del historial exclusivo de compras de monedas que muestra CoinTransactionsPage.tsx / myCoinPurchases
  • removePaymentMethod — resolver conectado; se llama desde PaymentMethodsPage.tsx; despacha a la API de eliminación de payment tokens de PayPal para métodos con provider: 'paypal'
  • createPaypalVaultSetupToken / confirmPaypalVaultSetup — guarda una cuenta de PayPal como método de pago reusable; conectado en payment-methods.resolvers.js + el flujo "Connect PayPal" de PaymentMethodsPage.tsx

Modelo PaymentTransaction

CampoDescripción
providerSiempre stripe
providerPaymentIntentIdID del PaymentIntent de Stripe
typecoin_purchase / subscription / tip / other
purposePropósito legible para humanos
amount / currencyMonto y moneda
feeAmount / netAmountComisión de Stripe y neto recibido
statuspending / processing / completed / failed / refunded / cancelled
failureCode / failureMessageDetalles del fallo
refundAmount / refundReasonInformación del reembolso

Tipos de método de pago

enum PaymentMethodType {
credit_card · debit_card · paypal · stripe · apple_pay · google_pay
}

Queries

myPaymentTransactions devuelve el historial de pagos del usuario autenticado. Filtra por status (p. ej. completed) o purpose para acotar los resultados. Úsalo para impulsar una pantalla de "Historial de pedidos".

paymentTransaction obtiene una sola transacción por ID — útil para una pantalla de confirmación posterior a la compra o flujos de soporte donde se necesita mostrar failureMessage.

paymentTransactionStats devuelve contadores agregados sin paginar por cada registro. Úsalo para el resumen de "total gastado" en la página de facturación.

query MyPaymentTransactions($status: String, $purpose: String, $limit: Int, $offset: Int) {
myPaymentTransactions(status: $status, purpose: $purpose, limit: $limit, offset: $offset) {
id type purpose amount currency status createdAt
providerPaymentIntentId
}
}

query PaymentTransaction($id: ID!) {
paymentTransaction(transactionId: $id) { id amount status failureMessage }
}

query PaymentStats {
paymentTransactionStats {
totalTransactions totalAmount completedTransactions failedTransactions
}
}

Métodos de pago

Los usuarios pueden guardar métodos de pago de Stripe para compras futuras más rápidas. El bloque billingAddress es requerido por Stripe para la verificación de la tarjeta.

myPaymentMethods lista todos los métodos guardados. Pasa includeInactive: true para mostrar también tarjetas vencidas o eliminadas (útil en flujos de soporte).

addPaymentMethod adjunta un ID de PaymentMethod de Stripe (proveniente del flujo de Stripe.js del lado del cliente) a la cuenta del usuario. Pasa setAsDefault: true para convertirlo en el predeterminado de inmediato.

setDefaultPaymentMethod cambia qué tarjeta guardada se cobra de forma predeterminada en compras futuras.

removePaymentMethod desvincula el método tanto de Closegram como de Stripe. No se puede eliminar el método predeterminado si existen otros — primero hay que establecer uno nuevo como predeterminado.

syncPaymentMethods resuelve discrepancias entre la base de datos local y los registros de Stripe. Llámalo si un usuario reporta que una tarjeta aparece como válida cuando en realidad fue eliminada en Stripe.

query MyPaymentMethods($includeInactive: Boolean) {
myPaymentMethods(includeInactive: $includeInactive) {
id type cardBrand cardLast4 cardExpMonth cardExpYear
isDefault isActive billingName billingEmail
billingAddress { line1 line2 city state postalCode country }
}
}

mutation AddPaymentMethod($paymentMethodId: String!, $setAsDefault: Boolean) {
addPaymentMethod(paymentMethodId: $paymentMethodId, setAsDefault: $setAsDefault) {
success message paymentMethod { id cardBrand cardLast4 isDefault }
}
}
mutation SetDefaultPaymentMethod($paymentMethodId: ID!) {
setDefaultPaymentMethod(paymentMethodId: $paymentMethodId) { success }
}
mutation RemovePaymentMethod($paymentMethodId: ID!) {
removePaymentMethod(paymentMethodId: $paymentMethodId) { success message }
}
# Sync stored methods with Stripe (resolves drift between local DB and Stripe)
mutation SyncPaymentMethods { syncPaymentMethods { success count } }

PayPal como método de pago guardado (Vault API v3)

myPaymentMethods no es exclusivo de Stripe: un usuario también puede guardar una cuenta de PayPal (type: paypal), usando la Vault API v3 de PayPal en lugar de la API de métodos de pago de Stripe. Reutiliza los mismos modelos PaymentMethod/PaymentCustomerprovider: 'paypal' en ambos — así que myPaymentMethods, setDefaultPaymentMethod y removePaymentMethod funcionan sobre una cuenta de PayPal guardada exactamente igual que sobre una tarjeta. removePaymentMethod despacha según provider: para un método de PayPal llama al endpoint de eliminación de payment token de PayPal en lugar del detach de Stripe.

Guardar una cuenta es un flujo de dos pasos, iniciado desde el botón "Connect PayPal" de PaymentMethodsPage.tsx:

  1. createPaypalVaultSetupToken — inicia un setup token de PayPal (sin compra asociada). El frontend pasa su setupTokenId a <PayPalButtons createVaultSetupToken={...}> (@paypal/react-paypal-js), que abre el popup de aprobación.
  2. confirmPaypalVaultSetup(setupTokenId, setAsDefault) — se llama desde onApprove una vez que el comprador aprueba; canjea el setup token por un token de pago permanente de PayPal y crea la fila PaymentMethod (billingEmail viene de la respuesta de PayPal).
mutation CreatePaypalVaultSetupToken {
createPaypalVaultSetupToken { setupTokenId }
}

mutation ConfirmPaypalVaultSetup($setupTokenId: String!, $setAsDefault: Boolean) {
confirmPaypalVaultSetup(setupTokenId: $setupTokenId, setAsDefault: $setAsDefault) {
success message
paymentMethod { id type isDefault isActive billingEmail }
}
}

Una vez guardada, se puede cobrar directamente — ver Monedas y propinas → Métodos de pago alternativos para purchaseCoinsWithSavedPaypal, que cobra el token guardado del lado del servidor sin ningún popup.

No requiere variables de entorno nuevas: reutiliza PAYPAL_CLIENT_ID / PAYPAL_CLIENT_SECRET / PAYPAL_API_BASE / NEXT_PUBLIC_PAYPAL_CLIENT_ID de la configuración del checkout de PayPal de una sola vez — ver apps/backend/docs/PAYMENTS_WALLETS_PAYPAL.md.

Flujo de compra de monedas

1. User selects a coin package on /settings/get-coins
2. Frontend calls createPaymentIntent
3. Backend creates a Stripe PaymentIntent → returns clientSecret
4. Frontend renders <Elements> with the clientSecret
5. User enters card details and confirms
6. Stripe confirms payment → webhook notifies backend
7. Backend receives payment_intent.succeeded
8. Backend credits coins (CoinPurchase + CoinTransaction records)
9. Apollo cache updates myCoinBalance

Seguridad

  • Los datos de la tarjeta nunca pasan por los servidores de Closegram — Stripe los maneja directamente.
  • STRIPE_SECRET_KEY vive únicamente en el backend.
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY es la única clave expuesta al frontend.
  • Las compras se validan mediante un webhook firmado con STRIPE_WEBHOOK_SECRET.

Librerías de frontend

PaquetePropósito
@stripe/stripe-jsCarga Stripe.js de forma asíncrona
@stripe/react-stripe-js<Elements>, <CardElement>, useStripe()