• Модуль интеграций
  • Сообщество
  • Блог
Документация
Плагины и интеграцииВсе расширения для Medusa от сообществаСтартерыЗапускайте проекты быстрее с готовыми решениями
ЭкспертыПодберите специалиста для разработки и развития вашего проекта на MedusaКейсыПосмотрите примеры Medusa в продакшене и успешные внедрения
Т-Касса от Т-Банка для Medusa: приём платежей для РФ

16 октября 2026 г. · Продукт

Gorgo снижает затраты на адаптацию Medusa к локальным рынкам.

Мы разрабатываем плагины интеграции, осуществляем поддержку и развиваем сообщество разработчиков на Medusa в Telegram.

  • Ресурсы Medusa
  • Плагины и интеграции
  • Модуль интеграций
  • Стартеры
  • Эксперты
  • Кейсы
  • Medusa Чат в Telegram
  • Medusa Новости в Telegram
  • Документация Gorgo
  • Связаться с нами
  • TelegramGitHub
Плагины
M

Manual transfer

Manual bank transfer with payment-proof upload and admin verification for Medusa v2 — by Haus Kayu

npm install medusa-payment-manual-transfer
Категория
Платежи
Создано
Hauskayu
Версия
0.1.0
Последнее обновление
1 день назад
Ежемесячные загрузки
Загрузка данных
Звезды на Github
1
npmNPMGitHubGithub

PG Medusa Transfer Manual by Haus Kayu

Manual bank transfer with payment-proof upload and admin verification for Medusa v2.

Terima transfer bank dengan upload bukti dan verifikasi di Admin Medusa. Dibuat dan dipakai sendiri oleh Haus Kayu.

Status: 0.1.0, ready for a first release; tested on Medusa 2.19 and 2.22. Coming soon to hauskayu.com.

Payment page (storefront)Verification in the Admin

English · Bahasa Indonesia


English

What it does

  • Bank accounts managed in the Admin (Settings → Manual transfer): bank name, account holder, account number, SWIFT/BIC, bank address, currency, regions, active/inactive, order. Several accounts can be active; the first active one matching the cart's currency and region is used.

  • Placing the order authorizes the payment: Medusa creates the order and reserves stock, the payment waits for the buyer's transfer ().

  • Unique payment reference per order, e.g. , guaranteed unique by a database index, with a check character that catches typos.

  • Payment history on the order page (Admin widget), including a warning when the account chosen at checkout was deactivated before the order was placed and another account was given to the buyer.

  • Payment deadline in business days (default 3), counted in your time zone and skipping your holidays.

  • Capture only through admin confirmation: Medusa's own "Capture" button (Payments section of the order) is not used for bank transfers and is refused. An admin confirms the transfer (amount received, date, bank reference) after checking the bank statement, and only then is the payment captured.

  • The account details are copied into each order when it is placed, so later edits only apply to new orders.

  • Payment-proof upload: JPG, PNG or PDF up to 5 MB, recognised by the file's content (not its name). Logged-in customers upload from their order; guests use a secret upload token. Proofs are stored privately in the database and admins open them through 5-minute signed links. After a rejection the buyer can upload again. A file already sent for another order is flagged. Uploads are rate limited per IP and per order.

  • Verification queue (sidebar → Manual transfers): tabs per status (needs verification, awaiting payment, underpaid, rejected, paid, expired, canceled, all) with counts, search by reference, e-mail or .

  • Actions on the order page (widget): Confirm paid (amount received, date, bank reference, note; a red warning and an explicit "the store absorbs the difference" tick when less than the amount due arrived), Underpaid (records a partial transfer and shows what is left to pay; the buyer can upload a proof for the rest) and Reject for a pending proof (reason required, shown to the buyer). Every action is in the payment history with the admin's name, time and note. Each form carries the warning check the bank statement, never rely on the proof alone.

  • Statuses: awaiting payment, awaiting verification, paid, underpaid, rejected, expired (and canceled).

  • Reminder 24 hours before the deadline (once per payment) and auto-cancel when the deadline passes: the order is canceled with Medusa's own workflow, so the stock reservation is released. Payments with a proof waiting for review or with part of the money received are left for an admin. Both run every 15 minutes in the worker.

  • Proof retention (optional): delete proof files some days after the order was paid or canceled, keeping the records.

  • Late badge and admin digest: a proof waiting for verification more than 1 business day, or an underpayment without follow-up for more than 3 business days, shows a red "Late" badge in the queue (per row and per tab) and is listed once in a digest e-mail to the admins. Both thresholds are options.

  • E-mails for every status change, to the buyer and to the admins, through Medusa's Notification Module (any e-mail provider): order placed with the payment instructions and the pro-forma invoice attached, proof received, proof rejected (with the reason), underpaid (what is left), paid, reminder, expired, canceled. Default templates in English and Indonesian; replace any of them with your own HTML files.

  • Pro-forma invoice PDF: store name, logo and address, order number, reference, items and totals, bank details and deadline. Attached to the order e-mail, downloadable by the buyer and from the order page; the layout can be replaced. Set in an embedded Noto Sans (open license, Latin subset), so names like "José Müller-Ñúñez" keep their accents; uses your store's font instead.

  • The e-mails and the invoice carry your store's identity only (Settings → Manual transfer → Store details).

  • Storefront pages for the Medusa DTC Starter and the Medusa Next.js Starter (in the npm package): payment instructions with copy buttons, exact amount, reference, countdown, the "fees: OUR" note and the invoice download, plus the proof upload with its status. English and Indonesian. A starter-independent core (React + Fetch) makes other storefronts easy. See Storefront.

