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

payload-minimax-adapter

v0.1.0

Published

MiniMax (moj.minimax.rs) invoicing adapter for Voxberg e-commerce

Readme

payload-minimax-adapter

npm version License: MIT

MiniMax invoicing & inventory adapter for PayloadCMS ecommerce applications.

MiniMax is a Serbian cloud accounting platform providing invoicing, customer management, item catalog, stock entries and the e-invoice / SEF (eFaktura) integration. The adapter wraps the REST API at https://moj.minimax.rs/RS/API, handles OAuth2 token rotation, caches FK lookups via the shared payload-payment-shared cache, and exposes a Voxberg-shaped order-to-invoice mapper.

MiniMax is not a payment adapter — it does not implement the PaymentAdapter interface. It's an ERP integration consumed by Voxberg's Orders afterChange hooks to issue invoices for completed orders.

LIVE since 2026-03-12

The full invoice pipeline is in production:

Order created (RSD, non-fiscal) → afterChange hook → processInvoice.ts
  → MiniMax client (auth, FK lookups, customer find/create)
  → createInvoice (draft) → issueAndGeneratePdf
  → getInvoiceAttachments (PDF base64)
  → upload PDF to Vercel Blob
  → email PDF to customer

Pipeline location: apps/voxberg/src/collections/Invoices/processInvoice.ts. API surface fully operational: auth, invoices, items (34 products live in MiniMax), customers, stock, VAT rates, payment methods.

Live tenant info

| Field | Value | | ------------ | ------------------------------------------------ | | Org ID | 88675 (changed from 88898 after paid upgrade) | | Company | BGASS LIMITED DOO BEOGRAD-ČUKARICA | | PIB | 114041084 | | Account type | Paid (API access enabled — confirmed 2026-03-10) |

Current version

0.1.0 (per package.json).

Installation

pnpm add payload-minimax-adapter

Peer dependencies

{
  "payload": "^3.81.0",
  "payload-payment-shared": "workspace:*",
}

payload-payment-shared (commit 0cd6307e) is a hard peer — see Shared payment runtime below.

Shared payment runtime

Despite not being a payment adapter, MiniMax shares the same cross-cutting helpers via payload-payment-shared:

| Symbol from payload-payment-shared | Used here for | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | createCache / DEFAULT_TTL_MS | Backs the FK cache for currencies, VAT rates, document numbering, payment methods. Use deleteCached(key) to invalidate after a write — the old behaviour of setCached(key, null) cached null for an hour and made newly-created MiniMax items disappear from listItems(). (Fixed in commit 81c2857a.) | | consoleLogger / PaymentLogger | Pino-compatible logger interface. Pass req.payload.logger so quota warnings flow through Payload's structured logger; defaults to consoleLogger. |

The local MiniMaxError is kept (it predates the shared package) and carries code + statusCode fields. Shared PaymentError is not used here.

Quick start

Environment variables

# Client podaci - obtained from MiniMax support (zahtev za pomoć)
MINIMAX_CLIENT_ID=your-client-id
MINIMAX_CLIENT_SECRET=your-client-secret

# User podaci - created in Moj Profil > Nova Aplikacija
MINIMAX_USERNAME=your-app-username
MINIMAX_PASSWORD=your-app-password

# Org ID (88675 for the BGASS production tenant)
MINIMAX_ORG_ID=your-organisation-id

# Optional - defaults to https://moj.minimax.rs/RS/API
MINIMAX_API_URL=https://moj.minimax.rs/RS/API

Create the client

import { MiniMaxClient } from 'payload-minimax-adapter'

const client = new MiniMaxClient({
  clientId: process.env.MINIMAX_CLIENT_ID!,
  clientSecret: process.env.MINIMAX_CLIENT_SECRET!,
  username: process.env.MINIMAX_USERNAME!,
  password: process.env.MINIMAX_PASSWORD!,
  orgId: process.env.MINIMAX_ORG_ID!,
  // Optional but strongly recommended in Payload contexts:
  logger: req.payload.logger,
})

