npm package: @rusoft/paycenter
Description: Express router and helpers for Paycenter proxy and payment confirmation.
Author: Rumesh Weerasinghe
Latest version: 1.0.8


# @rusoft/paycenter-proxy

Express router + helpers for Paycenter proxying and payment confirmation.

# ## Requirements
- Node.js >= 18 (uses built-in `fetch` and `crypto.randomUUID`)

## Install
```bash
npm i @rusoft/paycenter-proxy

## .env Requirements

# Required
PAYCENTER_TOKEN=your_authtoken_here
HMAC_SECRET=your_hmac_secret_here
PAYCENTER_CLIENTID=00000000
PAYCENTER_URL=https://paycorp-xxx.prod.aws.paycorp.lk/rest/service/proxy


## paycenter and Return API setup

# api/paycenter/route.ts

import { NextRequest } from "next/server";
import { proxyToPaycenter } from "@rusoft/paycenter";

export const runtime = "nodejs";

const mustGet = (k: string) => {
  const v = process.env[k];
  if (!v) throw new Error(`Missing env ${k}`);
  return v;
};

export async function POST(req: NextRequest) {
  try {
    const raw = await req.text(); // <-- raw JSON string

    const { status, body } = await proxyToPaycenter(raw, {
      paycenterUrl: mustGet("PAYCENTER_URL"),
      token: mustGet("PAYCENTER_TOKEN"),
      hmacSecret: mustGet("HMAC_SECRET"),
    });

    return new Response(body, {
      status,
      headers: { "content-type": "application/json" },
    });
  } catch (err: any) {
    console.error("Proxy error:", err);
    return new Response(
      JSON.stringify({ error: "Proxy error", message: err?.message }),
      { status: 500, headers: { "content-type": "application/json" } }
    );
  }
}

# api/paycenter/return/route.ts

import { NextRequest, NextResponse } from "next/server";
import { confirmPayment } from "@rusoft/paycenter";

export const runtime = "nodejs";

const must = (k: string) => {
  const v = process.env[k];
  if (!v) throw new Error(`Missing env ${k}`);
  return v;
};

export async function GET(req: NextRequest) {
  const url = new URL(req.url);
  const bridge = url.searchParams.get("bridge") === "1";
  const reqid = url.searchParams.get("reqid") || "";
  const clientRef = url.searchParams.get("clientRef") || "";

  const { status, body } = await confirmPayment(reqid, {
    paycenterUrl: must("PAYCENTER_URL"),
    clientId: must("PAYCENTER_CLIENTID"),
    token: must("PAYCENTER_TOKEN"),
    hmacSecret: must("HMAC_SECRET"),
  });

  if (!bridge) {
    return new Response(body, {
      status,
      headers: { "content-type": "application/json" },
    });
  }

  const jsonStr = typeof body === "string" ? body : JSON.stringify(body);
  const html = `<!doctype html>
<html><head><meta charset="utf-8"><title>Return</title></head>
<body>
<script>
  (function(){
    try{
      var payload = ${JSON.stringify(jsonStr)};
      var data; try{ data = JSON.parse(payload); }catch(e){ data = { raw: payload }; }
      window.parent.postMessage(
        { source: "paycenter", status: ${status}, clientRef: ${JSON.stringify(clientRef)}, reqid: ${JSON.stringify(reqid)}, data },
        location.origin
      );
    }catch(e){}
  })();
</script>
</body></html>`;
  return new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
}

## Page Implement

'Use Client'

# type paycenterMassage

type PaycenterMessage = {
  source: 'paycenter';
  status: number;
  clientRef: string;
  reqid: string;
  data: any;
};


# States

const [iframeUrl, setIframeUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [paymentResult, setPaymentResult] = useState<PaycenterMessage | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);

# Setup Bridge return

const bridgeReturnUrl = useMemo(() => {
    if (typeof window === 'undefined') return '';
    return `${window.location.origin}/api/paycenter/return?bridge=1`;
  }, []);

# Listen for return bridge → save in state

useEffect(() => {
    function onMessage(event: MessageEvent) {
      if (typeof window === 'undefined') return;
      if (event.origin !== window.location.origin) return;
      if (event.data?.source !== 'paycenter') return;
      setPaymentResult(event.data as PaycenterMessage); // <-- save first
      setIframeUrl(null); // close overlay
    }
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, []);

  // After state updated, alert it
  useEffect(() => {
    if (!paymentResult) return;
    alert(
      `Payment result:
