@jarwizz/jarshop-invoicing
v0.1.0
Published
JarShop invoicing Feature Package adopting the pinned Pinelab invoice engine.
Readme
JarShop Invoicing
@jarwizz/jarshop-invoicing is the invoicing Feature Package
described by ADR-0012. It adopts @pinelab/[email protected] and
explicitly supplies [email protected] and [email protected], which the plugin
imports without declaring dependencies.
Entries
@jarwizz/jarshop-invoicingexportsSellerProfile,InvoiceOrder,SlovakNonVatInvoiceInput,createSlovakNonVatInvoiceData,slovakNonVatInvoiceTemplate, andrenderSlovakNonVatInvoicewithout Vendure.@jarwizz/jarshop-invoicing/serverexportsJarShopInvoicingPluginand theJarShopInvoicingConfigtype. The JarShop plugin composes the adopted engine and exposes its Dashboard extension through a directly discoverable package entry. It also exportsseedSlovakNonVatInvoiceTemplate. It also exportsconfigureSlovakInvoicing,SlovakInvoicingOptions,configureInvoiceNumbering,JarShopInvoicingInit1789336800000, andjarShopInvoicingMigrations.
The server entry targets Vendure 3.7.1 and the exact peers in package.json.
Consumers import this package, not Pinelab, and do not choose the engine version.
Production publication and staging verification remain separate release work.
Installation policy
The Client Project must explicitly deny [email protected] in its pnpm
allowBuilds policy. Its install script must not download Chrome. The worker
image supplies Chrome through PUPPETEER_EXECUTABLE_PATH; image and font setup
belong to the later adoption and staging slices.
Verification
pnpm verify:packages builds and packs this package, validates its exports and
tarball inventory, runs publint and Are the Types Wrong, and imports the root
from a clean consumer without Vendure.
pnpm --filter @jarwizz/jarshop-invoicing test additionally packs and installs
the server in a separate consumer, compiles a typed Vendure configuration, and
boots Vendure 3.7.1 against disposable PostgreSQL to query the Invoice GraphQL
schema. The fixture follows the repository's skipLibCheck: true policy; it
checks the consumer configuration, not all upstream declaration files.
Docker is required. TestServer explicitly creates the disposable schema;
this is not migration evidence. No PDF rendering or Chrome launch is tested.
Slovak non-VAT template
The template is reviewed package source, uses local DejaVu Sans/Arial fonts,
and has no external assets or custom Handlebars helpers. The Seller Profile has
name, street, city, postalCode, ico, dic, vatStatus: "non-payer",
iban, and registerEntry. Other VAT statuses are rejected at runtime.
It renders “Nie sme platiteľmi DPH” with no VAT columns or totals.
renderSlovakNonVatInvoice({ seller, order, invoiceNumber, issuedAt }) returns
HTML without starting Chrome. invoiceNumber is supplied by the caller, never
allocated by the template. issuedAt is an explicit date rendered in the
Europe/Bratislava timezone. Order prices are Vendure minor units; discounted
line/unit prices including allocated order discounts (prorated*WithTax),
shipping, and the authoritative order total are formatted
in the order currency. Billing address takes precedence, falling back to
shipping when its street is absent, and the order customer supplies a missing
buyer name. Only synthetic identities are used in tests.
The engine compiles stored templates against the loadDataFn return value.
configureSlovakInvoicing supplies the compatible loader. It increments the
latest Channel number (or Last Issued Number), maps the Seller Profile and
order, and uses the stored original total for credits. A credit says
“Dobropis”, names the original invoice, and reverses it in full without
reusing lines from the now-cancelled or modified order.
After migrations, the Client Project's explicit seed script calls:
import { ModuleRef } from "@nestjs/core";
import { Injector, RequestContextService } from "@vendure/core";
import { seedSlovakNonVatInvoiceTemplate } from "@jarwizz/jarshop-invoicing/server";
const ctx = await app.get(RequestContextService).create({
apiType: "admin",
channelOrToken: channelToken,
});
await seedSlovakNonVatInvoiceTemplate(ctx, new Injector(app.get(ModuleRef)));The seed uses the adopted plugin's configuration service to enable generation and credit invoices for the selected Channel and replace its stored template. Repeat runs reuse that Channel's configuration row. Dashboard template edits are temporary and are replaced by the next explicit seed. Do not call this operation from application startup. Verify worker rendering before live adoption.
Follow-up scope
Runtime adoption is provided by the starter; delivery is #152; staging and publication are #153. Invoice entities, rendering, credit invoices, Dashboard screens, and events remain upstream-owned.
Last Issued Number and migrations
For an existing legal series, spread configureInvoiceNumbering(lastIssuedNumber)
into JarShopInvoicingPlugin.init(...). The value is reviewed Client Project
configuration. For example, 20260143 produces 20260144 as the first new
invoice with the adopted loader. Do not subtract one: Pinelab's misleadingly
named startInvoiceNumber is passed to the loader as the previous number.
Once invoices exist, the latest number in the current Channel takes precedence
over this initial configuration. A custom loadDataFn must preserve the same
increment-by-one contract, including on retries.
The helper accepts positive integers up to 2147483646, leaving room for the
next PostgreSQL integer. Zero is rejected because Pinelab replaces a falsy
start value with its default 10000. This API continues an existing series;
it does not define a new series or its format. The yearly series transition is
explicitly deferred pending the operator's answer tracked on #150.
Register package-owned migrations explicitly:
import { jarShopInvoicingMigrations } from "@jarwizz/jarshop-invoicing/server";
// In the Client Project's PostgreSQL dbConnectionOptions:
// migrations: [...clientMigrations, ...jarShopInvoicingMigrations],
// synchronize: false,
// migrationsRun: false,Run migrations through the Client Project's explicit migration command before
seeding or issuing. The hand-written migration targets PostgreSQL and Vendure's
default integer entity IDs and naming strategy. It creates invoice and
invoice_config, preserving the pinned upstream schema, including string
Channel/order IDs, the credit-invoice self-reference and the unique
(channelId, invoiceNumber) constraint. Do not apply it over tables previously
created by synchronization; reconcile such a database explicitly before adoption.
Rollback locks both tables and refuses while any invoice or configuration row
exists. Run migrations transactionally (TypeORM's default). Disabling the
feature is not a rollback operation.
test/migration.test.ts runs up/down/up against disposable PostgreSQL, checks
that TypeORM has no schema differences, verifies Channel-scoped uniqueness and
refusal to drop issued records. test/issuance.test.ts issues two real Vendure
orders against this migration and forces both initial candidates to collide;
it asserts the engine's retry warning and the resulting 20260144/20260145.
Only Chrome rendering and PDF storage are replaced at the system boundaries.
This proves local numbering and migration behavior, not PDF or staging readiness.
Runtime adoption
JarShopInvoicingPlugin.init(
configureSlovakInvoicing({
seller: reviewedSellerProfile,
lastIssuedNumber: 20260143,
vendureHost: "https://shop.example.com",
localStorageDirectory: "/data/invoices",
chromeExecutablePath: "/usr/bin/chromium",
}),
);The adopted subscriber handles OrderPlacedEvent; no second subscriber or
inline PDF generation is added. It enqueues a generate-invoice job with ten
retries. The worker renders and stores the PDF. Render errors are observable in
worker logs and the Vendure job queue; checkout is already complete. After
exhausted retries, repair Chrome/storage configuration before attempting
explicit operator recovery; automatic retries do not continue indefinitely.
Never log or distribute its email-bearing download URLs to customers.
The Dashboard bridge uses Pinelab's invoice queries and correction mutation, while presenting JarShop-owned operator controls. Downloads use an authenticated invoice-ID endpoint without customer data in the URL. The order detail distinguishes downloading an existing PDF, intentionally redelivering that document through the Client Project outbox, and correcting business data through Pinelab's credit-note-and-replacement flow. JarShop ships the bridge as raw TSX so the Client Project's Vite build transforms it.
test/event-path.test.ts uses the real Shop API COD mutation, EventBus,
PostgreSQL package migrations, SQL job queue, and local file strategy. It
asserts both placement and invoice-created events and one invoice. A synthetic
PDF failure is retried by the same job without changing successful placement.
Only Chrome rendering is replaced in this test. This proves one normal
placement and a pre-insert render retry; it does not claim general exactly-once
processing after arbitrary storage failures or replayed external events.
Generated-consumer verification installs retained, verified package tarballs,
checks a frozen reinstall, runs a browser COD checkout and waits for one real
worker-generated PDF. Registry installation of the unreleased 0.0.0 pin is
not available until the release slice updates and publishes the package matrix.
The worker needs Chrome and local fonts; server and worker need the same storage
directory and backups. This local proof does not replace real-image staging.