Create + issue an invoice

import {
  MiniMaxClient,
  findOrCreateCustomer,
  getCurrencyByCode,
  getVatRateByCode,
  getDocumentNumbering,
  createInvoice,
  issueAndGeneratePdf,
  getInvoiceAttachments,
  sendEInvoice,
} from 'payload-minimax-adapter'

// 1. Resolve FK references (cached for 1h via the shared TTL cache)
const customer = await findOrCreateCustomer(client, {
  name: 'Petar Petrovic',
  email: '[email protected]',
  isB2B: false,
})
const currency = await getCurrencyByCode(client, 'RSD')
const vatRate = await getVatRateByCode(client, 'S') // S = standard 20%
const docNumbering = await getDocumentNumbering(client)

// 2. Create draft invoice
const draft = await createInvoice(client, {
  Customer: customer,
  DateIssued: '2026-04-23',
  DateTransaction: '2026-04-23',
  DateDue: '2026-04-23',
  DocumentNumbering: docNumbering,
  Currency: currency,
  InvoiceType: 'R',          // R = invoice, P = proforma
  PricesOnInvoice: 'N',       // N = net prices (commit 017d7f7c)
  IssuedInvoiceRows: [
    {
      RowNumber: 1,
      ItemName: 'Proizvod ABC',
      Quantity: 2,
      PriceWithVAT: 1500,
      VATPercent: 20,
      VatRate: { ID: vatRate.ID },
      UnitOfMeasurement: 'kom',
    },
  ],
})

// 3. Issue + generate PDF in one action
const issued = await issueAndGeneratePdf(client, draft.IssuedInvoiceId, draft.RowVersion)

// 4. For B2B: send to SEF (eFaktura) — ForwardToSEF is mandatory in Serbia
// const sent = await sendEInvoice(client, issued.IssuedInvoiceId, issued.RowVersion)

// 5. Fetch the generated PDF
const attachments = await getInvoiceAttachments(client, issued.IssuedInvoiceId)

Authentication

OAuth2 Resource Owner Password Credentials (grant_type=password). Two credential sets are required:

  1. Client podaci (client_id + client_secret) — obtained from MiniMax support via "zahtev za pomoć".
  2. User podaci (username + password) — created in Moj Profil > Nova Aplikacija.
POST https://moj.minimax.rs/RS/AUT/oauth20/token
Content-Type: application/x-www-form-urlencoded

grant_type=password
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&username=YOUR_APP_USERNAME
&password=YOUR_APP_PASSWORD
&scope=minimax.rs

Tokens are cached in memory and automatically refreshed 60 seconds before expiry.

Warning: repeated failed auth attempts with the wrong password will lock the application. You then have to recreate it via Moj Profil.

Configuration

MiniMaxConfig

| Option | Type | Default | Description | | -------------- | --------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | clientId | string | required | OAuth2 Client ID. | | clientSecret | string | required | OAuth2 Client Secret. | | username | string | required | Application username. | | password | string | required | Application password. | | orgId | string | required | MiniMax Organisation ID (88675 in prod). | | apiUrl | string | https://moj.minimax.rs/RS/API | API base URL. | | authUrl | string | https://moj.minimax.rs/RS/AUT/oauth20/token | OAuth2 token URL. | | logger | PaymentLogger | consoleLogger | Pino-compatible logger. Pass req.payload.logger so quota warnings flow through Payload's structured logger. |

Voxberg integration

Hook trigger

// apps/voxberg/src/collections/Invoices/processInvoice.ts
// Triggered from the Orders afterChange hook when:
//   currency === 'RSD' && invoiceType !== 'fiscal'

Setup routes

The MiniMax integration ships with three setup/maintenance routes inside voxberg:

