Skip to content

Changelog

2026.05.18 — Discount Codes: full promo system live

A first-class discount code system is now available for all client orders.

New: Validate endpoint (client, unauthenticated-optional)

GET /api/v1/discounts/validate?code=SAVE20&subtotal=500

Returns discount_amount and final_total without consuming the code. Intended for checkout forms to give instant feedback.

Updated: POST /api/v1/orders accepts discount_code

{ "items": [], "payment_method": "bkash", "discount_code": "SAVE20" }

The code is re-validated and atomically consumed inside the order transaction. The order response now includes discount_code and discount_amount fields and total_amount reflects the post-discount price.

Admin CRUD for discount codes

Full create / read / update / delete surface at /admin/discounts plus GET /admin/discounts/:id/orders to inspect which orders used a specific code.

Delete is blocked if the code has already been applied to at least one order — set is_active = false to stop further use while keeping history.

See Discount Codes for the complete integration guide.


2026.05.18 — Redemption: phone required, email delivers voucher

Changed the redemption contract (POST /api/v1/redemption/redeem) to a simpler, fixed set of inputs:

FieldTypeDetail
codestringRequired. The 9-char token from the QR / URL.
phonestringRequired. Recipient msisdn — captured as a lead, used by support. The alias msisdn is also accepted.
emailstring (email)Optional. When provided, the revealed voucher code + instructions are emailed to the address after a successful redemption (fire-and-forget).

Removed the require_email/require_phone per-campaign flags from the redeem endpoint — phone is unconditionally required and email delivery is unconditionally optional. The lookup requires field now always returns { phone: true, email: false } so the storefront form stays in sync without any server-side configuration.

No authentication is required for either client endpoint (lookup or redeem).

See Promo Campaigns for the full flow.

2026.05.18 — Simplified fulfillment-fields shape & shorter doc routes

Two cleanups motivated by the new fulfillment_config.fields[] shape:

fulfillment_config is now a single shape

The legacy required_fields, optional_fields, field_labels and field_types arrays have been removed from every public API surface. fields[] is now the only shape — each entry already carries the name, label, type and required flag, making the legacy keys redundant.

"fulfillment_input": {
"mode": "CODE",
"fields": [
{ "name": "player_uid", "label": "Player UID", "type": "text", "required": true }
],
"required_fields": ["player_uid"],
"optional_fields": [],
"field_labels": { "player_uid": "Player UID" },
"field_types": { "player_uid": "text" },
"identifier_field": null
}

Affected endpoints: GET /api/v1/products/:id (response), POST/PUT /admin/products[/:id] (request DTO), and GET /admin/orders/variants/:id/fulfillment-fields (response). The normaliser still reads legacy storage rows (so saved fulfillment_config rows with the old shape keep working) but never emits them.

Shorter developer-portal routes

Every doc page has moved to a flat, single-segment URL. The legacy nested paths under /getting-started/ and /guides/ no longer exist.

WasIs now
/getting-started/quickstart//quickstart/
/getting-started/authentication//auth/
/getting-started/envelope//envelope/
/guides/orders//orders/
/guides/checkout-flow//checkout/
/guides/identifier-fields//identifiers/
/guides/inventory-signals//inventory/
/guides/payments//payments/
/guides/voucher-redemption//vouchers/
/guides/redemption//redemption/
/guides/sorting-and-curation//sorting/
/guides/webhooks//webhooks/
/guides/error-handling//error-handling/
/guides/admin-product-management//admin-products/
/guides/fulfillment-recovery//recovery/
/guides/sweepers//sweepers/

Bookmark the new URLs.

2026.05.17 — Per-product configurable fulfillment fields

Products can now declare arbitrary recipient inputs via a richer shape on fulfillment_config.fields[], with name, label, type and required per entry. Game cards need a Player UID and Zone ID; gift cards need an email; mobile top-ups need nothing extra — all without code changes.

{
"fields": [
{ "name": "player_uid", "label": "Player UID", "type": "text", "required": true },
{ "name": "zone_id", "label": "Zone ID", "type": "text", "required": false }
]
}
  • Admin write (POST/PUT /admin/products[/:id]) accepts fulfillment_config.fields[] — an array of FulfillmentFieldDto.
  • Client read (GET /api/v1/products/:id) returns the same shape under fulfillment_input so the storefront can render the right inputs.
  • Order validation (POST /api/v1/orders) honours the rich shape for required-field enforcement.
  • Variant fulfillment-fields endpoint (GET /admin/orders/variants/:id/fulfillment-fields) returns fields[], merging partner-side declared keys (e.g. Zendit requiredFields) as required entries.
  • Single-identifier rule still applies: at most one of email, player_id, account_id, uid across all entries — enforced at admin ingest time and again at the read path.

