Skip to content

Repository files navigation

ipslip

Test Docker License Stars Node

Example Serbian payment slip with NBS IPS QR code
🇬🇧 English 🇷🇸 Srpski

English

🧾 ipslip is an HTTP API for Serbian payment slip PDFs (uplatnice / naloge za prenos) with embedded NBS IPS QR codes.

Endpoint Method Description
/healthcheck GET ✅ Liveness probe — {"status":"ok"}
/generate-pdf POST 📄 Payment slip PDF with NBS IPS QR code

⚠️ Node.js 22 or 23 required (hyper-express / uWebSockets.js). Node 24+ is not supported yet.

🚀 Quick start (local)

git clone https://github.com/CerealKiller97/e-payment-slip.git
cd e-payment-slip
npm install
cp .env.example .env   # edit payee details
npm run start:dev      # http://127.0.0.1:3000

Production:

npm run build && npm start

Verify everything works (server must be running):

npm run smoke

🐳 Docker

cp .env.example .env
docker compose up --build -d
curl http://127.0.0.1:3000/healthcheck

docker run (prebuilt image, default tag 1.0.0):

mkdir -p invoices-local

docker run -d --name ipslip -p 3000:3000 \
  --env-file .env \
  --restart unless-stopped \
  --mount "type=bind,source=$(pwd)/invoices-local,target=/app/invoices" \
  ghcr.io/cerealkiller97/ipslip:1.0.0

Use --mount instead of -v if your project path contains : or spaces.

Compose / run option Purpose
env_file: .env / --env-file .env Load all variables from file
environment: / -e KEY=val Override file values (inline wins)
./invoices-local:/app/invoices 💾 Persist PDF cache on host

📝 Roadmap — invoice templates

Form type Status Notes
Obrazac 1 / Form 1 "1" ⬜ TODO o1 template in src/invoices.ts
Obrazac 2 / Form 2 "2" ⬜ TODO Dedicated o2 template — currently renders form 3
Obrazac 3 / Form 3 "3" ✅ Done o3 template with NBS IPS QR code
  • Form 1 — nalog za uplatu / uplata gotovinom
  • Form 2 — separate layout (not aliased to form 3)
  • Form 3 — nalog za prenos with IPS QR
  • Visual QA against official NBS paper forms
  • Golden-file PDF tests per template

⚙️ Environment variables

Loaded from .env (process.loadEnvFile()) or the process environment. A missing .env does not block startup.

Variable Required Description
PRIMALAC Conditional* Payee name/address — comma-separated lines → payeeName
RACUN_PRIMAOCCA Conditional* Payee account — 18 digits → payeeAccount
POZIV_NA_BROJ No Reference number for NBS IPS QR (RO tag)
RACUN_PLATIOCA No Reserved — not used in PDF template yet
PDF_CACHE_DIR No Cache dir (./invoices-local locally, /app/invoices in Docker)
PDF_CACHE_DEFAULT_EXPIRES_IN No Default cache TTL seconds (default 21600)

*Env or request body must supply both payee fields. Env wins when set.

PRIMALAC=JP EPS BEOGRAD,BALKANSKA 13
RACUN_PRIMAOCCA=845-000000-040484987
POZIV_NA_BROJ=97163220000111111111000
PDF_CACHE_DEFAULT_EXPIRES_IN=21600

📡 API

GET /healthcheck

{ "status": "ok" }

POST /generate-pdf

Field Type Required Description
payerName string Yes Payer (platilac) — commas → line breaks
purposeOfPayment string Yes Payment purpose (svrha plaćanja)
type "1" | "2" | "3" Yes 1 → form 1; 2 / 3 → form 3
amount number Yes Amount in RSD
cyrilic boolean No Cyrillic PDF + lang=sr_RS QR; else Latin
paymentCode number No Šifra plaćanja (default 289)
payeeName string Conditional Omit if PRIMALAC is set
payeeAccount string Conditional Omit if RACUN_PRIMAOCCA is set
expiresIn number No Cache TTL seconds (positive integer)

200application/pdf (invoice.pdf) · 422 validation · 502 NBS QR failure

payerName = who pays · payeeName / PRIMALAC = who receives

📋 Examples

Minimal (payee from .env):

curl -X POST http://127.0.0.1:3000/generate-pdf \
  -H "Content-Type: application/json" \
  -d '{
    "payerName": "Bogdan Bogdanović, Krunska 23, 11000 Beograd",
    "purposeOfPayment": "Radionica",
    "type": "3",
    "amount": 5000,
    "cyrilic": true
  }' \
  --output invoice.pdf

Full (payee in body):