Install

:

Then run , enable the provider on your regions, and add your bank account in Settings → Manual transfer. For e-mails, configure Medusa's Notification Module with a provider on the channel (see below).

E-mails and the pro-forma invoice

The plugin sends through Medusa's Notification Module, so any provider on the channel works (SendGrid, Resend, SES, SMTP plugins, …). Without a Notification Module nothing is sent and nothing fails.

ChangeBuyer e-mail (template)Admin e-mail
Order placed: instructions, exact amount, reference, deadline, invoice PDF attached
Proof uploaded, with the bank-statement warning
Proof rejected, with the reason and the instructions
Underpaid, with what is left
Paid
Deadline near–
Expired
Canceled
Waiting too long for an admin– (one digest)

Every notification has rendered by the plugin, (or your ), all values in , and an idempotency key, so a retried event never sends the same e-mail twice. Attachments are base64.

Store identity. The e-mails and the invoice show the store's name (Settings → Store) and the store metadata , , , and . Edit them in Settings → Manual transfer → Store details; the Preview menu there opens every e-mail and the invoice rendered with a made-up order.

Your own templates. Put HTML files in : or (names in the table above, plus with for the frame). The subject is the file's . A file containing is sent as it is, without the layout. Syntax: (escaped), (raw), (if / for each), (if not). Values: , , , , , , , , , , , , , , , and (the default wording).

Admin recipients. Set to the addresses that should get the admin e-mails (new order, proof uploaded, paid, digest, …). Without it they go to every Admin user: the plugin logs a warning once, and Settings → Manual transfer → E-mail notifications lists who receives them right now, plus the the buyer e-mails link to.