See the Admin Product Management guide for the full schema.

2026.05.17 — Redemption campaigns (QR promo codes)

A brand-new domain for distributing pre-funded voucher codes (Spotify, Netflix, Free Fire, …) via printed/shareable QR cards. The QR points to https://bdvoucher.com/redeem/<TOKEN>; scanning reveals the real voucher code + redemption instructions on the storefront and captures an email/phone lead in the process.

New DB tables (production schema)

  • redemption_campaigns — group of N codes; holds headline, CTA, default instructions, require_email/require_phone, max_per_ip, start/end window, optional variant_id for analytics.
  • redemption_codes — unique 9-char token, the actual_voucher_code revealed on redemption, per-code instructions override, status lifecycle pending_stock → unused → redeemed | voided | expired.
  • redemption_attempts — append-only audit log of every lookup/redeem.

Migration: prisma/migrations/redemption_module.sql (also seeds the Spotify Premium $10 — Promo campaign with the 10 sample tokens).

New client API endpoints (public, anonymous)

MethodPathPurpose
GET/api/v1/redemption/lookup/:codeReturns campaign info + required form fields; does not consume the code.
POST/api/v1/redemption/redeemAtomically consumes the code (SELECT … FOR UPDATE inside $transaction) and reveals voucher_code + instructions.

Same-IP 24-hour recovery is built in so a closed-tab user can re-see their voucher.

New admin API endpoints (/admin/redemption)

  • Campaign CRUDGET/POST/PUT/DELETE /campaigns[/:id] + GET /campaigns/:id/stats.
  • Codes
    • POST /campaigns/:id/codes/generate — bulk-create tokens. Two modes: pass entries[] with real codes (status unused), or count only (status pending_stock, fill in real codes later).
    • GET /campaigns/:id/codes — paginated, filterable.
    • GET /campaigns/:id/codes/export?base_url=… — CSV with the scan URL ready for the print shop.
    • POST /campaigns/:id/codes/void — bulk void unused codes.
    • PUT /codes/:id — set/edit actual_voucher_code, instructions, or status. Auto-flips pending_stock → unused the moment a real code is supplied.
    • DELETE /codes/:id — blocked once redeemed.

OpenAPI

Client now exposes 91 ops (was 89), admin exposes 236 ops (was 224). Specs regenerated and synced to docs-site/public/{client,admin}/.

See the new guide: Redemption campaigns (QR promo).

Zendit purchase scripts

For campaigns whose actual voucher codes come from a partner API, the new scripts/lib/zendit-redemption-import.ts helper buys a batch of N vouchers and splits them between a redemption campaign (auto-generated tokens or backfilling existing pending_stock rows) and the regular preloaded pool. Two ready-to-run entry points:

Terminal window
npm run zendit:import:spotify-redemption # SPOTIFY_CO_1M_WHS
npm run zendit:import:netflix-redemption # NETFLIX_US_20_WHS, 10 redeem + 5 preloaded

The Netflix import has already run in production: 10 new tokens under the netflix-20-promo campaign + 5 preloaded vouchers attached to variant 22406 (ZT-NETFLIX_US_20_WHS).

2026.05.14 — Curation, sort & variant ordering

Unified sort_order across every curated resource

Every customer-facing list on the storefront is now admin-curated by default. The sort_order INTEGER column + idx_<table>_sort_order index is wired up on:

  • brands, categories, tags, product_types
  • products, product_variants
  • payment_methods, faqs
  • homepage_banners, homepage_sections, homepage_section_items

Default ordering on every list endpoint is now sort_order ASC, id ASC so pagination stays stable even when many rows share the same sort_order.

New: product_variants.sort_order

Variants now carry the same column so operators can drag-reorder the variant chips on a product page. Migration: prisma/migrations/product_variants_sort_order.sql.

Admin reorder API — one canonical contract

A single ReorderDto = { items: [{ id, sort_order }] } payload (1–500 items, no duplicate ids, atomic Prisma $transaction) is accepted by:

MethodPath
PATCH/admin/brands/reorder
PATCH/admin/categories/reorder
PATCH/admin/tags/reorder
PATCH/admin/product-types/reorder
PATCH/admin/payment-methods/reorder
PATCH/admin/products/reorder
PATCH/admin/products/:productId/variants/reorder
POST/admin/homepage/banners/reorder
POST/admin/homepage/sections/reorder
POST/admin/homepage/sections/:sectionId/items/reorder

Unknown ids return 404; duplicates / empty / >500 items return 400.

Client API — ?variant_sort= on products

GET /products and GET /products/:id accept a new optional query param to control variant ordering inside each product’s variants[] array:

variant_sortOrder applied
price (default)price ASC, sort_order ASC, id ASC — cheapest first
sortsort_order ASC, id ASC — admin-curated
namename ASC, id ASC — alphabetical

Homepage and wishlist endpoints already used price-asc for variants; this aligns the main products endpoint with the same UX and adds the explicit override.

See the new Sorting & curation guide for the full contract.


2026.04.27 — Multi-code dispatch & consolidated email

Consolidated voucher email — one email per order

Previously, when a customer bought multiple products or multiple quantities of the same product, each order item triggered its own separate email. Customers received a cluttered inbox with several messages for a single checkout.

New behaviour: every fulfilled order sends exactly one email containing all voucher codes across every item and every quantity unit.

This same consolidated email is now used consistently across:

  • regular automatic fulfilment after customer checkout,
  • admin manual-code recovery,
  • admin bulk manual recovery,
  • admin partner retry recovery.

Each code in the email is labelled with its product and variant so the customer can clearly distinguish which code belongs to which purchase.

Order response: dispatched_email, email_sent_at, voucher_codes

Three new fields are now included at the top level of every order response (both client GET /orders/:id and admin GET /admin/orders/:id):

FieldDescription
dispatched_emailEmail address the consolidated voucher email was sent to. null until sent.
email_sent_atISO 8601 timestamp of the last successful voucher-email dispatch. null until sent.
voucher_codesFlat deduplicated list of all codes across every item — same list the customer received.

Manual dispatch now supports multiple codes (codes[])

POST /admin/orders/:id/fulfillment/manual-code can now accept an array of codes in a single request. This is required whenever an order item has quantity > 1 because each unit needs its own unique voucher code.

{
"orderItemId": 123,
"codes": [
{ "code": "ABCD-1234-EFGH-5678", "pin": "1111" },
{ "code": "WXYZ-9876-MNOP-5432", "pin": "2222" },
{ "code": "QRST-1122-UVWX-3344", "pin": "3333" }
],
"notes": "3× quantity — all codes from supplier"
}
  • The legacy code / pin / redemptionUrl / serialNumber shorthand still works for single-quantity items.
  • If both code and codes are provided, codes wins.
  • All codes are persisted as individual vouchers rows.
  • The customer receives one consolidated email containing every code.
  • The audit attempt records "FIRST-CODE (+N more)" in code_provided; the full array is in partner_response.allCodes.

New order-level admin API: fulfill all items in one request

Admins can now recover the whole order in one call:

  • POST /admin/orders/:id/fulfillment/manual-codes

Payload is grouped by orderItemId, and each item must provide exactly one code per purchased unit. Mixed partner sources are supported via item-level or per-code partnerId.

New one-click resend API for admin panel

Admins can now resend the full consolidated order voucher email without entering codes again:

  • POST /admin/orders/:id/fulfillment/resend-email

This sends all persisted voucher codes currently linked to the order in one email, again labelled by product and variant.

Database linkage for reliable resend / multi-quantity visibility

Each stored voucher is now linked directly to its order_item via:

  • vouchers.order_item_id

This makes multi-quantity orders, consolidated resend, and top-level voucher_codes reporting reliable even when an order contains multiple items or multiple units of the same item.

partnerId on manual dispatch is optional

Set partnerId when the manually-entered code(s) originate from a known partner so the vouchers rows link to that partner for analytics. Omit it for a pure admin entry — rows are saved with partner_id: null.


2026.04.27 — PayStation payment method

paystation added to the payment_method Postgres enum

POST /orders with payment_method: "paystation" previously returned 500 because the payments.method column’s Postgres enum production.payment_method did not include paystation. This is now fixed:

ALTER TYPE production.payment_method ADD VALUE IF NOT EXISTS 'paystation';

The Prisma payments.create() call was also refactored to use $executeRawUnsafe with an explicit ::production.payment_method cast, making the payment recording immune to future enum additions without requiring a Prisma client rebuild or Docker image rebuild.


2026.04.27 — Fulfillment hardening

Email failures on manual dispatch are now visible at the order level

Previously, when the voucher row was saved but the customer email failed to send, the order item was still marked fulfilled = true and the order could be promoted to SUCCEEDED — making the failure invisible outside the attempts log.

New behaviour:

  • The voucher is still saved and linked.
  • order_items.fulfilled stays false and fulfillment_status is set to DISPATCH_FAILED, surfacing the failure on the standard order list / detail endpoints.
  • orders.status stays on NEEDS_ATTENTION.
  • Related fulfillment_failed / fulfillment_partial admin alerts are not auto-resolved.
  • Re-submitting the same code is idempotent and triggers a fresh email attempt.

partnerId is optional on manual dispatch

Set it when the manually-entered voucher code originates from a known partner so the saved voucher row is linked for analytics. Omit it for a pure admin-manual entry — the row is saved with partner_id: null.