curl -X POST http://127.0.0.1:3000/generate-pdf \
  -H "Content-Type: application/json" \
  -d '{
    "payerName": "MRĐO MAČKATOVIĆ,ŽUPSKA 13,BEOGRAD 6",
    "payeeName": "JP EPS BEOGRAD,BALKANSKA 13",
    "payeeAccount": "845-000000-040484987",
    "purposeOfPayment": "UPLATA PO RAČUNU ZA EL. ENERGIJU",
    "type": "3",
    "amount": 3596.13,
    "paymentCode": 289,
    "cyrilic": false
  }' \
  --output invoice.pdf

JavaScript:

const response = await fetch('http://127.0.0.1:3000/generate-pdf', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    payerName: 'Bogdan Bogdanović, Krunska 23, 11000 Beograd',
    purposeOfPayment: 'Radionica',
    type: '3',
    amount: 5000,
    cyrilic: true,
  }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
const pdf = Buffer.from(await response.arrayBuffer());

💾 PDF caching

Identical requests hit disk cache ({hash}.pdf + {hash}.json) — no repeat NBS call.

Hash includes: payerName, purposeOfPayment, type, amount, cyrilic, paymentCode, payeeName, payeeAccount, POZIV_NA_BROJ.

TTL: expiresInPDF_CACHE_DEFAULT_EXPIRES_IN21600 s default.

📱 NBS IPS QR

POST https://nbs.rs/QRcode/api/qr/v1/generate/150 · 150×150 px · see src/services/qr-code.ts

🔧 Development

Script Description
npm run start:dev Dev server (nodemon + TypeScript)
npm run build Compile to build/src/
npm start Run production build
npm test Unit tests (72)
npm run smoke Smoke-test README examples
npm run lint / lint:fix oxlint
npm run format / format:check oxfmt

Built with hyper-express, VineJS, pdfmake. Docker image: node:22-bookworm-slim.

📄 License

Apache License 2.0 © Stefan Bogdanović — see LICENSE


Srpski

🧾 ipslip je HTTP API za generisanje PDF uplatnica / naloga za prenos sa ugrađenim NBS IPS QR kodovima.

Endpoint Metoda Opis
/healthcheck GET ✅ Provera rada — {"status":"ok"}
/generate-pdf POST 📄 PDF uplatnica sa NBS IPS QR kodom

⚠️ Potreban je Node.js 22 ili 23 (hyper-express / uWebSockets.js). Node 24+ još nije podržan.

🚀 Brzi start (lokalno)

git clone https://github.com/CerealKiller97/e-payment-slip.git
cd e-payment-slip
npm install
cp .env.example .env   # podesite podatke primaoca
npm run start:dev      # http://127.0.0.1:3000

Produkcija:

npm run build && npm start

Provera (server mora da radi):

npm run smoke

🐳 Docker

cp .env.example .env
docker compose up --build -d
curl http://127.0.0.1:3000/healthcheck

docker run (gotov image, podrazumevani tag 1.0.0):

mkdir -p invoices-local

docker run -d --name ipslip -p 3000:3000 \
  --env-file .env \
  --restart unless-stopped \
  --mount "type=bind,source=$(pwd)/invoices-local,target=/app/invoices" \
  ghcr.io/cerealkiller97/ipslip:1.0.0

Koristite --mount umesto -v ako putanja projekta sadrži : ili razmake.

Compose / run opcija Svrha
env_file: .env / --env-file .env Učitava promenljive iz fajla
environment: / -e KEY=val Prepisuje fajl (inline ima prednost)
./invoices-local:/app/invoices 💾 Perzistentan keš PDF-ova

📝 Plan — šabloni uplatnica

Obrazac type Status Napomena
Obrazac 1 "1" ✅ Gotovo o1 šablon u src/invoices.ts
Obrazac 2 "2" ⬜ TODO Poseban o2 šablon — trenutno renderuje obrazac 3
Obrazac 3 "3" ✅ Gotovo o3 šablon sa NBS IPS QR kodom
  • Obrazac 1 — nalog za uplatu / uplata gotovinom
  • Obrazac 2 — poseban layout (nije alias za obrazac 3)
  • Obrazac 3 — nalog za prenos sa IPS QR kodom
  • Vizuelna provera u odnosu na zvanične NBS obrasce
  • Golden-file PDF testovi po šablonu

⚙️ Promenljive okruženja

Učitavaju se iz .env (process.loadEnvFile()) ili okruženja procesa. Nedostajući .env ne sprečava pokretanje.