status=${paymentResult.status}
clientRef=${paymentResult.clientRef}
reqid=${paymentResult.reqid}
payload=${JSON.stringify(paymentResult.data, null, 2)}`
    );
  }, [paymentResult]);

# Send Init Req with payload

async function sendInitRequest() {
    setLoading(true);
    try {
      const payload = {
        version: '1.5',
        msgId: crypto.randomUUID(),
        operation: 'PAYMENT_INIT',
        requestDate: new Date().toISOString(),
        validateOnly: false,
        requestData: {
          clientId: 'xxxxxxxx',
          transactionType: 'PURCHASE',
          transactionAmount: {
            totalAmount: 0,
            paymentAmount: 200,
            serviceFeeAmount: 0,
            currency: 'LKR',
          },
          redirect: {
            returnUrl: bridgeReturnUrl,   // bridge posts JSON to parent
            cancelUrl: bridgeReturnUrl,   // optional
            returnMethod: 'GET',
          },
          clientRef: `ORDER-${crypto.randomUUID()}`,
          comment: `Test ${new Date().toISOString()}`,
          tokenize: false,
          useReliability: true,
          extraData: { st_id: '123456', batch_id: '102348748', group: '1231458' },
        },
      };

      const res = await fetch('/api/paycenter', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(payload),
      });

      const text = await res.text();
      let data: any;
      try { data = JSON.parse(text); } catch { data = { raw: text }; }

      const url =
        data?.responseData?.paymentPageUrl ||
        data?.paymentPageUrl ||
        data?.redirectUrl;

      if (!url) {
        alert('No payment page URL received.');
        return;
      }
      setIframeUrl(url);
    } catch (e: any) {
      console.error(e);
      alert(`Init failed: ${e?.message || 'Unknown error'}`);
    } finally {
      setLoading(false);
    }
  }

## retun UI Sample

<main style={{ minHeight: '100vh', padding: 20, display: 'flex', flexDirection: 'column', gap: 16, background: '#f9f9f9', color: '#333', fontFamily: 'Arial, sans-serif' }}>
      <h1>Paycenter API Test (Save then Alert)</h1>

      <button onClick={sendInitRequest} disabled={loading} style={{ alignSelf: 'flex-start', padding: '10px 20px', fontSize: 16 }}>
        {loading ? 'Sending…' : 'Send Init Request'}
      </button>

      {paymentResult && (
        <pre style={{ background: '#fff', padding: 12, borderRadius: 8, maxWidth: 900, overflow: 'auto' }}>
          {JSON.stringify(paymentResult, null, 2)}
        </pre>
      )}

      {iframeUrl && (
        <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 9999 }}>
          <div style={{ position: 'relative', width: '90%', height: '90%' }}>
            <button onClick={() => setIframeUrl(null)} style={{ position: 'absolute', top: 10, right: 10, background: '#fff', border: 'none', fontSize: 24, borderRadius: '50%', padding: '4px 10px', zIndex: 10000 }}>
              &times;
            </button>
            <iframe ref={iframeRef} src={iframeUrl} style={{ width: '100%', height: '100%', border: 'none', background: '#fff' }} />
          </div>
        </div>
      )}
    </main>


## Authors

- [@rusoft](https://www.github.com/ruma-lk)


## Used By

This project is for Paycorp Iframe:

-bancstac


## Tech Advice

Above content for NextJS Example use you can use as Normal in Express:

// server.js
import 'dotenv/config';
import express from 'express';
import cors from 'cors';


// If using as a dependency in another app, use:
import { createPaycenterRouter } from '@rusoft/paycenter';

const app = express();

// CORS (allow your frontend origin; comma-separate multiple origins in ALLOWED_ORIGINS)
const allowList = (process.env.ALLOWED_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean);
app.use(cors({
  origin: allowList.length ? allowList : true,
  credentials: true,
}));

// (Optional) health check
app.get('/', (_req, res) => res.json({ ok: true }));

// Mount Paycenter router
app.use(
  createPaycenterRouter({
    cors: {
      origin: allowList.length ? allowList : true,
      credentials: true,
    },
    basePath: process.env.BASE_PATH || '/api', // gives /api/paycenter and /api/paycenter/return
  })
);

const port = Number(process.env.PORT || 3001);
app.listen(port, () => {
  console.log(`➜ Paycenter proxy up:  http://localhost:${port}`);
  console.log(`   POST   ${process.env.BASE_PATH || '/api'}/paycenter`);
  console.log(`   GET    ${process.env.BASE_PATH || '/api'}/paycenter/return`);
  console.log(`   GET    /confirm?reqid=...`);
});