Closes two open wiring gaps left by the P1.9 and P1.8 chunks —
`PluginContext.files` and `PluginContext.reports` both previously
threw `UnsupportedOperationException` because the host's
`DefaultPluginContext` never received the concrete beans. This
commit plumbs both through and exercises them end-to-end via a
new printing-shop plug-in endpoint that generates a quote PDF,
stores it in the file store, and returns the file handle.
With this chunk the reference printing-shop plug-in demonstrates
**every extension seam the framework provides**: HTTP endpoints,
JDBC, metadata YAML, i18n, BPMN + TaskHandlers, JobHandlers,
custom fields on core entities, event publishing via EventBus,
ReportRenderer, and FileStorage. There is no major public plug-in
surface left unexercised.
## Wiring: DefaultPluginContext + VibeErpPluginManager
- `DefaultPluginContext` gains two new constructor parameters
(`sharedFileStorage: FileStorage`, `sharedReportRenderer: ReportRenderer`)
and two new overrides. Each is wired via Spring — they live in
platform-files and platform-reports respectively, but
platform-plugins only depends on api.v1 (the interfaces) and
NOT on those modules directly. The concrete beans are injected
by Spring at distribution boot time when every `@Component` is
on the classpath.
- `VibeErpPluginManager` adds `private val fileStorage: FileStorage`
and `private val reportRenderer: ReportRenderer` constructor
params and passes them through to every `DefaultPluginContext`
it builds per plug-in.
The `files` and `reports` getters in api.v1 `PluginContext` still
have their default-throw backward-compat shim — a plug-in built
against v0.8 of api.v1 loading on a v0.7 host would still fail
loudly at first call with a clear "upgrade to v0.8" message. The
override here makes the v0.8+ host honour the interface.
## Printing-shop reference — quote PDF endpoint
- New `resources/reports/quote-template.jrxml` inside the plug-in
JAR. Parameters: plateCode, plateName, widthMm, heightMm,
status, customerName. Produces a single-page A4 PDF with a
header, a table of plate attributes, and a footer.
- New endpoint `POST /api/v1/plugins/printing-shop/plates/{id}/generate-quote-pdf`.
Request body `{"customerName": "..."}`, response:
`{"plateId", "plateCode", "customerName",
"fileKey", "fileSize", "fileContentType", "downloadUrl"}`
The handler does ALL of:
1. Reads the plate row via `context.jdbc.queryForObject(...)`
2. Loads the JRXML from the PLUG-IN's own classloader (not
the host classpath — `this::class.java.classLoader
.getResourceAsStream("reports/quote-template.jrxml")` —
so the host's built-in `vibeerp-ping-report.jrxml` and the
plug-in's template live in isolated namespaces)
3. Renders via `context.reports.renderPdf(template, data)`
— uses the host JasperReportRenderer under the hood
4. Persists via `context.files.put(key, contentType, content)`
under a plug-in-scoped key `plugin-printing-shop/quotes/quote-<code>.pdf`
5. Returns the file handle plus a `downloadUrl` pointing at
the framework's `/api/v1/files/download` endpoint the
caller can immediately hit
## Smoke test (fresh DB + staged plug-in)
```
# create a plate
POST /api/v1/plugins/printing-shop/plates
{code: PLATE-200, name: "Premium cover", widthMm: 420, heightMm: 594}
→ 201 {id, code: PLATE-200, status: DRAFT, ...}
# generate + store the quote PDF
POST /api/v1/plugins/printing-shop/plates/<id>/generate-quote-pdf
{customerName: "Acme Inc"}
→ 201 {
plateId, plateCode: "PLATE-200", customerName: "Acme Inc",
fileKey: "plugin-printing-shop/quotes/quote-PLATE-200.pdf",
fileSize: 1488,
fileContentType: "application/pdf",
downloadUrl: "/api/v1/files/download?key=plugin-printing-shop/quotes/quote-PLATE-200.pdf"
}
# download via the framework's file endpoint
GET /api/v1/files/download?key=plugin-printing-shop/quotes/quote-PLATE-200.pdf
→ 200
Content-Type: application/pdf
Content-Length: 1488
body: valid PDF 1.5, 1 page
$ file /tmp/plate-quote.pdf
/tmp/plate-quote.pdf: PDF document, version 1.5, 1 pages (zip deflate encoded)
# list by prefix
GET /api/v1/files?prefix=plugin-printing-shop/
→ [{"key":"plugin-printing-shop/quotes/quote-PLATE-200.pdf",
"size":1488, "contentType":"application/pdf", ...}]
# plug-in log
[plugin:printing-shop] registered 8 endpoints under /api/v1/plugins/printing-shop/
[plugin:printing-shop] generated quote PDF for plate PLATE-200 (1488 bytes)
→ plugin-printing-shop/quotes/quote-PLATE-200.pdf
```
Four public surfaces composed in one flow: plug-in JDBC read →
plug-in classloader resource load → host ReportRenderer compile/
fill/export → host FileStorage put → host file controller
download. Every step stays on api.v1; zero plug-in code reaches
into a concrete platform class.
## Printing-shop plug-in — full extension surface exercised
After this commit the reference printing-shop plug-in contributes
via every public seam the framework offers:
| Seam | How the plug-in uses it |
|-------------------------------|--------------------------------------------------------|
| HTTP endpoints (P1.3) | 8 endpoints under /api/v1/plugins/printing-shop/ |
| JDBC (P1.4) | Reads/writes its own plugin_printingshop__* tables |
| Liquibase | Own changelog.xml, 2 tables created at plug-in start |
| Metadata YAML (P1.5) | 2 entities, 5 permissions, 2 menus |
| Custom fields on CORE (P3.4) | 5 plug-in fields on Partner/Item/SalesOrder/WorkOrder |
| i18n (P1.6) | Own messages_<locale>.properties, quote number msgs |
| EventBus (P1.7) | Publishes WorkOrderRequestedEvent from a TaskHandler |
| TaskHandlers (P2.1) | 2 handlers (plate-approval, quote-to-work-order) |
| Plug-in BPMN (P2.1 followup) | 2 BPMNs in processes/ auto-deployed at start |
| JobHandlers (P1.10 followup) | PlateCleanupJobHandler using context.jdbc + logger |
| ReportRenderer (P1.8) | Quote PDF from JRXML via context.reports |
| FileStorage (P1.9) | Persists quote PDF via context.files |
Everything listed in this table is exercised end-to-end by the
current smoke test. The plug-in is the framework's executable
acceptance test for the entire public extension surface.
## Tests
No new unit tests — the wiring change is a plain constructor
addition, the existing `DefaultPluginContext` has no dedicated
test class (it's a thin dataclass-shaped bean), and
`JasperReportRenderer` + `LocalDiskFileStorage` each have their
own unit tests from the respective parent chunks. The change is
validated end-to-end by the above smoke test; formalizing that
into an integration test would need Testcontainers + a real
plug-in JAR and belongs to a different (test-infra) chunk.
- Total framework unit tests: 337 (unchanged), all green.
## Non-goals (parking lot)
- Pre-compiled `.jasper` caching keyed by template hash. A
hot-path benchmark would tell us whether the cache is worth
shipping.
- Multipart upload of a template into a plug-in's own `files`
namespace so non-bundled templates can be tried without a
plug-in rebuild. Nice-to-have for iteration but not on the
v1.0 critical path.
- Scoped file-key prefixes per plug-in enforced by the framework
(today the plug-in picks its own prefix by convention; a
`plugin.files.keyPrefix` config would let the host enforce
that every plug-in-contributed file lives under
`plugin-<id>/`). Future hardening chunk.