You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
109 lines
3.1 KiB
109 lines
3.1 KiB
/**
|
|
* Pagos con Stripe.
|
|
*
|
|
* Se usa Checkout alojado en Stripe: la web no ve nunca los datos de la
|
|
* tarjeta, no hay formulario de pago propio que mantener y el cumplimiento de
|
|
* PCI queda del lado de Stripe.
|
|
*
|
|
* Configuracion (ver .env.example):
|
|
* STRIPE_SECRET_KEY clave secreta de la cuenta
|
|
* STRIPE_WEBHOOK_SECRET secreto del endpoint /api/stripe/webhook
|
|
*/
|
|
|
|
import Stripe from 'stripe';
|
|
import { env } from '$env/dynamic/private';
|
|
import { MONEDA, PRECIO_PACK_CENTIMOS, CANCIONES_POR_PACK } from '$lib/tienda';
|
|
|
|
export class ErrorDePago extends Error {
|
|
constructor(mensaje: string) {
|
|
super(mensaje);
|
|
this.name = 'ErrorDePago';
|
|
}
|
|
}
|
|
|
|
let cliente: Stripe | null = null;
|
|
|
|
/** `true` si la tienda puede cobrar en este entorno. */
|
|
export function pagosConfigurados(): boolean {
|
|
return Boolean(env.STRIPE_SECRET_KEY);
|
|
}
|
|
|
|
function stripe(): Stripe {
|
|
if (!env.STRIPE_SECRET_KEY) {
|
|
throw new ErrorDePago('Los pagos no están configurados en este entorno.');
|
|
}
|
|
cliente ??= new Stripe(env.STRIPE_SECRET_KEY);
|
|
return cliente;
|
|
}
|
|
|
|
export interface PeticionDePago {
|
|
packs: number;
|
|
email: string;
|
|
usuarioId: string;
|
|
urlExito: string;
|
|
urlCancelacion: string;
|
|
/** Slugs comprados, para poder reconstruir el pedido desde el webhook. */
|
|
slugs: string[];
|
|
}
|
|
|
|
/**
|
|
* Crea la sesion de pago y devuelve su id y la URL a la que redirigir.
|
|
*
|
|
* El importe no se envia como total: se envia el precio del pack y la
|
|
* cantidad, para que la factura de Stripe diga exactamente lo mismo que el
|
|
* carrito de la web.
|
|
*/
|
|
export async function crearSesionDePago(peticion: PeticionDePago): Promise<{
|
|
id: string;
|
|
url: string;
|
|
}> {
|
|
const sesion = await stripe().checkout.sessions.create({
|
|
mode: 'payment',
|
|
customer_email: peticion.email,
|
|
client_reference_id: peticion.usuarioId,
|
|
line_items: [
|
|
{
|
|
quantity: peticion.packs,
|
|
price_data: {
|
|
currency: MONEDA,
|
|
unit_amount: PRECIO_PACK_CENTIMOS,
|
|
product_data: {
|
|
name: `Pack de ${CANCIONES_POR_PACK} canciones`,
|
|
description: 'Descarga en MP3 de las canciones elegidas, con todas sus versiones.'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
metadata: {
|
|
usuarioId: peticion.usuarioId,
|
|
// Metadato informativo: la fuente de verdad son las filas de
|
|
// pedido_item, no esta cadena.
|
|
canciones: peticion.slugs.join(',').slice(0, 480)
|
|
},
|
|
success_url: peticion.urlExito,
|
|
cancel_url: peticion.urlCancelacion
|
|
});
|
|
|
|
if (!sesion.url) {
|
|
throw new ErrorDePago('Stripe no ha devuelto una URL de pago.');
|
|
}
|
|
|
|
return { id: sesion.id, url: sesion.url };
|
|
}
|
|
|
|
/** Comprueba la firma del webhook y devuelve el evento ya verificado. */
|
|
export function verificarEventoDeWebhook(cuerpo: string, firma: string | null): Stripe.Event {
|
|
if (!env.STRIPE_WEBHOOK_SECRET) {
|
|
throw new ErrorDePago('Falta STRIPE_WEBHOOK_SECRET.');
|
|
}
|
|
if (!firma) {
|
|
throw new ErrorDePago('La petición no lleva firma de Stripe.');
|
|
}
|
|
return stripe().webhooks.constructEvent(cuerpo, firma, env.STRIPE_WEBHOOK_SECRET);
|
|
}
|
|
|
|
/** Recupera una sesion de pago para comprobar su estado. */
|
|
export async function obtenerSesionDePago(id: string): Promise<Stripe.Checkout.Session> {
|
|
return stripe().checkout.sessions.retrieve(id);
|
|
}
|