Files
NightSpire/web-next/lib/pay-with-dpoints.ts
T
Inna 809eb756c8 send-gift: permitir pagar con PV (puntos de voto), más caro que PD
Solo en /send-gift se añade una 4ª forma de pago: PV (vote points, gratis por
votar). Cuesta más que PD para preservar su valor: coste_PV = coste_PD ×
VP_PRICE_FACTOR (=2, configurable en lib/pay-with-dpoints).

- lib/dpoints: getVPointsBalance, creditVPoints y spendVPoints (débito atómico
  del campo vp, con FOR UPDATE), espejo de las de PD.
- lib/pay-with-dpoints: VP_PRICE_FACTOR, vpointsCost() y payServiceWithVPoints()
  (descuenta PV, ejecuta el envío y reembolsa si falla).
- gift/checkout: rama provider='vp' (además de 'pd').
- service-success: la entrega inmediata cubre 'pd' y 'vp'.
- PaymentMethodSelect: opción VP opcional (solo si el form pasa vpBalance);
  muestra coste en PV, insuficiencia y el saldo VP.
- SendGiftForm + página: pasan el saldo VP.
- i18n Pay: vp, vpCost, insufficientVp, balanceVp, errors.insufficientVp (es/en).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:18:14 +00:00

76 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spendDPoints, creditDPoints, spendVPoints, creditVPoints, PD_PER_UNIT } from './dpoints'
/**
* Los PV (vote points) son gratis (se ganan votando), así que pagar con PV es
* más caro que con PD: cuesta este factor por el coste en PD. Cambiar aquí.
*/
export const VP_PRICE_FACTOR = 2
/** Coste en PD de un precio en euros (100 PD = 1 €). */
export function dpointsCost(priceEur: number): number {
return Math.round(priceEur * PD_PER_UNIT)
}
/** Coste en PV de un precio en euros (coste PD × VP_PRICE_FACTOR). */
export function vpointsCost(priceEur: number): number {
return dpointsCost(priceEur) * VP_PRICE_FACTOR
}
/**
* Paga un servicio con el saldo PD: descuenta el coste de forma atómica y
* ejecuta la acción al momento (sin pasarela). Si la ejecución falla, reembolsa
* el saldo. Devuelve `insufficientPd` si no hay saldo suficiente.
*/
export async function payServiceWithDPoints(
accountId: number,
priceEur: number,
fulfill: () => Promise<boolean>,
): Promise<{ success: boolean; error?: string }> {
const cost = dpointsCost(priceEur)
if (!(cost > 0)) return { success: false, error: 'invalidRequest' }
const spent = await spendDPoints(accountId, cost)
if (!spent.success) {
return { success: false, error: spent.error === 'insufficientFunds' ? 'insufficientPd' : 'genericError' }
}
let ok = false
try {
ok = await fulfill()
} catch {
ok = false
}
if (!ok) {
await creditDPoints(accountId, cost) // reembolso si no se pudo ejecutar
return { success: false, error: 'fulfillFailed' }
}
return { success: true }
}
/**
* Paga un servicio con el saldo PV: descuenta el coste (más caro que en PD) y
* ejecuta la acción al momento. Reembolsa si falla. Devuelve `insufficientVp`
* si no hay saldo suficiente.
*/
export async function payServiceWithVPoints(
accountId: number,
priceEur: number,
fulfill: () => Promise<boolean>,
): Promise<{ success: boolean; error?: string }> {
const cost = vpointsCost(priceEur)
if (!(cost > 0)) return { success: false, error: 'invalidRequest' }
const spent = await spendVPoints(accountId, cost)
if (!spent.success) {
return { success: false, error: spent.error === 'insufficientFunds' ? 'insufficientVp' : 'genericError' }
}
let ok = false
try {
ok = await fulfill()
} catch {
ok = false
}
if (!ok) {
await creditVPoints(accountId, cost) // reembolso si no se pudo ejecutar
return { success: false, error: 'fulfillFailed' }
}
return { success: true }
}