• Closes the P1.8 row of the implementation plan — **every Phase 1
    platform unit is now ✅**. New platform-reports subproject wrapping
    JasperReports 6.21.3 with a minimal api.v1 ReportRenderer facade,
    a built-in self-test JRXML template, and a thin HTTP surface.
    
    ## api.v1 additions (package `org.vibeerp.api.v1.reports`)
    
    - `ReportRenderer` — injectable facade with ONE method for v1:
        `renderPdf(template: InputStream, data: Map<String, Any?>): ByteArray`
      Caller loads the JRXML (or pre-compiled .jasper) from wherever
      (plug-in JAR classpath, FileStorage, DB metadata row, HTTP
      upload) and hands an open stream to the renderer. The framework
      reads the bytes, compiles/fills/exports, and returns the PDF.
    - `ReportRenderException` — wraps any engine exception so plug-ins
      don't have to import concrete Jasper exception types.
    - `PluginContext.reports: ReportRenderer` — new optional member
      with the default-throw backward-compat pattern used for every
      other addition. Plug-ins that ship quote PDFs, job cards,
      delivery notes, etc. inject this through the context.
    
    ## platform-reports runtime
    
    - `JasperReportRenderer` @Component — wraps JasperReports' compile
      → fill → export cycle into one method.
        * `JasperCompileManager.compileReport(template)` turns the
          JRXML stream into an in-memory `JasperReport`.
        * `JasperFillManager.fillReport(compiled, params, JREmptyDataSource(1))`
          evaluates expressions against the parameter map. The empty
          data source satisfies Jasper's requirement for a non-null
          data source when the template has no `<field>` definitions.
        * `JasperExportManager.exportReportToPdfStream(jasperPrint, buffer)`
          produces the PDF bytes. The `JasperPrint` type annotation on
          the local is deliberate — Jasper has an ambiguous
          `exportReportToPdfStream(InputStream, OutputStream)` overload
          and Kotlin needs the explicit type to pick the right one.
        * Every stage catches `Throwable` and re-throws as
          `ReportRenderException` with a useful message, keeping the
          api.v1 surface clean of Jasper's exception hierarchy.
    
    - `ReportController` at `/api/v1/reports/**`:
        * `POST /ping`    render the built-in self-test JRXML with
                          the supplied `{name: "..."}` (optional, defaults
                          to "world") and return the PDF bytes with
                          `application/pdf` Content-Type
        * `POST /render`  multipart upload a JRXML template + return
                          the PDF. Operator / test use, not the main
                          production path.
      Both endpoints @RequirePermission-gated via `reports.report.render`.
    
    - `reports/vibeerp-ping-report.jrxml` — a single-page JRXML with
      a title, centred "Hello, $P{name}!" text, and a footer. Zero
      fields, one string parameter with a default value. Ships on the
      platform-reports classpath and is loaded by the `/ping` endpoint
      via `ClassPathResource`.
    
    - `META-INF/vibe-erp/metadata/reports.yml` — 1 permission + 1 menu.
    
    ## Design decisions captured in-file
    
    - **No template compilation cache.** Every call compiles the JRXML
      fresh. Fine for infrequent reports (quotes, job cards); a hot
      path that renders thousands of the same report per minute would
      want a `ConcurrentHashMap<String, JasperReport>` keyed by
      template hash. Deliberately NOT shipped until a benchmark shows
      it's needed — the cache key semantics need a real consumer.
    - **No multiple output formats.** v1 is PDF-only. Additive
      overloads for HTML/XLSX land when a real consumer needs them.
    - **No data-source argument.** v1 is parameter-driven, not
      query-driven. A future `renderPdf(template, data, rows)`
      overload will take tabular data for `<field>`-based templates.
    - **No Groovy / Janino / ECJ.** The default `JRJavacCompiler` uses
      `javax.tools.ToolProvider.getSystemJavaCompiler()` which is
      available on any JDK runtime. vibe_erp already requires a JDK
      (not JRE) for Liquibase + Flowable + Quartz, so we inherit this
      for free. Zero extra compiler dependencies.
    
    ## Config trap caught during first build (documented in build.gradle.kts)
    
    My first attempt added aggressive JasperReports exclusions to
    shrink the transitive dep tree (POI, Batik, Velocity, Castor,
    Groovy, commons-digester, ...). The build compiled fine but
    `JasperCompileManager.compileReport(...)` threw
    `ClassNotFoundException: org.apache.commons.digester.Digester`
    at runtime — Jasper uses Digester internally to parse the JRXML
    structure, and excluding the transitive dep silently breaks
    template loading.
    
    Fix: remove ALL exclusions. JasperReports' dep tree IS heavy,
    but each transitive is load-bearing for a use case that's only
    obvious once you exercise the engine end-to-end. A benchmark-
    driven optimization chunk can revisit this later if the JAR size
    becomes a concern; for v1.0 the "just pull it all in" approach is
    correct. Documented in the build.gradle.kts so the next person
    who thinks about trimming the dep tree reads the warning first.
    
    ## Smoke test (fresh DB, as admin)
    
    ```
    POST /api/v1/reports/ping {"name": "Alice"}
      → 200
        Content-Type: application/pdf
        Content-Length: 1436
        body: %PDF-1.5 ... (valid 1-page PDF)
    
    $ file /tmp/ping-report.pdf
      /tmp/ping-report.pdf: PDF document, version 1.5, 1 pages (zip deflate encoded)
    
    POST /api/v1/reports/ping   (no body)
      → 200, 1435 bytes, renders with default name="world" from JRXML
        defaultValueExpression
    
    # negative
    POST /api/v1/reports/render  (multipart with garbage bytes)
      → 400 {"message": "failed to compile JRXML template:
             org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1;
             Content is not allowed in prolog."}
    
    GET /api/v1/_meta/metadata
      → permissions includes "reports.report.render"
    ```
    
    The `%PDF-` magic header is present and the `file` command on
    macOS identifies the bytes as a valid PDF 1.5 single-page document.
    JasperReports compile + fill + export are all running against
    the live JDK 21 javac inside the Spring Boot app on first boot.
    
    ## Tests
    
    - 3 new unit tests in `JasperReportRendererTest`:
      * `renders the built-in ping template to a valid PDF byte stream`
        — checks for the `%PDF-` magic header and a reasonable size
      * `renders with the default parameter when the data map is empty`
        — proves the JRXML's defaultValueExpression fires
      * `wraps compile failures in ReportRenderException` — feeds
        garbage bytes and asserts the exception type
    - Total framework unit tests: 337 (was 334), all green.
    
    ## What this unblocks
    
    - **Printing-shop quote PDFs.** The reference plug-in can now ship
      a `reports/quote.jrxml` in its JAR, load it in an HTTP handler
      via classloader, render via `context.reports.renderPdf(...)`,
      and either return the PDF bytes directly or persist it via
      `context.files.put("reports/quote-$code.pdf", "application/pdf", ...)`
      for later download. The P1.8 → P1.9 chain is ready.
    - **Job cards, delivery notes, pick lists, QC certificates.**
      Every business document in a printing shop is a report
      template + a data payload. The facade handles them all through
      the same `renderPdf` call.
    - **A future reports PBC.** When a PBC actually needs report
      metadata persisted (template versioning, report scheduling), a
      new pbc-reports can layer on top without changing api.v1 —
      the renderer stays the lowest-level primitive, the PBC becomes
      the management surface.
    
    ## Phase 1 completion
    
    With P1.8 landed:
    
    | Unit | Status |
    |------|--------|
    | P1.2 Plug-in linter        | ✅ |
    | P1.3 Plug-in HTTP + lifecycle | ✅ |
    | P1.4 Plug-in Liquibase + PluginJdbc | ✅ |
    | P1.5 Metadata store + loader | ✅ |
    | P1.6 ICU4J translator | ✅ |
    | P1.7 Event bus + outbox | ✅ |
    | P1.8 JasperReports integration | ✅ |
    | P1.9 File store | ✅ |
    | P1.10 Quartz scheduler | ✅ |
    
    **All nine Phase 1 platform units are now done.** (P1.1 Postgres RLS
    was removed by the early single-tenant refactor, per CLAUDE.md
    guardrail #5.) Remaining v1.0 work is cross-cutting: pbc-finance
    GL growth, the web SPA (R1–R4), OIDC (P4.2), the MCP server (A1),
    and richer per-PBC v2/v3 scopes.
    
    ## Non-goals (parking lot)
    
    - Template caching keyed by hash.
    - HTML/XLSX exporters.
    - Pre-compiled `.jasper` support via a Gradle build task.
    - Sub-reports (master-detail).
    - Dependency-tree optimisation via selective exclusions — needs a
      benchmark-driven chunk to prove each exclusion is safe.
    - Plug-in loader integration for custom font embedding. Jasper's
      default fonts work; custom fonts land when a real customer
      plug-in needs them.
    zichun authored
     
    Browse Code »

  • BLOCKER: wire Hibernate multi-tenancy
    - application.yaml: set hibernate.tenant_identifier_resolver and
      hibernate.multiTenancy=DISCRIMINATOR so HibernateTenantResolver is
      actually installed into the SessionFactory
    - AuditedJpaEntity.tenantId: add @org.hibernate.annotations.TenantId so
      every PBC entity inherits the discriminator
    - AuditedJpaEntityListener.onCreate: throw if a caller pre-set tenantId
      to a different value than the current TenantContext, instead of
      silently overwriting (defense against cross-tenant write bugs)
    
    IMPORTANT: dependency hygiene
    - pbc-identity no longer depends on platform-bootstrap (wrong direction;
      bootstrap assembles PBCs at the top of the stack)
    - root build.gradle.kts: tighten the architectural-rule enforcement to
      also reject :pbc:* -> platform-bootstrap; switch plug-in detection
      from a fragile pathname heuristic to an explicit
      extra["vibeerp.module-kind"] = "plugin" marker; reference plug-in
      declares the marker
    
    IMPORTANT: api.v1 surface additions (all non-breaking)
    - Repository: documented closed exception set; new
      PersistenceExceptions.kt declares OptimisticLockConflictException,
      UniqueConstraintViolationException, EntityValidationException, and
      EntityNotFoundException so plug-ins never see Hibernate types
    - TaskContext: now exposes tenantId(), principal(), locale(),
      correlationId() so workflow handlers (which run outside an HTTP
      request) can pass tenant-aware calls back into api.v1
    - EventBus: subscribe() now returns a Subscription with close() so
      long-lived subscribers can deregister explicitly; added a
      subscribe(topic: String, ...) overload for cross-classloader event
      routing where Class<E> equality is unreliable
    - IdentityApi.findUserById: tightened from Id<*> to PrincipalId so the
      type system rejects "wrong-id-kind" mistakes at the cross-PBC boundary
    
    NITs:
    - HealthController.kt -> MetaController.kt (file name now matches the
      class name); added TODO(v0.2) for reading implementationVersion from
      the Spring Boot BuildProperties bean
    vibe_erp authored
     
    Browse Code »
  • vibe_erp authored
     
    Browse Code »