{"id":50513894,"url":"https://github.com/artcodestudio/wn-vouchers-plugin","last_synced_at":"2026-06-02T22:03:21.745Z","repository":{"id":362043662,"uuid":"1256988533","full_name":"ArtCodeStudio/wn-vouchers-plugin","owner":"ArtCodeStudio","description":"Gift vouchers for WinterCMS: online purchase via Mollie, digital (PDF + QR) and physical vouchers, partial redemption with a balance ledger.","archived":false,"fork":false,"pushed_at":"2026-06-02T11:10:08.000Z","size":130,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T11:22:56.632Z","etag":null,"topics":["gift-cards","gutschein","laravel","mollie","octobercms","pdf","php","qr-code","vouchers","wintercms"],"latest_commit_sha":null,"homepage":"https://artandcode.studio","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ArtCodeStudio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-02T09:03:36.000Z","updated_at":"2026-06-02T11:10:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ArtCodeStudio/wn-vouchers-plugin","commit_stats":null,"previous_names":["artcodestudio/wn-vouchers-plugin"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ArtCodeStudio/wn-vouchers-plugin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArtCodeStudio%2Fwn-vouchers-plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArtCodeStudio%2Fwn-vouchers-plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArtCodeStudio%2Fwn-vouchers-plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArtCodeStudio%2Fwn-vouchers-plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ArtCodeStudio","download_url":"https://codeload.github.com/ArtCodeStudio/wn-vouchers-plugin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArtCodeStudio%2Fwn-vouchers-plugin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33838221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gift-cards","gutschein","laravel","mollie","octobercms","pdf","php","qr-code","vouchers","wintercms"],"created_at":"2026-06-02T22:03:20.777Z","updated_at":"2026-06-02T22:03:21.738Z","avatar_url":"https://github.com/ArtCodeStudio.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JumpLink.Vouchers\n\n\u003e A gift-voucher (“Gutschein”) system for [WinterCMS](https://wintercms.com).\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)\n[![WinterCMS](https://img.shields.io/badge/WinterCMS-1.2-blue.svg)](https://wintercms.com)\n[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777bb4.svg)](https://www.php.net)\n\nSell gift vouchers online with validated data and real payments, deliver them\n**digitally** (PDF + QR code by email) or **physically** (pre-printed card by\npost), and let staff redeem them — including **partial redemptions with a\nrunning balance** — from the backend or a tablet at the till.\n\nOriginally built to replace a brittle “request a voucher” contact form for a\nseaside restaurant, but written as a self-contained, reusable plugin: money,\nnumbering, ledger, payment, PDF/QR and fulfillment are all generic.\n\n\u003e **Standalone by design.** This is *not* an extension of\n\u003e [`JumpLink.Events`](https://github.com/ArtCodeStudio/wn-events-plugin). Vouchers\n\u003e (monetary value, sequential numbering, a balance ledger, payment, PDF/QR,\n\u003e fulfillment) are a distinct domain. The proven Events conventions (plugin\n\u003e registration, service classes, settings, mail templates, route style) are\n\u003e *reused, not coupled*.\n\n---\n\n## Features\n\n- 🛒 **Online purchase** with a free amount (“Wunschbetrag”) plus quick-pick\n  buttons, bounded by configurable min/max.\n- 💳 **Mollie payment** (Apple/Google Pay, cards, SEPA, PayPal via one\n  integration). The webhook is the **sole** voucher-issuing authority — no\n  voucher is created until the payment is confirmed server-side.\n- 📄 **Digital delivery**: a branded PDF with a QR code, downloadable and emailed.\n- 📬 **Physical delivery**: a pre-printed card by post, with a configurable\n  service fee and a shipping notification for staff.\n- 💶 **Partial redemption with a running balance**: a 50 € voucher spent in a\n  30 € order leaves 20 €. Backed by an **append-only ledger**, so every\n  redemption is auditable and the balance can always be recomputed.\n- 🔢 **Configurable starting number** so you can continue an existing paper\n  ledger; auto-issued numbers and hand-written ones stay in disjoint ranges and\n  can never collide.\n- 🧾 **Multi-purpose voucher (Mehrzweckgutschein) VAT model** by default: no VAT\n  at sale, the VAT rate/split is captured at **redemption**.\n- 🔐 **Signed QR tokens** (HMAC), not the bare code — a leaked voucher number\n  can’t be forged into a redeem link.\n- 🛠️ **Backend management** of orders, vouchers and redemptions, plus a settings\n  page for numbering, fees, denominations, VAT mode and PDF branding.\n\n## Status \u0026 roadmap\n\nThis plugin is built in milestones. **The digital MVP (M0 + M1) is complete and\ntested.**\n\n| Milestone | Scope | Status |\n|-----------|-------|--------|\n| **M0** | Data model + migrations, backend UI, deterministic core (numbering, ledger-safe redemption, code + signed QR token), test suite | ✅ Done |\n| **M1** | Purchase + return components, Mollie payment flow (webhook issuance), PDF/QR rendering, confirmation email, backend partial redemption | ✅ Done |\n| **M2** | Physical fulfillment + service fee + shipping notification + manual numbering | ⏳ Planned |\n| **M3** | Tablet POS page (backend-auth gated, QR camera scan, on-site sale) | ⏳ Planned |\n| **M4** | Accounting reconciliation, retention/anonymization | ⏳ Planned |\n\nThe end-to-end digital flow works: a buyer picks an amount, pays via Mollie, the\nwebhook issues exactly one numbered voucher, the PDF (with signed QR) is rendered\nand emailed, and staff can book ledger-safe (partial) redemptions from the\nbackend. The physical-card service fee + shipping and the tablet POS scan page\nfollow in M2/M3.\n\n## Requirements\n\n- WinterCMS **1.2+** (Laravel 9 era), PHP **8.1+**\n- A [Mollie](https://www.mollie.com) account (test key works for development)\n- Payment/PDF/QR rely on `mollie/mollie-api-php`, `barryvdh/laravel-dompdf` and\n  `endroid/qr-code` — declared as dependencies of this plugin.\n\n## Installation\n\nUntil this is published on Packagist, install it as a plugin directory and pull\nits runtime dependencies into the app:\n\n```bash\n# from your WinterCMS project root\ngit clone https://github.com/ArtCodeStudio/wn-vouchers-plugin.git \\\n  plugins/jumplink/vouchers\n\n# Install the plugin's third-party dependencies into the app (Winter does not\n# run Laravel package auto-discovery, so the plugin registers dompdf itself).\ncomposer require mollie/mollie-api-php barryvdh/laravel-dompdf endroid/qr-code\n\nphp artisan winter:up    # runs the 3 migrations\n```\n\n## Configuration\n\nSecrets live only in your `.env` (never in the database, settings, or git):\n\n```dotenv\nMOLLIE_API_KEY=test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   # or live_…\nVOUCHER_TOKEN_SECRET=change-me                         # HMAC pepper for QR tokens\n```\n\nEverything else is configured in the backend under **Settings → Vouchers**:\nstarting number, service fee, min/max value, denominations, default validity,\nVAT mode, Mollie mode (test/live), sender/notification addresses and PDF\nbranding.\n\n## How it works\n\n### Money is integer cents\n\nAll amounts are stored as integer cents (`balance_cents`, `face_value_cents`, …)\nto avoid floating-point rounding errors in balance arithmetic. Formatting to\neuros happens only at the view layer.\n\n### The ledger is the source of truth\n\nThree tables back the plugin:\n\n- **`jumplink_vouchers_voucher_orders`** — a purchase and its payment. Created\n  as `pending`; it only issues vouchers once the payment is confirmed.\n- **`jumplink_vouchers_vouchers`** — the voucher itself: code, number, initial\n  value, a **cached** `balance_cents`, status, and a per-voucher token secret.\n- **`jumplink_vouchers_redemptions`** — an **append-only ledger**. Every (partial)\n  redemption, reversal or adjustment is one immutable row with `amount_cents`,\n  a balance snapshot, a `vat_breakdown`, and an idempotency key.\n\nThe invariant `balance_cents == initial_value_cents − SUM(redemptions)` always\nholds. `balance_cents` is just a row-locked cache; the ledger can always rebuild\nit. Writes go through `RedemptionService::redeem()` inside a DB transaction with\n`lockForUpdate()`, re-reading the ledger sum **inside** the lock and rejecting\nover-redemption — safe against double-taps and the Christmas rush.\n\n### Numbering can’t collide\n\n`VoucherNumberService::allocate()` hands out auto numbers atomically\n(`lockForUpdate` on the current max, `+1`) starting from a configurable floor\n(e.g. `100000`). Hand-written paper-ledger numbers live in a disjoint low range\n(`number_source = 'manual'`), so the two can never overlap — matching the mental\nmodel of a shop continuing its existing binder.\n\n### QR codes carry a signed token\n\nThe human-readable code (e.g. `MAM-100042-K`, with a mod-36 check character for\nphone/till typos) is guessable and sequential, so it is **not** what the QR\nencodes. The QR carries a signed token — an HMAC over the voucher id keyed by\nthe per-voucher secret plus an app pepper (`VOUCHER_TOKEN_SECRET`). A leaked\nnumber can’t be forged into a redeem link, and the QR never debits anything by\nitself; it only opens an **authenticated** lookup.\n\n### VAT: multi-purpose voucher by default\n\nFor a multi-purpose voucher (Mehrzweckgutschein), VAT is not due at sale but at\n**redemption**, at whichever rate applies to what was actually bought (e.g. a\nreduced food rate vs. a standard rate for drinks/service). The plugin records\nthe rate/split per redemption in `vat_breakdown`; the sale receipt carries the\nlegal multi-purpose-voucher note. The mode is configurable (multi-purpose ↔\nsingle-purpose). *Always confirm the treatment with your tax advisor.*\n\n## Usage\n\n### Components\n\n| Component | Tag | Purpose |\n|-----------|-----|---------|\n| `VoucherPurchase` | `voucherPurchase` | Purchase form on the buy page (amount + buyer/recipient data, Mollie redirect). |\n| `VoucherReturn` | `voucherReturn` | Landing page after payment — polls order status, offers the PDF download. |\n| `VoucherPos` | `voucherPos` | Backend-auth-gated till page for on-site lookup, redemption and sale (M3). |\n\n### Backend\n\nA **Vouchers** main menu with **Vouchers / Orders / Redemptions** submenus\n(orders carry a “paid, not yet fulfilled” counter), plus permissions\n`jumplink.vouchers.manage_vouchers`, `…manage_orders` and `…redeem_vouchers`.\n\n### Console\n\n```bash\nphp artisan jumplink:vouchers-verify         # assert the balance invariant for every voucher\nphp artisan jumplink:vouchers-verify --fix   # recompute any drifted cached balances from the ledger\n```\n\n## Development\n\nThe plugin is developed against a local WinterCMS install with the plugin\nbind-mounted (or cloned) into `plugins/jumplink/vouchers`:\n\n```bash\ncomposer create-project wintercms/winter my-winter\ncd my-winter\ngit clone https://github.com/ArtCodeStudio/wn-vouchers-plugin.git \\\n  plugins/jumplink/vouchers\nphp artisan winter:up\n```\n\nAny container runtime works (Docker, Podman, Lando, …) as long as PHP 8.1+ with\n`gd`, `pdo_mysql`, `intl`, `zip`, `bcmath` and `mbstring` is available.\n\n### Tests\n\n```bash\n# from the WinterCMS app root\nphp artisan winter:test -p JumpLink.Vouchers\n```\n\nThe suite covers partial redemption, over-redemption rejection, the\nfull-redemption status transition, idempotency and the balance invariant;\nvoucher issuance (atomic numbering, digital/physical type, webhook idempotency);\nthe `paid`/`failed` webhook paths; PDF + QR rendering; and code/token validity\nplus tamper rejection. Mollie is mocked at the `PaymentService` seam, so the\ntests never touch the network.\n\n## Security\n\n- Honeypot + server-side validation on the purchase endpoint; rate-limiting on\n  purchase and redemption.\n- The Mollie webhook never trusts the request body — it re-fetches the payment\n  by id and checks the order metadata.\n- Signed, time-limited URLs for PDF downloads.\n- Data minimization: digital vouchers need no postal address; the buyer IP is\n  kept only briefly for abuse auditing, with a retention/anonymization path.\n- Secrets stay in `.env`; nothing sensitive is committed.\n\n## Contributing\n\nIssues and pull requests are welcome. Please keep money in integer cents, treat\nthe redemption ledger as append-only, and add a test for any change to the\nnumbering, balance or token logic.\n\n## License\n\n[MIT](LICENSE) © JumpLink – Art+Code Studio\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartcodestudio%2Fwn-vouchers-plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fartcodestudio%2Fwn-vouchers-plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartcodestudio%2Fwn-vouchers-plugin/lists"}