Database schema

  • partner_api_transactions.partner_id is now nullable with ON DELETE SET NULL. Required so admin-manual transactions (no real partner) can be logged without a synthetic FK.
  • orders.dispatched_email VARCHAR(255) — email address the consolidated voucher email was sent to.
  • orders.email_sent_at TIMESTAMPTZ — timestamp of last successful dispatch.

Bug fixes

  • Foreign key violation on vouchers.partner_id during admin manual dispatch — partner_id: 0 is now coerced to null in storeVouchers and logTransaction.
  • Misleading “voucher row not found” errors when the underlying Prisma write failed — storeVouchers now re-throws the real error.
  • PAYMENT_METHODS array now includes paystation, fixing a 400 Bad Request on POST /orders with payment_method: "paystation".
  • bKash 502 errors now include the gateway’s actual statusCode / statusMessage in the response body for faster diagnosis.
  • Order status promotion now recognises NEEDS_ATTENTION as a valid target state in PATCH /admin/orders/:id/status.
  • Redirect-fulfillment emails now use the real order_number instead of the internal numeric order id.

Tests

  • FulfillmentService unit suite — 17 tests (added multi-code dispatch test).
  • AdminFulfillmentService suite hardened (12 tests).
  • PaymentsService suite — 51 tests covering all three gateways.
  • All 80 fulfillment + payment unit tests pass.

2026.04.26 — Payments documentation & test coverage

New: dedicated Payments guide

End-to-end walkthrough covering all three live gateways:

  • SSLCommerz — cards / banks / MFS aggregator
  • bKash Tokenized Checkout (v1.2.0-beta)
  • PayStation — multi-MFS (bKash, Nagad, Rocket, cards)

The guide documents the unified POST /api/v1/payments/initiate/:orderId entry point, the per-gateway callback URLs, the server-side re-verification we perform on every callback, the demo-MSISDN bypass for QA, and the idempotency / Redis-locking guarantees on fulfillment.

Swagger / OpenAPI

Every payment endpoint and DTO now ships full Swagger annotations:

  • InitiatePaymentDto — enum-validated method, examples for every field
  • SSLCommerzCallbackDto, BkashCallbackDto, PayStationCallbackDto — documented field-by-field
  • POST /payments/initiate/:orderId, GET /payments/status/:transactionId, and the three */callback endpoints all carry @ApiOperation summaries, parameter docs, and response codes

The regenerated specs are visible in the Client API reference.


2026.04.23 — Admin fulfillment recovery

New admin endpoints (/admin/orders)

Three new endpoints let operators recover paid orders whose vouchers were never delivered — without touching code or re-running background jobs.

MethodPathPurpose
GET/admin/orders/:id/fulfillment-attemptsFull audit trail of every recovery attempt for an order
POST/admin/orders/:id/fulfillment/manual-codeAdmin enters one or more voucher codes; delivered via the same consolidated email pipeline as a regular order
POST/admin/orders/:id/fulfillment/partner-retryRe-runs the upstream partner (Zendit etc.) for the unfulfilled item

Behavior highlights

  • Every attempt is logged — success, email failure, or partner failure all land in fulfillment_attempts with a correlation_id so nothing is ever silently lost.
  • Partner failures never throw — if the partner call fails, the error payload (status, raw response, stack trace) is persisted so admins can inspect it without reading server logs.
  • Same email pipeline — both recovery paths reuse FulfillmentService (storeVouchers + consolidated email); the customer receives the identical voucher email as a regular order.
  • Auto-resolution — on successful recovery the open fulfillment_failed / fulfillment_partial admin alert is automatically resolved with a reference to the correlation_id.
  • Order status flip — once all items are fulfilled the order transitions from NEEDS_ATTENTIONSUCCEEDED automatically.

2026.04 — Developer Portal launch

  • 🎉 New developer portal at developer.bdvoucher.com (this site).
  • ✨ Scalar API Reference replaces Swagger UI as the primary in-app docs surface at /docs and /admin/docs.
  • 🧪 Every endpoint now ships multi-language code samples (cURL, Node, Python, PHP, Go, Ruby) via x-codeSamples.
  • 📚 Standardized error envelope ErrorEnvelope + reusable 400/401/403/404/422/429/500 responses on every operation.
  • 🏷️ All 42 controllers carry @ApiTags + @ApiBearerAuth — sidebar grouping is tidy out of the box.
  • 🔍 Added Spectral + Redocly CI lint and oasdiff-based breaking-change guard.
  • 📊 New npm run docs:audit scores doc quality per operation (fails CI below threshold).

Earlier

Older changelog entries are tracked in the repository’s CHANGELOG.md and git tags.