Promenljiva Obavezna Opis
PRIMALAC Uslovno* Ime/adresa primaoca — zarezima odvojeni redovi → payeeName
RACUN_PRIMAOCCA Uslovno* Račun primaoca — 18 cifara → payeeAccount
POZIV_NA_BROJ Ne Poziv na broj za NBS IPS QR (RO tag)
RACUN_PLATIOCA Ne Rezervisano — još se ne koristi u šablonu
PDF_CACHE_DIR Ne Folder keša (./invoices-local lokalno, /app/invoices u Dockeru)
PDF_CACHE_DEFAULT_EXPIRES_IN Ne Podrazumevani TTL keša u sekundama (21600)

*Env ili telo zahteva mora da sadrži oba polja primaoca. Env ima prednost.

PRIMALAC=JP EPS BEOGRAD,BALKANSKA 13
RACUN_PRIMAOCCA=845-000000-040484987
POZIV_NA_BROJ=97163220000111111111000
PDF_CACHE_DEFAULT_EXPIRES_IN=21600

📡 API

GET /healthcheck

{ "status": "ok" }

POST /generate-pdf

Polje Tip Obavezno Opis
payerName string Da Platilac — zarezi postaju prelomi reda
purposeOfPayment string Da Svrha plaćanja
type "1" | "2" | "3" Da 1 → obrazac 1; 2 / 3 → obrazac 3
amount number Da Iznos u RSD
cyrilic boolean Ne Ćirilica na PDF-u + lang=sr_RS QR; inače latinica
paymentCode number Ne Šifra plaćanja (podrazumevano 289)
payeeName string Uslovno Izostaviti ako je PRIMALAC u env-u
payeeAccount string Uslovno Izostaviti ako je RACUN_PRIMAOCCA u env-u
expiresIn number Ne TTL keša u sekundama (pozitivan ceo broj)

200application/pdf (invoice.pdf) · 422 validacija · 502 NBS QR greška

payerName = platilac · payeeName / PRIMALAC = primalac

📋 Primeri

Minimalan (primalac iz .env):

curl -X POST http://127.0.0.1:3000/generate-pdf \
  -H "Content-Type: application/json" \
  -d '{
    "payerName": "Bogdan Bogdanović, Krunska 23, 11000 Beograd",
    "purposeOfPayment": "Radionica",
    "type": "3",
    "amount": 5000,
    "cyrilic": true
  }' \
  --output uplatnica.pdf

Pun (primalac u telu):

curl -X POST http://127.0.0.1:3000/generate-pdf \
  -H "Content-Type: application/json" \
  -d '{
    "payerName": "MRĐO MAČKATOVIĆ,ŽUPSKA 13,BEOGRAD 6",
    "payeeName": "JP EPS BEOGRAD,BALKANSKA 13",
    "payeeAccount": "845-000000-040484987",
    "purposeOfPayment": "UPLATA PO RAČUNU ZA EL. ENERGIJU",
    "type": "3",
    "amount": 3596.13,
    "paymentCode": 289,
    "cyrilic": false
  }' \
  --output uplatnica.pdf

JavaScript:

const response = await fetch('http://127.0.0.1:3000/generate-pdf', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    payerName: 'Bogdan Bogdanović, Krunska 23, 11000 Beograd',
    purposeOfPayment: 'Radionica',
    type: '3',
    amount: 5000,
    cyrilic: true,
  }),
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));
const pdf = Buffer.from(await response.arrayBuffer());

💾 Keširanje PDF-a

Identični zahtevi koriste keš na disku ({hash}.pdf + {hash}.json) — bez ponovnog NBS poziva.

Hash uključuje: payerName, purposeOfPayment, type, amount, cyrilic, paymentCode, payeeName, payeeAccount, POZIV_NA_BROJ.

TTL: expiresInPDF_CACHE_DEFAULT_EXPIRES_IN → podrazumevano 21600 s.

📱 NBS IPS QR

POST https://nbs.rs/QRcode/api/qr/v1/generate/150 · 150×150 px · vidi src/services/qr-code.ts

🔧 Razvoj

Skripta Opis
npm run start:dev Dev server (nodemon + TypeScript)
npm run build Kompajliranje u build/src/
npm start Pokretanje produkcionog build-a
npm test Unit testovi (72)
npm run smoke Smoke-test README primera
npm run lint / lint:fix oxlint
npm run format / format:check oxfmt

Tehnologije: hyper-express, VineJS, pdfmake. Docker image: node:22-bookworm-slim.

📄 Licenca

Apache License 2.0 © Stefan Bogdanović — pogledajte LICENSE

About

⚡️PDF generator for Serbian Payment Slip

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages