npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mostajs/booking-payment

v0.1.0

Published

Adapter thin entre @mostajs/booking et @mostajs/payment (multi-provider Stripe/Chargily/Satim/PayPal). Hook beforeReserve capture deposit, webhook handler confirme reservation après paiement, refund automatique selon cancellation window.

Readme

@mostajs/booking-payment

Auteur : Dr Hamid MADANI [email protected] License : AGPL-3.0-or-later Version : 0.1.0

Adapter thin entre @mostajs/booking et @mostajs/payment (multi-provider Stripe / Chargily / Satim CIB / PayPal). Pas de duplication de logique paiement — juste la glue :

  • Hook afterReserve crée un checkout, stocke l'URL dans reservation.metadata.paymentUrl
  • Webhook handler vérifie signature + confirme la reservation au paiement réussi
  • Hook beforeCancel lance refund automatique selon la cancellation window de la resource

Table des matières

  1. Architecture
  2. Quick start
  3. API
  4. Refund policies
  5. Multi-provider (devise auto)
  6. Webhook routing
  7. Patterns
  8. Limites v0.1
  9. Modules liés

1. Architecture

                          POST /api/booking/reserve
                                    │
                                    ▼
        ┌──────────────────────────────────────────────┐
        │   manager.reserve({ slotId, userId, ... })   │
        │   → reservation status='pending'              │
        │   → fires afterReserve hook                  │
        └────────────────────────┬─────────────────────┘
                                 │
                                 ▼
        ┌──────────────────────────────────────────────┐
        │ adapter.afterReserve (booking-payment hook)  │
        │   → resolveAmount(reservation, slot, ...)    │
        │   → provider.createCheckout(...)             │
        │   → reservation.metadata.paymentUrl = URL    │
        └──────────────────────────────────────────────┘
                                 │
                                 ▼
        App reads metadata.paymentUrl, redirects user to Stripe
                                 │
                                 ▼
                  USER PAYS ON STRIPE / CHARGILY
                                 │
                                 ▼
              POST /api/booking/payment/webhook (Stripe)
                                 │
                                 ▼
        ┌──────────────────────────────────────────────┐
        │ adapter.webhookHandler(req)                  │
        │   → provider.verifyWebhook(body, signature)  │
        │   → event.type === 'checkout.session.completed'?
        │   → manager.confirmReservation(reservationId)│
        │   → reservation.status = 'confirmed'         │
        └──────────────────────────────────────────────┘

Pour les cancellations :

manager.cancelReservation(id, reason)
  → beforeCancel hook
  → isRefundable(reservation, slot, resource) ?
  → provider.createRefund(paymentSessionId)
  → reservation.metadata.refundId / refundedAmount

2. Quick start

Installation

npm install @mostajs/booking @mostajs/payment @mostajs/booking-payment

Setup avec Stripe

import { createBookingManager } from '@mostajs/booking'
import { createBookingPayment } from '@mostajs/booking-payment'
import { stripeProvider } from '@mostajs/payment'  // ou un provider custom

const stripe = stripeProvider({
  secretKey: process.env.STRIPE_SECRET!,
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
})

const adapter = createBookingPayment({
  manager,
  provider: stripe,
  // Calcule le montant selon resource.kind (médical = gratuit, resto = deposit fixe, etc.)
  resolveAmount: (reservation, slot, resource) => {
    if (resource?.kind === 'doctor-visit') return null  // gratuit, géré par sécu
    if (resource?.kind === 'restaurant-table')
      return { amount: 1000, currency: 'EUR', description: 'Deposit restaurant' }
    if (resource?.kind === 'event')
      return { amount: (resource.metadata as any)?.price ?? 2000, currency: 'EUR' }
    return null
  },
  successUrl: r => `https://app.example.com/booking/${r.id}/confirmed`,
  cancelUrl:  r => `https://app.example.com/booking/${r.id}/cancelled`,
  webhookUrl: 'https://app.example.com/api/booking/payment/webhook',
  refundPolicy: 'window-based',  // refund si dans la cancellationWindow.freeUntilMs de la resource
})

// Plug les hooks
const manager = createBookingManager({
  // ...
  hooks: adapter.hooks,
})

Route webhook Next.js

// app/api/booking/payment/webhook/route.ts
import { adapter } from '@/lib/booking-payment'

export async function POST(req: Request) {
  return adapter.webhookHandler(req, { signatureHeader: 'stripe-signature' })
}

Flow côté app

// app/api/booking/slots/[slotId]/reserve/route.ts
import { manager } from '@/lib/booking'