| Route | Purpose | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | POST /api/minimax/setup-items | Creates the catalog items in MiniMax (PROIZVOD / DOSTAVA, etc.). | | POST /api/minimax/sync-skus | Aligns SKUs between Payload and MiniMax. Required: see SKU format gotcha. | | POST /api/minimax/setup-numbering | Debug helper for document numbering + payment methods. |

Additional ops routes: /api/minimax/retry, /api/minimax/sync-stock, /api/minimax/seed-stock, /api/minimax/stock-entry.

SKU format gotcha

MiniMax uses codes like 235-0016. Payload uses VXB-* format. They do NOT line up.

Run POST /api/minimax/sync-skus after seeding new products — the route walks both catalogs and writes the MiniMax code onto the Payload variant so subsequent invoice rows reference the correct MiniMax item id. Without this, invoice creation either fails or creates a duplicate "ad-hoc" line item with no stock linkage.

Voxberg fields

Added to collections by the integration (not by this package):

  • Orders.invoiceType'fiscal' | 'b2c_invoice' | 'b2b'
  • Orders.taxNumber — PIB for B2B customers
  • Transactions.miniMaxInvoiceId — reference to the issued MiniMax invoice id

Admin UI:

  • InvoiceStatus.tsx — shows MiniMax invoice status in the Orders admin panel
  • /api/minimax/retry — manual retry for failed invoices

Invoice workflow

Standard B2C

createInvoice (POST /issuedinvoices)
    ↓
issueAndGeneratePdf (PUT /issuedinvoices/{id}/actions/issueAndGeneratepdf?rowVersion=…)
    ↓
getInvoiceAttachments (GET /issuedinvoices/{id}/attachments) → PDF base64

B2B with SEF e-invoice

createInvoice
    ↓
issueAndGeneratePdf
    ↓
sendEInvoice (PUT /issuedinvoices/{id}/actions/sendEInvoice?rowVersion=…)

Invoice types in Voxberg orders

| Orders.invoiceType | Behaviour | | -------------------- | ------------------------------------------------------------------------ | | fiscal | Skipped — fiscal receipts are handled at POS, not via MiniMax. | | b2c_invoice | Standard invoice: create → issue + PDF. | | b2b | Business invoice: create → issue + PDF → send to SEF (eFaktura). |

Invoice status codes

| Code | Status | Description | | ---- | ------ | -------------------------- | | O | Draft | Just created, editable | | I | Issued | Finalised, PDF generated |

API reference

Client

new MiniMaxClient(config)
client.get<T>(path, params?)
client.post<T>(path, body)
client.put<T>(path, body, params?)
client.del(path)
client.action<T>(resourcePath, actionName, rowVersion)

All endpoints are prefixed with /api/orgs/{orgId}/ automatically.

Invoices

| Function | Description | | ------------------------------------------------ | -------------------------------------------- | | createInvoice(client, data) | Create a draft invoice | | getInvoice(client, id) | Get invoice by id | | issueAndGeneratePdf(client, id, rowVersion) | Issue invoice and generate PDF (single call) | | issueInvoice(client, id, rowVersion) | Issue invoice only (no PDF) | | generatePdf(client, id, rowVersion) | Generate PDF for an already-issued invoice | | sendEInvoice(client, id, rowVersion) | Send invoice to SEF (B2B e-invoice) | | getInvoiceAttachments(client, id) | Get all attachments incl. generated PDF | | cancelInvoice(client, id, rowVersion, reason?) | Cancel an issued invoice |

Customers

| Function | Description | | ------------------------------------ | ----------------------------------------------- | | findOrCreateCustomer(client, data) | Find by email/tax number, or create new (APR registry lookup by PIB for B2B) |

Items / catalog

| Function | Description | | ----------------------------------------- | --------------------------------- | | listItems(client, params?) | List catalog items | | getItemById(client, id) | Lookup by MiniMax item id | | getItemByCode(client, code) | Lookup by SKU/code (cached) | | createItem(client, data) | Create item | | findOrCreateItem(client, code, data) | Upsert by code |

Stock

