@bakissation/tasdid-adapters
v1.2.1
Published
Lightweight, vendor-agnostic framework bindings for @bakissation/tasdid — mount the SATIM payment lifecycle (start/return/reconcile/refund) on the Web Fetch API: Next.js App Router, Hono, Remix, SvelteKit, Cloudflare Workers, Bun, Deno.
Maintainers
Readme
@bakissation/tasdid-adapters
Lightweight, vendor-agnostic framework bindings for @bakissation/tasdid. Mount the SATIM (CIB/Edahabia) payment lifecycle — start / return / reconcile / refund — as routes, in a few lines, on the Web Fetch API: Next.js App Router, Hono, Remix, SvelteKit, Cloudflare Workers, Bun, Deno.
npm i @bakissation/tasdid-adaptersWhy
Wiring the payment lifecycle into a route is the same boring glue every time: validate the body, call tasdid, map errors to HTTP, redirect the buyer. This does it once — and binds to the Web Fetch standard, not a framework, so it survives framework majors (Next 14→15→16…) untouched and runs on any Fetch runtime. No framework dependency, no vendor lock-in. Orchestration stays in tasdid; this is thin.
Quick start (Next.js App Router)
// lib/pay.ts
import { createCheckout, createPostgresStore } from '@bakissation/tasdid';
import { createSatimClient } from '@bakissation/satim';
import { createFetchHandlers } from '@bakissation/tasdid-adapters/fetch';
const store = createPostgresStore(pgPool);
const checkout = createCheckout({ satim: createSatimClient(satimConfig), store });
export const pay = createFetchHandlers(checkout, {
successUrl: '/thanks',
failUrl: '/payment-failed',
store, // enables the reconcile sweep
authorize: ({ headers }) => // guards refund + reconcile (you pick the scheme)
headers.authorization === `Bearer ${process.env.PAY_ADMIN_TOKEN}`,
});// app/api/pay/route.ts
import { pay } from '@/lib/pay';
export const dynamic = 'force-dynamic';
export const POST = pay.start;Each remaining route file is the same one line:
// app/api/pay/return/route.ts → export const GET = pay.handleReturn // SATIM redirects buyer here
// app/api/pay/reconcile/route.ts → export const GET = pay.reconcile // scheduled sweep (guarded)
// app/api/pay/refund/route.ts → export const POST = pay.refund // admin (guarded)The browser navigates to the returned redirectUrl (full page — SATIM's hosted page, an independent context = PCI SAQ-A). On return, handleReturn reconfirms against the gateway (never trusts the redirect) and 303s the buyer to successUrl/failUrl with ?payment=<id>.
The reconcile sweep — schedule it with anything
SATIM has no webhooks, so a periodic sweep is how abandoned/expired orders settle. pay.reconcile is just a guarded GET — hit it from any scheduler (system cron, a CI schedule, a worker/queue, your platform's cron). Vendor-agnostic by design; auth is your authorize hook:
curl -H "Authorization: Bearer $PAY_ADMIN_TOKEN" https://yourapp/api/pay/reconcileIt returns operational counts (paid/failed/expired/refunded/stillPending) plus a failures list — each { paymentId, code }, where code is a safe reason (e.g. REFUND_FAILED, or UNKNOWN) so you can triage a sweep without leaking gateway internals.
Other runtimes (same handlers)
// Hono / Workers / Bun / Deno
app.post('/api/pay', (c) => pay.start(c.req.raw));
app.get('/api/pay/return', (c) => pay.handleReturn(c.req.raw));
// React Router v7 / Remix — resource routes ({ request } is a Web Request)
export const action = ({ request }) => pay.start(request); // app/routes/api.pay.tsx
export const loader = ({ request }) => pay.handleReturn(request); // app/routes/api.pay.return.tsxExpress / Connect / Fastify (Node http)
Not Fetch-native? The /node entry binds to Node's http (IncomingMessage → ServerResponse) — same handlers, same core, still zero framework deps.
import { createNodeHandlers } from '@bakissation/tasdid-adapters/node';
const pay = createNodeHandlers(checkout, { successUrl: '/thanks', failUrl: '/payment-failed', store, authorize });
// Express / Connect — req/res are Node's objects, so mount directly
app.use(express.json());
app.post('/api/pay', pay.start);
app.get ('/api/pay/return', pay.handleReturn);
app.get ('/api/pay/reconcile', pay.reconcile);
app.post('/api/pay/refund', pay.refund);
// Fastify — pass the parsed body and hijack the reply (Fastify drains req.raw)
fastify.post('/api/pay', (req, reply) => { reply.hijack(); return pay.start(req.raw, reply.raw, { body: req.body }); });
// NestJS rides /node. Express platform (default) — req/res are the native objects:
@Post() start(@Req() req: Request, @Res() res: Response) { return pay.start(req, res); }
// Fastify platform — hijack + raw: res.hijack(); return pay.start(req.raw, res.raw, { body: req.body });Options
| Option | |
|---|---|
| successUrl / failUrl | path/URL, or (result) => string, for the return redirect |
| store | the same PaymentStore your checkout uses — required for reconcile |
| authorize | guard for refund + reconcile; you choose the scheme (bearer, session, IP…) |
| sweepLimit | max payments per reconcile run |
| onError | override the TasdidError → HTTP mapping |
Errors map by code: INVALID_INPUT → 400, NOT_FOUND → 404, refund/transition conflicts → 409, gateway failures → 502, else 500. Bodies are generic ({ error, code }) — no gateway internals, no card data.
Footprint
Zero framework dependency. Peers: @bakissation/tasdid + @bakissation/dinar (you already have them). One package, subpath entries — /fetch (Next.js App Router, Hono, Remix / React Router v7, SvelteKit, Workers, Bun, Deno) + /node (Express, Connect, Fastify, NestJS), same headless core (the . export, createPaymentHandlers).
License
MIT © Abdelbaki Berkati
Credits
Built and maintained by Abdelbaki Berkati — berkati.xyz · @bakissation.