export async function POST(req: Request, { params }) {
  const { slotId } = await params
  const session = await getServerSession()
  const reservation = await manager.reserve({ slotId, holderUserId: session.user.id })
  // adapter.afterReserve a enrichi metadata.paymentUrl automatiquement
  const paymentUrl = (reservation.metadata as any)?.paymentUrl
  if (!paymentUrl) return Response.json(reservation)  // gratuit
  return Response.json({ ...reservation, redirectTo: paymentUrl })
}

3. API

createBookingPayment(opts) → BookingPaymentAdapter

interface BookingPaymentOptions {
  manager: BookingManager
  provider: BookingPaymentProvider   // de @mostajs/payment

  /** Resolveur du montant (retourne null pour skip paiement). */
  resolveAmount: (reservation, slot, resource) =>
    { amount: number; currency?: string; description?: string } | null

  /** URLs success / cancel (string template avec {reservationId} OU fonction). */
  successUrl: string | ((reservation) => string)
  cancelUrl:  string | ((reservation) => string)

  /** URL absolue du webhook (envoyé au provider). */
  webhookUrl?: string

  /** Politique de remboursement. Default 'window-based'. */
  refundPolicy?: 'always' | 'window-based' | 'never' | callback

  defaultCurrency?: string             // default 'EUR'
  autoConfirmOnPayment?: boolean       // default true

  // Callbacks
  onCheckoutCreated?:  (r, url, sessionId) => void
  onPaymentSucceeded?: (r, sessionId) => void
  onPaymentFailed?:    (r, reason) => void
  onRefunded?:         (r, refundId, amount) => void
  onError?:            (where, error) => void
}
interface BookingPaymentAdapter {
  hooks: BookingHooks                  // à passer à createBookingManager({ hooks })

  /** Trigger manuel d'un checkout (si app a créé la reservation séparément). */
  startCheckout(reservationId: string): Promise<{ url: string; sessionId: string } | null>

  /** Web-standard handler pour le webhook provider. */
  webhookHandler(req: Request, opts?: { signatureHeader?: string }): Promise<Response>

  /** Refund manuel (e.g. depuis admin UI). */
  refund(reservationId: string, opts?: { amount?: number; reason?: string }):
    Promise<{ id: string; amount: number; status: string } | null>
}

BookingPaymentProvider interface

Sous-ensemble de PaymentProvider de @mostajs/payment que l'adapter consomme :

interface BookingPaymentProvider {
  createCheckout(params): Promise<{ url: string | null; sessionId: string }>
  verifyWebhook(body: string, signature: string): Promise<{ type: string; data: Record<string, unknown> }>
  createRefund?(params): Promise<{ id: string; amount: number; status: string }>
}

Tout provider qui implémente cette interface marche : Stripe, Chargily, Satim CIB, PayPal (tous fournis par @mostajs/payment), ou un provider custom (test, sandbox, mock).


4. Refund policies

'window-based' (default)

Refund si slot.startAt - now > resource.policy.cancellationWindow.freeUntilMs. Exemple :

const restaurantTable = await manager.createResource({
  kind: 'restaurant-table',
  // ...
  policy: { cancellationWindow: { freeUntilMs: 2 * 3600_000 } },  // 2h
})

Annulation > 2h avant le slot → refund. Annulation tardive → no refund (deposit conservé).

'always'

Refund systématique. Cas d'usage : promotions, gestes commerciaux par défaut.

'never'

Pas de refund. Cas : Eventbrite tickets (achat ferme).

Custom callback

refundPolicy: async (reservation, slot, resource) => {
  // Logique métier custom : refund si user a un compte VIP, ou si moins de X cancellations dans l'année, etc.
  const user = await usersRepo.findById(reservation.holderUserId!)
  if (user?.tier === 'vip') return true
  const recentCancels = await reservationsRepo.count({
    holderUserId: user.id,
    status: 'cancelled',
    cancelledAt: { $gt: Date.now() - 365 * 86400_000 },
  })
  return recentCancels < 3
},

5. Multi-provider (devise auto)

@mostajs/payment v0.5 expose pickProviderByCurrency :

import { pickProviderByCurrency } from '@mostajs/payment/lib/webhook-helpers'

const adapter = createBookingPayment({
  manager,
  // Provider dynamic : pour DZD utilise Chargily, sinon Stripe
  provider: {
    async createCheckout(params) {
      const { provider } = pickProviderByCurrency(params.currency)
      return provider.createCheckout(params)
    },
    async verifyWebhook(body, signature) {
      // l'app doit savoir quel provider, via discriminator dans le path ou header
      throw new Error('Use provider-specific webhook routes for multi-provider')
    },
  },
  // ...
})