Invoice font. The default layout embeds Noto Sans (SIL Open Font License, subset to the Latin scripts, see ), so accented names and addresses come out intact; only the glyphs used end up in the PDF. For your brand font, or for other scripts, set to a TTF/OTF file (relative to the app's working directory) or ; bold falls back to regular, mono to the bundled one. An unreadable file logs a warning and the bundled fonts are used. Characters a font has no glyph for lose their accent or become "?".

Your own invoice. is a function, or a module path whose default export is one: with the labels, store, items, totals, account and the loaded order in and pdf-lib in . embeds the same fonts as the default layout.

Testing without a real provider. The package includes a notification provider that writes every e-mail to a folder (, , and the attachments) instead of sending it. For development only:

Storefront

The npm package holds the buyer's pages for both Medusa starters, in English and Indonesian:

StarterInstall
Medusa DTC Starter (current)
Medusa Next.js Starter (deprecated upstream)

Each installer copies the core ( of the package: React components, Fetch API helpers, labels, no dependency on a starter) to , adds the example's thin, starter-specific layer (page, two route handlers, the place-order button) and makes four small, checked edits (payment method name, place-order button, order-page link, no "paid at" for unpaid transfers). Placing an order leads to : status, exact amount, bank details and reference with copy buttons, deadline with countdown, the "choose OUR" note, the invoice download and the proof upload. Guests get there with the upload token, logged-in customers with their session. Set to so the e-mails link to the same page.

Another storefront? Use the core directly: src/storefront/README.md. Details per starter: examples/dtc-starter/README.md, examples/nextjs-starter/README.md.

The buyer's IP behind a storefront

Uploads, status reads and token exchanges are limited per IP. Behind a storefront every request comes from the storefront's server, so the examples send the buyer's IP in together with a shared secret in . The backend uses that header only when the secret matches; otherwise it counts the IP of the connection. is never read: Medusa sets Express' to 1, so anyone reaching the backend directly could pick their own IP with it.

(or the plugin option ). The storefront takes the buyer's IP from , then the first entry, so the proxy in front of the storefront must set them (Vercel does; nginx: ). Without the secret all storefront traffic shares one IP budget: raise then. A reverse proxy in front of Medusa can send the two headers itself, too.

Signing secret (production)

Guest upload tokens and the signed proof links are HMACs. In production set a secret of its own, so rotating the JWT secret does not break the links in e-mails already sent:

The plugin option wins over , which wins over (the fallback, fine for development). Changing the secret invalidates every upload token and proof link issued before.

Locks (server + worker)

Uploads, rejections, confirmations, underpayments, reminders and the expiry job lock the payment they change. The plugin uses Medusa's Locking Module from the container, which Medusa loads by default. Its default provider is in-memory, which only protects one process: when you run a separate server and worker (or several instances), use a shared provider, e.g.

( works too.) Only if an app disabled the Locking Module altogether does the plugin fall back to a lock kept in its own process, and it logs a warning once.

Proof retention

Proofs are kept forever by default. With , a daily job (03:30) deletes the file contents of proofs whose payment was captured (paid) or canceled more than N days ago. The proof record stays: file name, size, SHA-256, who uploaded it, review and history, so duplicate detection keeps working. The Admin shows "file deleted" instead of the View button, and old signed links answer .

Admin API

MethodPath
GET / POSTlist / create accounts
GET / POST / DELETEread / update / delete an account
GETthe order's manual-transfer payment, history and proofs
GETthe pro-forma invoice PDF ()
GETthe verification queue: , with and per status, per row
GETan e-mail (template name) or , rendered with a made-up order
POSTconfirm a transfer and capture the payment
POSTrecord a partial transfer (status )
GET for the Admin pages
GETa signed link to view a proof, valid 5 minutes
POSTreject a pending proof, required

takes . Partial transfers recorded with count towards the amount due; it is refused when less than that arrived in total, unless is . takes the same fields without and is refused when the transfer covers the whole amount (confirm instead).

Always check the bank statement in your banking app before confirming. Never rely on a proof image alone.

Store API (payment proofs)

All need the publishable API key. The buyer is either the logged-in customer who placed the order (session or bearer) or anyone holding the order's upload token ( header, or ).

MethodPath
GETinstructions (reference, amount, account, deadline), status, / , , proofs
GETthe pro-forma invoice PDF ()
POSTmultipart: (JPG/PNG/PDF ≤ 5 MB), optional
GETright after checkout (24 h): trade the completed cart id for

Anyone else gets . Too many requests get with . Upload tokens are HMAC-derived from the order id, so the e-mails can always rebuild the link ( in ); they only work while the payment accepts proofs.

The per-IP limit is kept in memory per server process; the per-order limit is counted in the database. Which IP counts: see The buyer's IP behind a storefront. Run several server instances? Use a shared locking provider (e.g. ), since proof uploads, rejections and confirmations lock the payment.

Payment data

(and the payment session's data) holds , (the copied account details), , , (, , , , , , ), , , , (partial transfers), , and, once confirmed, . The provider rebuilds it from these keys at every step, so data a storefront sends when creating the session is ignored.

Events for notifications: and (), and ( of the payment), (), () and (). The plugin's own subscriber turns them (and / ) into the e-mails above.

Development

holds the E2E suite, the test app and the storefront run (: both examples installed in pinned starter checkouts, checkout to "paid" in Chromium): see integration-tests/README.md and CONTRIBUTING.md. renders every e-mail and the invoice with a made-up order, without Medusa. Changes: CHANGELOG.md. Security reports: SECURITY.md. More screenshots: docs/screenshots.

Roadmap

  • Storage adapter for proofs (not built yet): proofs live in the database today, which is private by construction and needs no setup. A pluggable adapter for a private bucket (S3, Cloudflare R2, …) with short-lived signed URLs is planned for stores with many or large proofs. Medusa's File Module is deliberately not used: with , "private" files land under the public folder.

Bahasa Indonesia

Fitur

  • Rekening bank dikelola dari Admin (Settings → Manual transfer): nama bank, nama penerima, nomor rekening, SWIFT/BIC, alamat bank, mata uang, region, aktif/nonaktif, urutan. Rekening aktif pertama yang cocok dengan mata uang dan region keranjang yang dipakai.

  • Order dibuat saat checkout: pembayaran di-authorize sehingga order tercipta dan stok ditahan, status menunggu pembayaran ().

  • Kode referensi unik per order, mis. , dijamin unik oleh index database, dengan karakter cek untuk menangkap salah ketik.

  • Riwayat pembayaran di halaman order (widget Admin), termasuk peringatan kalau rekening pilihan saat checkout dinonaktifkan sebelum order dibuat dan pembeli diberi rekening lain.

  • Batas waktu pembayaran dalam hari kerja (default 3), dihitung di zona waktu toko dan melewati hari libur.

  • Capture hanya lewat konfirmasi admin: tombol "Capture" bawaan Medusa ditolak. Admin mengonfirmasi transfer (nominal diterima, tanggal, ref bank) setelah mengecek mutasi, baru pembayaran di-capture.

  • Detail rekening disalin ke setiap order, jadi perubahan rekening hanya berlaku untuk order baru.

  • Upload bukti transfer: JPG, PNG, atau PDF maksimal 5 MB, dicek dari isi file (bukan namanya). Pelanggan yang login mengunggah dari halaman order; tamu memakai token rahasia. Bukti disimpan privat di database dan admin melihatnya lewat link bertanda tangan yang berlaku 5 menit. Setelah ditolak, pembeli bisa upload ulang. File yang sama yang pernah dikirim untuk order lain ditandai. Upload dibatasi per IP dan per order.

  • Antrean verifikasi (sidebar → Transfer manual): tab per status (perlu verifikasi, menunggu pembayaran, kurang bayar, ditolak, lunas, kedaluwarsa, dibatalkan, semua) dengan jumlahnya, cari lewat referensi, email, atau .

  • Aksi di halaman order (widget): Konfirmasi lunas (nominal diterima, tanggal, ref bank, catatan; peringatan merah dan centang "toko menanggung selisih" kalau kurang dari tagihan), Kurang bayar (mencatat transfer sebagian dan menghitung sisa; pembeli bisa upload bukti untuk sisanya), dan Tolak bukti (alasan wajib, dilihat pembeli). Semua aksi tercatat di riwayat dengan nama admin, waktu, dan catatan, dan setiap form membawa peringatan cek mutasi di aplikasi bank, jangan hanya percaya bukti.

  • Tombol "Capture" bawaan Medusa tidak dipakai untuk transfer bank dan akan ditolak; capture terjadi lewat Konfirmasi lunas.

  • Pengingat 24 jam sebelum batas waktu dan auto-cancel saat kedaluwarsa: order dibatalkan dengan workflow Medusa sehingga stok kembali. Pembayaran yang buktinya menunggu dicek atau yang sudah dibayar sebagian dibiarkan untuk admin. Keduanya jalan tiap 15 menit di worker.

  • Masa simpan bukti (opsional, ): isi file bukti dihapus N hari setelah order lunas atau dibatalkan; data buktinya (nama, ukuran, hash, riwayat) tetap disimpan. Default = simpan selamanya.

  • Badge "Terlambat" dan email ringkasan ke admin: bukti yang menunggu verifikasi lebih dari 1 hari kerja, atau kurang bayar tanpa tindak lanjut lebih dari 3 hari kerja, diberi badge merah di antrean (per baris dan per tab) dan dikirim sekali dalam email ringkasan ke admin. Ambangnya bisa diatur ().

  • Email untuk setiap perubahan status, ke pembeli dan admin, lewat Notification Module Medusa (provider email apa pun): order dibuat (instruksi + invoice PDF terlampir), bukti diterima, bukti ditolak (dengan alasan), kurang bayar (sisa tagihan), lunas, pengingat, kedaluwarsa, dibatalkan. Template default bahasa Inggris dan Indonesia (, atau order), bisa diganti dengan file HTML sendiri ().

  • Invoice PDF pro-forma: nama, logo, alamat toko, nomor order, referensi, barang dan total, detail rekening, batas waktu. Terlampir di email order, bisa diunduh pembeli dan dari halaman order; layout-nya bisa diganti (). Font Noto Sans (lisensi terbuka, subset Latin) disematkan sehingga nama seperti "José Müller-Ñúñez" tetap utuh; untuk memakai font toko sendiri.

  • Penerima email admin: isi (disarankan). Kalau kosong, semua user Admin menerimanya; plugin menulis peringatan di log dan Settings → Manual transfer → Notifikasi email menampilkan siapa saja penerimanya saat ini serta yang dipakai di email pembeli.

  • Halaman storefront untuk Medusa DTC Starter dan Medusa Next.js Starter (ikut paket npm): halaman instruksi (detail rekening dengan tombol salin, nominal persis, kode referensi, hitung mundur, catatan biaya OUR, unduh invoice) dan upload bukti dengan statusnya, teks EN + ID. Intinya (: komponen React, fungsi API, label) tidak bergantung starter. Pasang dengan (atau ), lalu isi dengan . Detail: examples/dtc-starter/README.md.

  • Email dan invoice hanya memakai identitas toko Anda (Settings → Manual transfer → Detail toko). Menu Pratinjau di sana membuka setiap email dan invoice dengan order contoh.

  • Tes email tanpa provider asli: provider menulis setiap email ke folder (HTML, teks, JSON, lampiran) alih-alih mengirimnya. Hanya untuk development.

Secret produksi: buat dengan lalu isi di . Tanpa itu plugin memakai (cukup untuk development). Mengganti secret membatalkan semua token upload dan link bukti yang sudah terkirim.

IP pembeli di balik storefront: batas per IP memakai header dari storefront hanya kalau cocok dengan (atau opsi ); selain itu IP koneksi. tidak pernah dibaca. Isi secret yang sama di server storefront (jangan ), dan pastikan proxy di depan storefront mengisi .

Lock: plugin memakai Locking Module Medusa dari container. Provider default-nya in-memory (hanya satu proses); kalau server dan worker dipisah, pakai atau (contoh di bagian English). Hanya kalau Locking Module dimatikan, plugin memakai lock di prosesnya sendiri dan menulis peringatan.

Roadmap: adapter penyimpanan bukti ke bucket privat (S3, Cloudflare R2, …) dengan signed URL, belum dikerjakan.

Konfigurasi lengkap ada di bagian English di atas.

Selalu cek mutasi di aplikasi bank sebelum konfirmasi, jangan hanya percaya bukti transfer.


Kenapa kami membuatnya

Haus Kayu menjual perkakas dan furnitur jati dari Jepara ke pembeli di AS. Untuk custom furniture dan order besar, pembeli lebih nyaman transfer langsung ke rekening USD kami daripada membayar dengan kartu.

Plugin lain dari Haus Kayu

  • PG Medusa Doit.id: github.com/haustex/pg-medusa-doit, npm

About Haus Kayu

Haus Kayu: teak tools and furniture from Jepara, Indonesia.

Support

  • E-mail: dev@hauskayu.com
  • WhatsApp: chat with us

License

MIT © Haus Kayu

Еще в этой категории

Посмотреть все
Платежи
Braintree logo

Braintree

От Lambda Curry

Поддержка платежей и 3D Secure через Braintree

Загрузка данных
GitHubnpm
Платежи
Pay. logo

Pay.

От Webbers

Принимайте кредитные карты, цифровые платежи и купи сейчас, плати потом

Загрузка данных
GitHubnpm
Платежи
Mollie logo

Mollie

От Variable Vic

Легко принимайте мультивалютные платежи через Mollie

Загрузка данных
GitHubnpm

Еще от этого автора

Посмотреть все
Платежи
D

Doit

От Hauskayu

doit.id payment gateway (QRIS, Virtual Account, bank transfer) for Medusa v2 — by Haus Kayu

Загрузка данных
GitHubnpm
npm install medusa-payment-manual-transfer
1module.exports = defineConfig({
2 // …
3 plugins: [
4 {
5 resolve: "medusa-payment-manual-transfer",
6 options: {
7 // secret: "…", // signs guest upload tokens and proof links; default MANUAL_TRANSFER_SECRET, then http.jwtSecret
8 // trustedProxySecret: "…", // the storefront sends the buyer's IP with it; default MANUAL_TRANSFER_PROXY_SECRET
9 showCredit: true, // the small plugin credit under the plugin's Admin pages
10 proofRetentionDays: null, // e.g. 365: delete proof files that long after the order was paid or canceled
11 reminderHours: 24, // reminder event this many hours before the deadline
12 autoCancelExpired: true, // cancel orders whose deadline passed without payment
13 adminReminders: {
14 verificationBusinessDays: 1, // "Late" when a proof waits longer than this
15 underpaidBusinessDays: 3, // "Late" when an underpayment has no follow-up for longer than this
16 email: true, // digest e-mail to the admins (the badge is always shown)
17 },
18 notifications: {
19 adminEmails: ["orders@your-store.com"], // recommended; unset = every Admin user (logged, shown in Settings)
20 locale: "en", // buyer e-mails: "en" or "id"; an order's metadata.locale wins
21 // adminLocale: "id",
22 paymentUrl: "https://your-store.com/transfer/{order_id}?token={token}", // the example storefront's page; no link without it
23 adminUrl: "https://your-store.com/app", // links in the admin e-mails
24 // templatesDir: "./emails", // your own templates, see below
25 // templateIds: { order_placed: "d-123" }, // provider-side templates (e.g. SendGrid)
26 // buyer: { payment_reminder: false }, // switch single e-mails off
27 // admin: { paid: false, overdue: true },
28 attachInvoice: true,
29 },
30 invoice: {
31 // note: "Bank fees are paid by the buyer.",
32 // fontPath: "./fonts/Brand.ttf", // your TTF/OTF, or { regular, bold, mono }; default: bundled Noto Sans
33 // render: "./src/invoice.js", // your own PDF layout, see below
34 },
35 proofUploads: {
36 perIp: 10, // upload attempts per IP per window
37 perIpWindowMinutes: 15,
38 readsPerIpPerMinute: 120,
39 perOrderPerDay: 5, // proofs one order may receive in 24 hours
40 tokenExchangesPerIp: 10, // cart id → upload token exchanges per IP per window (own budget)
41 tokenExchangesWindowMinutes: 60,
42 },
43 },
44 },
45 ],
46 modules: [
47 {
48 resolve: "@medusajs/medusa/payment",
49 options: {
50 providers: [
51 {
52 resolve: "medusa-payment-manual-transfer/providers/manual-transfer",
53 id: "manual-transfer",
54 options: {
55 referencePrefix: "MT", // 1–6 of A–Z/0–9
56 deadlineBusinessDays: 3, // 1–60
57 timezone: "Asia/Jakarta", // IANA zone business days are counted in (default UTC)
58 holidays: ["2026-12-25"], // YYYY-MM-DD, skipped when counting
59 },
60 },
61 ],
62 },
63 },
64 ],
65})
1{
2 resolve: "@medusajs/medusa/notification",
3 options: {
4 providers: [
5 {
6 resolve: "medusa-payment-manual-transfer/providers/email-preview",
7 id: "email-preview",
8 options: { channels: ["email"], dir: "./email-previews" },
9 },
10 ],
11 },
12},
1openssl rand -hex 32
2# Medusa .env # storefront .env (server-side only, never NEXT_PUBLIC_*)
3MANUAL_TRANSFER_PROXY_SECRET=<value> MANUAL_TRANSFER_PROXY_SECRET=<same value>
1openssl rand -hex 32
2# .env
3MANUAL_TRANSFER_SECRET=<the 64 hex characters>
1modules: [
2 {
3 resolve: "@medusajs/medusa/locking",
4 options: {
5 providers: [
6 { resolve: "@medusajs/medusa/locking-redis", id: "locking-redis", is_default: true, options: { redisUrl: process.env.REDIS_URL } },
7 ],
8 },
9 },
10]