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

medusa-payment-cutluy

v0.2.4

Published

Medusa Payment Module Provider for CutLuy — Bakong KHQR scan-to-pay (asynchronous payment method).

Readme

medusa-payment-cutluy

A Medusa Payment Module Provider for CutLuy — Bakong KHQR scan-to-pay payments.

CutLuy is an asynchronous payment method: the customer scans a KHQR code (or opens a hosted checkout page) and pays from their banking app. The plugin is a standalone Medusa payment provider (like medusa-payment-stripe) and installable in any Medusa v2 application.


Features

  • ✅ Create a CutLuy KHQR payment per payment session
  • ✅ Exposes qr_string and checkout_url to the storefront through the payment session data
  • ✅ Asynchronous authorization (pending_authorization) → order created as "awaiting"
  • ✅ Webhook handling via Medusa's built-in listener with HMAC-SHA256 signature verification (over the raw body) and session/amount verification
  • ✅ Scheduled sweep job that detects CutLuy payments expired/failed while their order is still awaiting (dropped webhook events) and flags them for operators
  • payment.completed webhook marks the session captured and completes the cart/order
  • ✅ Poll-based status (getPaymentStatus) and retrieval (retrievePayment)
  • ✅ USD-only enforcement (CutLuy only charges USD)
  • ❌ Capture / cancel / delete / update / refund are not part of CutLuy's public v1 API — implemented as safe no-ops or explicit errors

How it works

sequenceDiagram
    participant S as Storefront
    participant M as Medusa Backend
    participant C as CutLuy

    S->>M: initiate payment session (provider_id = cutluy)
    M->>C: POST /v1/payments (amount in USD, metadata.session_id)
    C-->>M: payment { id, qr_string, checkout_url }
    M-->>S: payment session data (qr_string / checkout_url)
    S->>S: render QR or redirect to checkout_url
    S->>M: place order → authorizePayment → pending_authorization
    C->>M: webhook payment.completed → /hooks/payment/cutluy_cutluy
    M->>M: verify X-CutLuy-Signature → mark captured → complete cart/order

Because authorizePayment returns pending_authorization, the order is created with an awaiting payment status. When CutLuy fires payment.completed, Medusa's processPaymentWorkflow re-invokes authorizePayment — the provider re-checks the CutLuy payment status and returns captured once it's paid — which creates the Payment record, captures it, and flips the order to paid/captured. If the QR expires or the payment fails, payment.expired / payment.failed are mapped to a FAILED action — which Medusa 2.18's webhook processor ignores, so the order remains awaiting and no capture occurs. The storefront must surface CutLuy's own payment status (poll getPaymentStatus), and operators see a warning log per event.

Async flow requirement: authorizePayment polls GET /v1/payments/:id on every call and only returns pending_authorization while the payment is still pending. This is what lets the webhook-driven autocapture flow create and capture the Payment.


Requirements

  • Medusa v2.17.0 or later (uses the async payment methods support)
  • Node.js 20+
  • A CutLuy account with an API key and a configured webhook endpoint

Testing

A complete end-to-end test harness lives in the medusa-test-app/ directory (sibling of this repo — not part of the package):

  • docker-compose.yml — Postgres + Redis for the Medusa app
  • medusa-app/ — a create-medusa-app backend with the plugin registered
  • cutluy-mock/server.mjs — a mock of the CutLuy API (http://localhost:8080/v1), so no real credentials are needed
  • e2e-test.mjs — drives the full flow via the API: cart → payment session (pp_cutluy_cutluy) → verify qr_string/checkout_url → complete cart (order "awaiting") → simulate customer paying at the mock → deliver a signed payment.completed webhook → assert the order becomes captured

When you later point the provider at the real CutLuy API (CUTLUY_API_URL + real ck_... key), the same script works unchanged.

Dependency & security posture: the published package ships zero runtime dependencies — it only declares the @medusajs/framework peer (>=2.17.0) that any Medusa app already provides. Scanner findings (CVEs, telemetry, minified files) that appear for this package come from Medusa core's own dependency tree (e.g. @medusajs/telemetry, which is opt-out via MEDUSA_TELEMETRY_DISABLED=true), and are identical for every Medusa plugin — they resolve upstream when Medusa updates its dependencies.


1. Install

Install locally for development (yalc)

From this plugin's directory (pnpm is the package manager for this repo):

pnpm install
pnpm medusa plugin:publish   # pushes to the LOCAL yalc registry (dev only — this is not npm)

Then, in your Medusa application:

npx medusa plugin:add medusa-payment-cutluy

While developing, run pnpm medusa plugin:develop in this plugin's directory to watch changes and auto-update the app.

Install from npm

npm install medusa-payment-cutluy

2. Configure

In medusa-config.ts of your Medusa application, register the provider in the Payment Module's providers array:

import { defineConfig } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...other config
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            // provider installed from the local registry or npm
            resolve: "medusa-payment-cutluy/providers/cutluy",
            id: "cutluy",
            options: {
              apiKey: process.env.CUTLUY_API_KEY,
              webhookSecret: process.env.CUTLUY_WEBHOOK_SECRET,
              // apiUrl: "https://cutluy.com/v1",   // optional override
              // timeoutMs: 15000,                  // optional
            },
          },
        ],
      },
    },
  ],
})

Add the environment variables to your application's .env:

# apps/backend/.env
CUTLUY_API_KEY=ck_live_...
CUTLUY_WEBHOOK_SECRET=whsec_...

The provider's identifier is pp_cutluy_cutluy. Enable it in a region from the Medusa Admin (Settings → Regions → Payment Providers).