Pour le webhook multi-provider, mieux : N routes Next, une par provider :

  • /api/booking/payment/webhook/stripeadapterStripe.webhookHandler(req)
  • /api/booking/payment/webhook/chargilyadapterChargily.webhookHandler(req, { signatureHeader: 'signature' })

Avec 2 instances d'adapter, chacune avec son provider.


6. Webhook routing

Stripe

export async function POST(req: Request) {
  return adapter.webhookHandler(req)  // signatureHeader = 'stripe-signature' par défaut
}

Chargily

export async function POST(req: Request) {
  return adapterChargily.webhookHandler(req, { signatureHeader: 'signature' })
}

Events Stripe gérés

| Event | Action | |---|---| | checkout.session.completed | manager.confirmReservation(reservationId) | | payment_intent.succeeded | idem | | payment_intent.payment_failed | manager.cancelReservation(id, 'payment-failed') | | checkout.session.expired | idem | | autres | ignore (200) |

L'identification de la reservation se fait via metadata.reservationId injecté dans createCheckout.


7. Patterns

Pattern A — Booking gratuit + payant mixés

resolveAmount: (reservation, slot, resource) => {
  // null = pas de paiement (médical, cours gratuit, etc.)
  if (resource?.metadata?.price == null || resource.metadata.price === 0) return null
  return { amount: resource.metadata.price as number, currency: 'EUR' }
},

Pattern B — Pricing dynamique (depuis @mostajs/booking-pricing futur)

import { calculatePrice } from '@mostajs/booking-pricing'

resolveAmount: async (reservation, slot, resource) => {
  const price = await calculatePrice({ reservation, slot, resource })
  if (!price) return null
  return { amount: price.amount, currency: price.currency, description: price.label }
},

Pattern C — Frais d'annulation tardive (no full refund)

refundPolicy: async (reservation, slot, resource) => {
  const ttl = slot.startAt - Date.now()
  const window = resource?.policy?.cancellationWindow?.freeUntilMs ?? 0
  if (ttl > window) return true                         // refund full
  if (ttl > window / 2) {
    // refund partial via adapter.refund(id, { amount: original / 2 })
    // (logique custom à mettre dans la route /cancel côté app)
    return false  // skip auto-refund, app gère manual
  }
  return false  // forfeit total
},

Pattern D — Tests sans Stripe (mock provider)

const mockProvider: BookingPaymentProvider = {
  async createCheckout(params) {
    return { url: `http://localhost:3000/mock-pay/${params.orderId}`, sessionId: `mock_${params.orderId}` }
  },
  async verifyWebhook(body, _sig) {
    return JSON.parse(body)
  },
  async createRefund(params) {
    return { id: `mock_refund_${Date.now()}`, amount: params.amount ?? 0, status: 'succeeded' }
  },
}

Pattern E — Audit / Prometheus

const adapter = createBookingPayment({
  // ...
  onCheckoutCreated:  (r, url, sid) => metrics.increment('payment.checkout', { resource: r.resourceId }),
  onPaymentSucceeded: (r) =>          metrics.increment('payment.succeeded', { resource: r.resourceId }),
  onPaymentFailed:    (r, reason) =>  metrics.increment('payment.failed',   { reason }),
  onRefunded:         (r, _, amt) =>  metrics.observe('payment.refund.amount', amt),
  onError:            (where, err) => sentry.captureException(err, { tags: { where } }),
})

8. Limites v0.1

  • Pas de partial refund automatique : utiliser adapter.refund(id, { amount }) manuellement
  • reservation.metadata.paymentUrl set in-place : pour persister en DB, l'app doit appeler son repo.save() après reserve. Le manager core ne réexpose pas updateReservation directement (à fixer dans booking v0.2)
  • Pas de retry automatique sur webhook (Stripe retry side, à configurer côté Stripe Dashboard)
  • Pas d'idempotence sur double webhook (Stripe envoie parfois 2× le même event). Le manager.confirmReservation lui-même est idempotent (rejette si status ≠ 'pending'), donc OK en pratique
  • Refund window basée uniquement sur resource.policy.cancellationWindow.freeUntilMs : pour des windows plus complexes (échelonnée), utiliser refundPolicy callback

9. Modules liés


License : AGPL-3.0-or-later Auteur : Dr Hamid MADANI [email protected]