| Function | Description | | ------------------------------------- | ------------------------------------------ | | getStockLevels(client, warehouseId?) | All stock levels (cached 15min) | | getStockForItem(client, itemId) | Stock for a single item | | buildStockMap(client, warehouseId?) | Code → quantity map for batch operations |

Stock entries (delivery notes)

| Function | Description | | ------------------------------------------------- | ------------------------------------ | | createStockEntry(client, data) | Create stock entry (priemka) | | getStockEntry(client, id) | Get stock entry by id | | listStockEntries(client, params?) | List stock entries | | confirmStockEntry(client, id, rowVersion) | Confirm stock entry | | cancelStockEntryConfirmation(client, id, rv) | Cancel confirmation | | deleteStockEntry(client, id) | Delete (only drafts) | | getDeliveryNotePdf(client, id) | Download delivery note PDF |

Warehouses

| Function | Description | | --------------------------------- | ----------------------------------- | | listWarehouses(client) | All warehouses | | getWarehouseById(client, id) | Lookup by id | | getWarehouse(client) | Convenience: first/default warehouse |

Cached FK lookups (1h TTL via shared cache)

| Function | Description | | --------------------------------------- | -------------------------------------------- | | getCurrencyByCode(client, code) | Currency FK (e.g. 'RSD'). | | getVatRateByCode(client, code, date?) | VAT rate FK (e.g. 'S' = 20%). | | getDocumentNumbering(client) | First available document numbering. | | getPaymentMethods(client) | Available payment methods. | | getCountryByCode(client, code) | Country FK lookup. | | clearCache() | Clear all cached FK lookups (use after writes). |

Order mapper

| Function | Description | | ------------------------------------ | ----------------------------------------- | | orderToMiniMaxInvoice(order, refs) | Map a Voxberg Order to MiniMax invoice data. Sale discount applied to product rows only — never to shipping/COD (commit 44b4f325). |

Important MiniMax API conventions

Boolean values

MiniMax uses string booleans: "D" = Yes (Da), "N" = No (Ne). Never true/false.

PricesOnInvoice: 'N'    // Net prices (post commit 017d7f7c)
SubjectToVAT: 'N'       // Not subject to VAT
ForwardToSEF: 'D'       // MANDATORY for all Serbia invoices (commit c4b61916)

Row Version (optimistic locking)

All actions (issue, generatePdf, sendEInvoice, cancel) require a rowVersion query parameter — a concurrency token returned with every response. Always pass the latest RowVersion from the previous API response.

const draft = await createInvoice(client, data)         // RowVersion = "AAA..."
const issued = await issueAndGeneratePdf(client, draft.IssuedInvoiceId, draft.RowVersion)
// issued.RowVersion is now "BBB..." for the next action

FK references

Foreign-key references use { ID: number, Name?: string }:

Customer: { ID: 12345, Name: 'Petar Petrovic' }
Currency: { ID: 1 }
VatRate: { ID: 3 }
DocumentNumbering: { ID: 1 }

Use the cached lookup helpers above to resolve these.

Endpoints

All paths are prefixed with /api/orgs/{orgId}/ by the client:

| Path | Description | | ---------------------------- | ---------------------------------------- | | /issuedinvoices | Issued invoices CRUD | | /customers | Customers CRUD | | /customers/by-tax-number | Create customer from APR registry by PIB | | /currencies/by-code/{code} | Currency lookup | | /vat-rates/by-code/{code} | VAT rate lookup | | /document-numbering | Document numbering configs | | /payment-methods | Payment methods | | /items / /items/by-code/{code} | Catalog items | | /warehouses | Warehouses | | /stocks | Stock levels | | /stockentries | Stock entries (priemka) |

Rate limits

| Limit | Value | | --------------------- | ---------- | | Daily calls per org | 1,000 | | Monthly calls per org | 20,000 |

The client tracks daily calls and emits a structured warning at 800 via logger.warn. Plan accordingly:

  • Use cached FK lookups (currency, VAT rate, doc numbering) to reduce calls.
  • Batch operations where possible.
  • Each invoice typically takes 3–5 API calls (lookups + create + issue).