Options

| Option | Required | Description | | --------------- | :------: | --------------------------------------------------------------------------- | | apiKey | ✅ | CutLuy secret API key (ck_live_... / ck_test_...) | | webhookSecret | ⚠️ | Signing secret used to verify X-CutLuy-Signature. Without it webhooks are rejected. | | apiUrl | | Override the API base URL (default https://cutluy.com/v1) | | timeoutMs | | HTTP request timeout (default 15000) |


3. Configure the webhook in CutLuy

  1. Make sure your CutLuy store has a payment link configured (payment creation returns 404 payment_link_not_found otherwise).

  2. In the CutLuy dashboard, go to Webhooks.

  3. Add an endpoint pointing at Medusa's built-in payment webhook listener:

    https://<your-medusa-backend>/hooks/payment/cutluy_cutluy

    (cutluy is the provider's identifier, repeated for the provider id.)

  4. Copy the endpoint's signing secret into CUTLUY_WEBHOOK_SECRET.

The provider verifies the X-CutLuy-Signature header (HMAC-SHA256 of <t>.<rawBody>) before trusting any event. Medusa's built-in listener acks the request with 200 immediately and processes the event asynchronously (~5s delay, up to 3 internal attempts). Invalid or missing signatures are logged and dropped — the request is still acked, so CutLuy does not retry after a 2xx (non-2xx or timeout responses are retried with exponential backoff, up to 8 times); monitor your backend logs for signature failures. Use the dashboard's Send test or resend a delivery to exercise your endpoint.

Before completing, the provider also verifies the webhook's payment against its payment session: the session must exist, the payment must be USD, and the webhook amount must match the session amount cent-exact. On a mismatch or unknown session the event is logged and ignored (the order stays awaiting). The payload is HMAC-authenticated, so this guards against CutLuy-side drift, not forgery.

Payment sweep job

Because Medusa 2.18's webhook processor ignores payment.expired / payment.failed events, an order whose QR expired or whose payment failed would stay awaiting forever with no signal. The plugin ships a scheduled job, poll-cutluy-payments, that closes the loop:

  • Runs every 15 minutes (node-schedule cron */15 * * * *).
  • Lists the provider's payment sessions older than 15 minutes that are still pending / pending_authorization, polls CutLuy for each, and for payments that are expired or failed at CutLuy:
    • logs a warning with the session id, CutLuy payment id, amount, and currency, and
    • marks the session error (best-effort) so it stops being silently pending.
  • Sessions are only touched once (they leave the pending set), so re-runs are idempotent.

To load the job, the plugin must be listed in the app's plugins array (the provider itself is registered under modules):

plugins: ["medusa-payment-cutluy"],

The staleness window is tunable via CUTLUY_SWEEP_STALE_AFTER_MS (default 900000).


4. Storefront integration

The payment session data contains everything the storefront needs:

{
  "id": "PUETcMUOKStjZsCb6zAl8kg9fMRGM85x",
  "status": "pending",
  "amount": "1.50",
  "currency": "USD",
  "qr_string": "00020101021229...",   // render as a QR code
  "checkout_url": "https://cutluy.com/pay/PUETcMUOKStjZsCb6zAl8kg9fMRGM85x",
  "expires_at": "2026-07-09T12:05:00.000Z"
}

Choose one of:

  • Redirect the customer to checkout_url (hosted, branded page with countdown and live status), or
  • Render qr_string as a QR code in your own UI (e.g. with a qrcode library) and poll the cart/order status.

After the customer pays, the payment.completed webhook completes the order automatically — no storefront polling required.

CutLuy's hosted checkout redirects back to your configured success/failure URLs after a terminal payment, appending ?status=success|failed&payment_id=…&reference_id=…. Since Medusa drops payment.expired / payment.failed webhooks, the failed redirect (or polling getPaymentStatus) is how the storefront learns the order failed — the order itself stays awaiting until the sweep job or an operator acts.


5. Development

pnpm install        # install dependencies
pnpm test           # run unit tests (vitest)
pnpm build          # medusa plugin:build → outputs to .medusa/server
pnpm dev            # watch + push to the local yalc registry for the test app

6. Testing in a full Medusa app (Docker)

This repo ships only the plugin. To test it end-to-end, run a Medusa app in Docker and install the plugin into it. Follow the official guide — Install Medusa with Docker — then:

  1. Clone the DTC Starter repo and set up docker-compose.yml, Dockerfile, start.sh as described in the guide.
  2. Install the plugin locally via yalc (npx medusa plugin:publish here, then npx yalc add medusa-payment-cutluy in the app), or mount this plugin's folder and install it with medusa plugin:add.
  3. Register the provider in apps/backend/medusa-config.ts (see Configure) and add the CUTLUY_* env vars to apps/backend/.env.
  4. docker compose up --build -d, create an admin user, and enable the CutLuy payment provider in a region.
  5. Expose the backend to the internet (e.g. with a tunnel) and set the webhook URL in the CutLuy dashboard.

API mapping

| Medusa provider method | CutLuy API call | | ---------------------------- | ---------------------------------------- | | initiatePayment | POST /v1/payments | | getPaymentStatus | GET /v1/payments/:id | | retrievePayment | GET /v1/payments/:id | | getWebhookActionAndData | webhook events (signature verified) | | capturePayment / cancelPayment / deletePayment / updatePayment | no-op (not in CutLuy v1 API) | | refundPayment | throws — not supported by CutLuy yet |

Status mapping: pending/scanned → pending · paid → captured · expired/failed → error/failed.


License

MIT