Caveat: the in-memory counter resets every 24h since the first call, not at midnight UTC. On serverless / next dev restarts the counter resets to 0, so warnings may never fire. A Payload-persisted MiniMaxUsage global is on the roadmap.

Error handling

import { MiniMaxError } from 'payload-minimax-adapter'

try {
  await issueAndGeneratePdf(client, invoiceId, rowVersion)
} catch (error) {
  if (error instanceof MiniMaxError) {
    console.error(error.code)        // 'AUTH_FAILED', 'HTTP_404', etc.
    console.error(error.statusCode)  // 401, 404, etc.
    console.error(error.message)
  }
}

| Code | Description | | -------------------- | --------------------------------- | | AUTH_FAILED | OAuth2 authentication failed | | CURRENCY_NOT_FOUND | Currency code not found | | VAT_RATE_NOT_FOUND | VAT rate code not found | | NO_DOC_NUMBERING | No document numbering configured | | HTTP_{status} | API returned non-OK status | | MINIMAX_ERROR | Generic / wrapped error |

TypeScript

Full type surface re-exported from ./types:

import type {
  MiniMaxConfig,
  MiniMaxRef,
  MiniMaxInvoiceData,
  MiniMaxInvoice,
  MiniMaxInvoiceRow,
  MiniMaxPaymentMethodEntry,
  MiniMaxDocumentAttachment,
  MiniMaxAttachment,
  MiniMaxCustomerData,
  MiniMaxCustomer,
  MiniMaxListResponse,
  MiniMaxDocumentNumbering,
  MiniMaxCurrency,
  MiniMaxVatRate,
  MiniMaxPaymentMethod,
  MiniMaxCountry,
  MiniMaxItem,
  MiniMaxItemData,
  MiniMaxItemFull,
  MiniMaxWarehouse,
  MiniMaxStockLevel,
  MiniMaxStockEntryData,
  MiniMaxStockEntry,
  MiniMaxStockEntryRow,
  OrderData,
  AddressData,
} from 'payload-minimax-adapter'

Recent changes

| Commit | Change | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | | 81c2857a | P0 cache invalidation fix (deleteCached instead of setCached(null)), P1 structured logger, switch to shared cache, e2e skip guard. | | 44b4f325 | Apply sale discount to product rows only — never to shipping/COD lines. | | 2209d361 | Convert package to ESM-first (type: module). | | 589d6aba | Convert cents → currency units in the invoice mapper. | | cc89af0c | Resolve invoice item prices from populated product/variant. | | 6b7dea04 | Use Tekući račun (bank transfer) payment method — no fiscal numbering needed. | | dd467b7a | Send empty payment-methods array to prevent MiniMax fiscal default. | | c4b61916 | ForwardToSEF mandatory for all Serbian invoices. | | 017d7f7c | Switch to net prices (PricesOnInvoice='N'), drop StocksAccount from items. | | 5d1f2331 | Add AlreadyPaid field to payment method entry. | | 2026-03-12 | Pipeline went LIVEprocessInvoice.ts orchestrates MiniMax → PDF → Vercel Blob → email. |

Getting MiniMax credentials

User podaci

  1. Log in at MiniMax RS.
  2. Moj Profil > Uredi osnovne podatke.
  3. Click Nova aplikacija at the bottom.
  4. Enter application name, username, password.
  5. These become MINIMAX_USERNAME + MINIMAX_PASSWORD.

Client podaci

  1. Contact MiniMax support via zahtev za pomoć.
  2. Request API client credentials for external integration.
  3. You receive client_id + client_secret.
  4. These become MINIMAX_CLIENT_ID + MINIMAX_CLIENT_SECRET.

Organisation ID

Either look it up in the MiniMax URL or call GET /api/currentuser/orgs to list accessible organisations. Production tenant is 88675.

License

MIT © blaze IT s.r.o.

Links