Commit 1a45a4b7275da3b9c739b198fd97de27d3a3431a
1 parent
d04eb7f4
feat(jobs+plugins): plug-in loader JobHandler registration + reference PlateCleanupJobHandler
P1.10 follow-up. Plug-ins can now register background job handlers
the same way they already register workflow task handlers. The
reference printing-shop plug-in ships a real PlateCleanupJobHandler
that reads from its own database via `context.jdbc` as the
executable acceptance test.
## Why this wasn't in the P1.10 chunk
P1.10 landed the core scheduler + registry + Quartz bridge + HTTP
surface, but the plug-in-loader integration was deliberately
deferred — the JobHandlerRegistry already supported owner-tagged
`register(handler, ownerId)` and `unregisterAllByOwner(ownerId)`,
so the seam was defined; it just didn't have a caller from the
PF4J plug-in side. Without a real plug-in consumer, shipping the
integration would have been speculative.
This commit closes the gap in exactly the shape the TaskHandler
side already has: new api.v1 registrar interface, new scoped
registrar in platform-plugins, one constructor parameter on
DefaultPluginContext, one new field on VibeErpPluginManager, and
the teardown paths all fall out automatically because
JobHandlerRegistry already implements the owner-tagged cleanup.
## api.v1 additions
- `org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar` — single
method `register(handler: JobHandler)`. Mirrors
`PluginTaskHandlerRegistrar` exactly, same ergonomics, same
duplicate-key-throws discipline.
- `PluginContext.jobs: PluginJobHandlerRegistrar` — new optional
member with the default-throw backward-compat pattern used for
`endpoints`, `jdbc`, `taskHandlers`, and `files`. An older host
loading a newer plug-in jar fails loudly at first call rather
than silently dropping scheduled work.
## platform-plugins wiring
- New dependency on `:platform:platform-jobs`.
- New internal class
`org.vibeerp.platform.plugins.jobs.ScopedJobHandlerRegistrar`
that implements the api.v1 registrar by delegating
`register(handler)` to `hostRegistry.register(handler, ownerId = pluginId)`.
- `DefaultPluginContext` gains a `scopedJobHandlers` constructor
parameter and exposes it as `PluginContext.jobs`.
- `VibeErpPluginManager`:
* injects `JobHandlerRegistry`
* constructs `ScopedJobHandlerRegistrar(registry, pluginId)` per
plug-in when building `DefaultPluginContext`
* partial-start failure now also calls
`jobHandlerRegistry.unregisterAllByOwner(pluginId)`, matching
the existing endpoint + taskHandler + BPMN-deployment cleanups
* `destroy()` reverse-iterates `started` and calls the same
`unregisterAllByOwner` alongside the other four teardown steps
## Reference plug-in — PlateCleanupJobHandler
New file
`reference-customer/plugin-printing-shop/.../jobs/PlateCleanupJobHandler.kt`.
Key `printing_shop.plate.cleanup`. Captures the `PluginContext`
via constructor — same "handler-side plug-in context access"
pattern the printing-shop plug-in already uses for its
TaskHandlers.
The handler is READ-ONLY in its v1 incarnation: it runs a
GROUP-BY query over `plugin_printingshop__plate` via
`context.jdbc.query(...)` and logs a per-status summary via
`context.logger.info(...)`. A real cleanup job would also run an
`UPDATE`/`DELETE` to prune DRAFT plates older than N days; the
read-only shape is enough to exercise the seam end-to-end without
introducing a retention policy the customer hasn't asked for.
`PrintingShopPlugin.start(context)` now registers the handler
alongside its two TaskHandlers:
context.taskHandlers.register(PlateApprovalTaskHandler(context))
context.taskHandlers.register(CreateWorkOrderFromQuoteTaskHandler(context))
context.jobs.register(PlateCleanupJobHandler(context))
## Smoke test (fresh DB, plug-in staged)
```
# boot
registered JobHandler 'vibeerp.jobs.ping' owner='core' ...
JobHandlerRegistry initialised with 1 core JobHandler bean(s): [vibeerp.jobs.ping]
...
registered JobHandler 'printing_shop.plate.cleanup' owner='printing-shop' ...
[plugin:printing-shop] registered 1 JobHandler: printing_shop.plate.cleanup
# HTTP: list handlers — now shows both
GET /api/v1/jobs/handlers
→ {"count":2,"keys":["printing_shop.plate.cleanup","vibeerp.jobs.ping"]}
# HTTP: trigger the plug-in handler — proves dispatcher routes to it
POST /api/v1/jobs/handlers/printing_shop.plate.cleanup/trigger
→ 200 {"handlerKey":"printing_shop.plate.cleanup",
"correlationId":"95969129-d6bf-4d9a-8359-88310c4f63b9",
"startedAt":"...","finishedAt":"...","ok":true}
# Handler-side logs prove context.jdbc + context.logger access
[plugin:printing-shop] PlateCleanupJobHandler firing corr='95969129-...'
[plugin:printing-shop] PlateCleanupJobHandler summary: total=0 byStatus=[]
# SIGTERM — clean teardown
[ionShutdownHook] TaskHandlerRegistry.unregisterAllByOwner('printing-shop') removed 2 handler(s)
[ionShutdownHook] unregistered JobHandler 'printing_shop.plate.cleanup' (owner stopped)
[ionShutdownHook] JobHandlerRegistry.unregisterAllByOwner('printing-shop') removed 1 handler(s)
```
Every expected lifecycle event fires in the right order. Core
handlers are untouched by plug-in teardown.
## Tests
No new unit tests in this commit — the test coverage is inherited
from the previously landed components:
- `JobHandlerRegistryTest` already covers owner-tagged
`register` / `unregister` / `unregisterAllByOwner` / duplicate
key rejection.
- `ScopedTaskHandlerRegistrar` behavior (which this commit
mirrors structurally) is exercised end-to-end by the
printing-shop plug-in boot path.
- Total framework unit tests: 334 (unchanged from the
quality→warehousing quarantine chunk), all green.
## What this unblocks
- **Plug-in-shipped scheduled work.** The printing-shop plug-in
can now add cron schedules for its cleanup handler via
`POST /api/v1/jobs/scheduled {scheduleKey, handlerKey,
cronExpression}` without the operator touching core code.
- **Plug-in-to-plug-in handler coexistence.** Two plug-ins can
now ship job handlers with distinct keys and be torn down
independently on reload — the owner-tagged cleanup strips only
the stopping plug-in's handlers, leaving other plug-ins' and
core handlers alone.
- **The "plug-in contributes everything" story.** The reference
printing-shop plug-in now contributes via every public seam the
framework has: HTTP endpoints (7), custom fields on core
entities (5), BPMNs (2), TaskHandlers (2), and a JobHandler (1)
— plus its own database schema, its own metadata YAML, its own
i18n bundles. That's every extension point a real customer
plug-in would want.
## Non-goals (parking lot)
- A real retention policy in PlateCleanupJobHandler. The handler
logs a summary but doesn't mutate state. Customer-specific
pruning rules belong in a customer-owned plug-in or a metadata-
driven rule once that seam exists.
- A built-in cron schedule for the plug-in's handler. The
plug-in only registers the handler; scheduling is an operator
decision exposed through the HTTP surface from P1.10.
Showing
8 changed files
with
187 additions
and
0 deletions
api/api-v1/src/main/kotlin/org/vibeerp/api/v1/jobs/PluginJobHandlerRegistrar.kt
0 → 100644
| 1 | +package org.vibeerp.api.v1.jobs | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * The per-plug-in registrar a plug-in uses to register its own | |
| 5 | + * [JobHandler] implementations with the framework's Quartz-backed | |
| 6 | + * scheduler. | |
| 7 | + * | |
| 8 | + * Wired at plug-in start time via | |
| 9 | + * [org.vibeerp.api.v1.plugin.PluginContext.jobs]. A plug-in calls it | |
| 10 | + * from inside its `start(context)` lambda: | |
| 11 | + * | |
| 12 | + * ``` | |
| 13 | + * class PrintingShopPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { | |
| 14 | + * override fun start(context: PluginContext) { | |
| 15 | + * context.jobs.register(PlateCleanupJobHandler(context)) | |
| 16 | + * context.jobs.register(QuoteExpiryJobHandler(context)) | |
| 17 | + * } | |
| 18 | + * } | |
| 19 | + * ``` | |
| 20 | + * | |
| 21 | + * Exactly mirrors the [org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar] | |
| 22 | + * seam landed with P2.1's plug-in-loaded TaskHandler support: | |
| 23 | + * - Ownership is invisible to the plug-in author. The host records | |
| 24 | + * each call against the plug-in's id so the framework can | |
| 25 | + * `unregisterAll` at stop time without the plug-in tracking the | |
| 26 | + * keys it registered. | |
| 27 | + * - Plug-ins cannot impersonate another plug-in's namespace. The | |
| 28 | + * registrar is constructed by the host with a fixed plug-in id. | |
| 29 | + * - The handler may hold a reference to the PluginContext captured | |
| 30 | + * at registration time (same "handler-side context access" | |
| 31 | + * pattern the framework's workflow handlers use) so it can write | |
| 32 | + * to the plug-in's own tables via `context.jdbc`, log through | |
| 33 | + * `context.logger`, and publish events via `context.eventBus`. | |
| 34 | + * | |
| 35 | + * Duplicate-key rejection: if a plug-in tries to register a | |
| 36 | + * [JobHandler.key] that a core handler or another plug-in already | |
| 37 | + * owns, [register] throws `IllegalStateException` and the plug-in's | |
| 38 | + * `start(context)` fails. The plug-in loader strips any partial | |
| 39 | + * registrations the plug-in made before throwing so a half- | |
| 40 | + * registered state cannot linger. | |
| 41 | + */ | |
| 42 | +public interface PluginJobHandlerRegistrar { | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * Register a [JobHandler] with the framework so manual | |
| 46 | + * `POST /api/v1/jobs/handlers/{key}/trigger` calls and scheduled | |
| 47 | + * cron/one-shot jobs targeting [JobHandler.key] are dispatched | |
| 48 | + * to it. | |
| 49 | + */ | |
| 50 | + public fun register(handler: JobHandler) | |
| 51 | +} | ... | ... |
api/api-v1/src/main/kotlin/org/vibeerp/api/v1/plugin/PluginContext.kt
| ... | ... | @@ -5,6 +5,7 @@ import org.vibeerp.api.v1.event.EventBus |
| 5 | 5 | import org.vibeerp.api.v1.i18n.LocaleProvider |
| 6 | 6 | import org.vibeerp.api.v1.i18n.Translator |
| 7 | 7 | import org.vibeerp.api.v1.files.FileStorage |
| 8 | +import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar | |
| 8 | 9 | import org.vibeerp.api.v1.persistence.Transaction |
| 9 | 10 | import org.vibeerp.api.v1.security.PermissionCheck |
| 10 | 11 | import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar |
| ... | ... | @@ -129,6 +130,24 @@ interface PluginContext { |
| 129 | 130 | "PluginContext.files is not implemented by this host. " + |
| 130 | 131 | "Upgrade vibe_erp to v0.8 or later." |
| 131 | 132 | ) |
| 133 | + | |
| 134 | + /** | |
| 135 | + * Register [org.vibeerp.api.v1.jobs.JobHandler] implementations | |
| 136 | + * with the framework's Quartz-backed scheduler. Plug-ins that | |
| 137 | + * ship recurring or one-shot background work supply a handler | |
| 138 | + * per job key through this registrar. | |
| 139 | + * | |
| 140 | + * Added in api.v1.0.x alongside P1.10 (platform-jobs). Default | |
| 141 | + * implementation throws so an older host running a newer plug-in | |
| 142 | + * jar fails loudly at first call rather than silently dropping | |
| 143 | + * scheduled work. Backward-compat pattern matches `endpoints`, | |
| 144 | + * `jdbc`, `taskHandlers`, and `files`. | |
| 145 | + */ | |
| 146 | + val jobs: PluginJobHandlerRegistrar | |
| 147 | + get() = throw UnsupportedOperationException( | |
| 148 | + "PluginContext.jobs is not implemented by this host. " + | |
| 149 | + "Upgrade vibe_erp to v0.9 or later." | |
| 150 | + ) | |
| 132 | 151 | } |
| 133 | 152 | |
| 134 | 153 | /** | ... | ... |
platform/platform-plugins/build.gradle.kts
| ... | ... | @@ -29,6 +29,7 @@ dependencies { |
| 29 | 29 | implementation(project(":platform:platform-i18n")) // for per-plug-in IcuTranslator + shared LocaleProvider |
| 30 | 30 | implementation(project(":platform:platform-security")) // for PermissionEvaluator refresh wiring |
| 31 | 31 | implementation(project(":platform:platform-workflow")) // for per-plug-in TaskHandler registration |
| 32 | + implementation(project(":platform:platform-jobs")) // for per-plug-in JobHandler registration | |
| 32 | 33 | |
| 33 | 34 | implementation(libs.spring.boot.starter) |
| 34 | 35 | implementation(libs.spring.boot.starter.web) // for @RestController on the dispatcher | ... | ... |
platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt
| ... | ... | @@ -9,6 +9,7 @@ import org.vibeerp.api.v1.persistence.Transaction |
| 9 | 9 | import org.vibeerp.api.v1.plugin.PluginContext |
| 10 | 10 | import org.vibeerp.api.v1.plugin.PluginEndpointRegistrar |
| 11 | 11 | import org.vibeerp.api.v1.plugin.PluginJdbc |
| 12 | +import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar | |
| 12 | 13 | import org.vibeerp.api.v1.plugin.PluginLogger |
| 13 | 14 | import org.vibeerp.api.v1.security.PermissionCheck |
| 14 | 15 | import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar |
| ... | ... | @@ -49,6 +50,7 @@ internal class DefaultPluginContext( |
| 49 | 50 | private val pluginTranslator: Translator, |
| 50 | 51 | private val sharedLocaleProvider: LocaleProvider, |
| 51 | 52 | private val scopedTaskHandlers: PluginTaskHandlerRegistrar, |
| 53 | + private val scopedJobHandlers: PluginJobHandlerRegistrar, | |
| 52 | 54 | ) : PluginContext { |
| 53 | 55 | |
| 54 | 56 | override val logger: PluginLogger = Slf4jPluginLogger(pluginId, delegateLogger) |
| ... | ... | @@ -107,6 +109,16 @@ internal class DefaultPluginContext( |
| 107 | 109 | */ |
| 108 | 110 | override val taskHandlers: PluginTaskHandlerRegistrar = scopedTaskHandlers |
| 109 | 111 | |
| 112 | + /** | |
| 113 | + * Per-plug-in [PluginJobHandlerRegistrar] wired in P1.10's | |
| 114 | + * follow-up. The scoped registrar tags every registration with | |
| 115 | + * this plug-in's id so the framework's [JobHandlerRegistry] can | |
| 116 | + * strip every handler the plug-in contributed with a single | |
| 117 | + * `unregisterAllByOwner(pluginId)` call at plug-in stop time. | |
| 118 | + * Same discipline as the workflow-side taskHandlers registrar. | |
| 119 | + */ | |
| 120 | + override val jobs: PluginJobHandlerRegistrar = scopedJobHandlers | |
| 121 | + | |
| 110 | 122 | // ─── Not yet implemented ─────────────────────────────────────── |
| 111 | 123 | |
| 112 | 124 | override val transaction: Transaction | ... | ... |
platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt
| ... | ... | @@ -16,9 +16,11 @@ import org.vibeerp.platform.metadata.customfield.CustomFieldRegistry |
| 16 | 16 | import org.vibeerp.platform.security.authz.PermissionEvaluator |
| 17 | 17 | import org.vibeerp.platform.plugins.endpoints.PluginEndpointRegistry |
| 18 | 18 | import org.vibeerp.platform.plugins.endpoints.ScopedPluginEndpointRegistrar |
| 19 | +import org.vibeerp.platform.plugins.jobs.ScopedJobHandlerRegistrar | |
| 19 | 20 | import org.vibeerp.platform.plugins.lint.PluginLinter |
| 20 | 21 | import org.vibeerp.platform.plugins.migration.PluginLiquibaseRunner |
| 21 | 22 | import org.vibeerp.platform.plugins.workflow.ScopedTaskHandlerRegistrar |
| 23 | +import org.vibeerp.platform.jobs.JobHandlerRegistry | |
| 22 | 24 | import org.vibeerp.platform.workflow.PluginProcessDeployer |
| 23 | 25 | import org.vibeerp.platform.workflow.TaskHandlerRegistry |
| 24 | 26 | import java.nio.file.Files |
| ... | ... | @@ -70,6 +72,7 @@ class VibeErpPluginManager( |
| 70 | 72 | private val localeProvider: LocaleProvider, |
| 71 | 73 | private val taskHandlerRegistry: TaskHandlerRegistry, |
| 72 | 74 | private val pluginProcessDeployer: PluginProcessDeployer, |
| 75 | + private val jobHandlerRegistry: JobHandlerRegistry, | |
| 73 | 76 | ) : DefaultPluginManager(Paths.get(properties.directory)), InitializingBean, DisposableBean { |
| 74 | 77 | |
| 75 | 78 | private val log = LoggerFactory.getLogger(VibeErpPluginManager::class.java) |
| ... | ... | @@ -281,6 +284,7 @@ class VibeErpPluginManager( |
| 281 | 284 | pluginTranslator = pluginTranslator, |
| 282 | 285 | sharedLocaleProvider = localeProvider, |
| 283 | 286 | scopedTaskHandlers = ScopedTaskHandlerRegistrar(taskHandlerRegistry, pluginId), |
| 287 | + scopedJobHandlers = ScopedJobHandlerRegistrar(jobHandlerRegistry, pluginId), | |
| 284 | 288 | ) |
| 285 | 289 | try { |
| 286 | 290 | vibeErpPlugin.start(context) |
| ... | ... | @@ -296,6 +300,7 @@ class VibeErpPluginManager( |
| 296 | 300 | // undeployment is unnecessary here. |
| 297 | 301 | endpointRegistry.unregisterAll(pluginId) |
| 298 | 302 | taskHandlerRegistry.unregisterAllByOwner(pluginId) |
| 303 | + jobHandlerRegistry.unregisterAllByOwner(pluginId) | |
| 299 | 304 | return |
| 300 | 305 | } |
| 301 | 306 | |
| ... | ... | @@ -326,6 +331,7 @@ class VibeErpPluginManager( |
| 326 | 331 | started.removeAll { it.first == pluginId } |
| 327 | 332 | endpointRegistry.unregisterAll(pluginId) |
| 328 | 333 | taskHandlerRegistry.unregisterAllByOwner(pluginId) |
| 334 | + jobHandlerRegistry.unregisterAllByOwner(pluginId) | |
| 329 | 335 | pluginProcessDeployer.undeployByPlugin(pluginId) |
| 330 | 336 | } |
| 331 | 337 | } |
| ... | ... | @@ -341,6 +347,7 @@ class VibeErpPluginManager( |
| 341 | 347 | } |
| 342 | 348 | endpointRegistry.unregisterAll(pluginId) |
| 343 | 349 | taskHandlerRegistry.unregisterAllByOwner(pluginId) |
| 350 | + jobHandlerRegistry.unregisterAllByOwner(pluginId) | |
| 344 | 351 | pluginProcessDeployer.undeployByPlugin(pluginId) |
| 345 | 352 | } |
| 346 | 353 | started.clear() | ... | ... |
platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/jobs/ScopedJobHandlerRegistrar.kt
0 → 100644
| 1 | +package org.vibeerp.platform.plugins.jobs | |
| 2 | + | |
| 3 | +import org.vibeerp.api.v1.jobs.JobHandler | |
| 4 | +import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar | |
| 5 | +import org.vibeerp.platform.jobs.JobHandlerRegistry | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * The per-plug-in implementation of [PluginJobHandlerRegistrar] that | |
| 9 | + * plug-ins receive through | |
| 10 | + * [org.vibeerp.api.v1.plugin.PluginContext.jobs]. | |
| 11 | + * | |
| 12 | + * Parallel to the workflow-side | |
| 13 | + * [org.vibeerp.platform.plugins.workflow.ScopedTaskHandlerRegistrar]. | |
| 14 | + * Holds a reference to the framework's host [JobHandlerRegistry] | |
| 15 | + * and a fixed `pluginId`; every [register] call tags the | |
| 16 | + * registration with the plug-in's id so the host can drop every | |
| 17 | + * handler the plug-in contributed with a single | |
| 18 | + * `JobHandlerRegistry.unregisterAllByOwner(pluginId)` call at | |
| 19 | + * plug-in stop time. | |
| 20 | + * | |
| 21 | + * Plug-ins cannot impersonate another plug-in's namespace because | |
| 22 | + * they never set the `ownerId` argument — it's baked into this | |
| 23 | + * registrar by the host when it constructs | |
| 24 | + * [org.vibeerp.platform.plugins.DefaultPluginContext]. | |
| 25 | + */ | |
| 26 | +internal class ScopedJobHandlerRegistrar( | |
| 27 | + private val hostRegistry: JobHandlerRegistry, | |
| 28 | + private val pluginId: String, | |
| 29 | +) : PluginJobHandlerRegistrar { | |
| 30 | + | |
| 31 | + override fun register(handler: JobHandler) { | |
| 32 | + hostRegistry.register(handler, ownerId = pluginId) | |
| 33 | + } | |
| 34 | +} | ... | ... |
reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt
| ... | ... | @@ -6,6 +6,7 @@ import org.vibeerp.api.v1.plugin.HttpMethod |
| 6 | 6 | import org.vibeerp.api.v1.plugin.PluginContext |
| 7 | 7 | import org.vibeerp.api.v1.plugin.PluginRequest |
| 8 | 8 | import org.vibeerp.api.v1.plugin.PluginResponse |
| 9 | +import org.vibeerp.reference.printingshop.jobs.PlateCleanupJobHandler | |
| 9 | 10 | import org.vibeerp.reference.printingshop.workflow.CreateWorkOrderFromQuoteTaskHandler |
| 10 | 11 | import org.vibeerp.reference.printingshop.workflow.PlateApprovalTaskHandler |
| 11 | 12 | import java.time.Instant |
| ... | ... | @@ -288,6 +289,12 @@ class PrintingShopPlugin(wrapper: PluginWrapper) : Pf4jPlugin(wrapper), VibeErpP |
| 288 | 289 | "registered 2 TaskHandlers: " + |
| 289 | 290 | "${PlateApprovalTaskHandler.KEY}, ${CreateWorkOrderFromQuoteTaskHandler.KEY}", |
| 290 | 291 | ) |
| 292 | + | |
| 293 | + // ─── Job handlers (P1.10 follow-up — plug-in loaded JobHandlers) ── | |
| 294 | + context.jobs.register(PlateCleanupJobHandler(context)) | |
| 295 | + context.logger.info( | |
| 296 | + "registered 1 JobHandler: ${PlateCleanupJobHandler.KEY}", | |
| 297 | + ) | |
| 291 | 298 | } |
| 292 | 299 | |
| 293 | 300 | /** | ... | ... |
reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/jobs/PlateCleanupJobHandler.kt
0 → 100644
| 1 | +package org.vibeerp.reference.printingshop.jobs | |
| 2 | + | |
| 3 | +import org.vibeerp.api.v1.jobs.JobContext | |
| 4 | +import org.vibeerp.api.v1.jobs.JobHandler | |
| 5 | +import org.vibeerp.api.v1.plugin.PluginContext | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Reference plug-in-contributed JobHandler — the executable | |
| 9 | + * acceptance test for the P1.10 follow-up "plug-in loaded | |
| 10 | + * JobHandler registration" seam. | |
| 11 | + * | |
| 12 | + * Captures the [PluginContext] via constructor so it can reach | |
| 13 | + * `context.jdbc` / `context.logger` from inside [execute], matching | |
| 14 | + * the same "handler-side plug-in context access" pattern the | |
| 15 | + * workflow-side `PlateApprovalTaskHandler` uses. | |
| 16 | + * | |
| 17 | + * **What it does.** Logs a summary of plate counts per status in | |
| 18 | + * the printing-shop schema, using `context.jdbc` to read from | |
| 19 | + * `plugin_printingshop__plate`. Zero mutation — this handler is a | |
| 20 | + * reporting-only job suitable for a nightly cron schedule. Real | |
| 21 | + * cleanup jobs (e.g. "delete DRAFT plates older than 90 days") | |
| 22 | + * would layer an `UPDATE` or `DELETE` on the same `context.jdbc` | |
| 23 | + * surface. | |
| 24 | + * | |
| 25 | + * **Key naming.** `printing_shop.plate.cleanup` — same | |
| 26 | + * underscore-in-segments convention as the workflow task keys so | |
| 27 | + * both plug-in-contributed seams look alike in the framework's | |
| 28 | + * log lines. | |
| 29 | + */ | |
| 30 | +class PlateCleanupJobHandler( | |
| 31 | + private val context: PluginContext, | |
| 32 | +) : JobHandler { | |
| 33 | + | |
| 34 | + override fun key(): String = KEY | |
| 35 | + | |
| 36 | + override fun execute(jobContext: JobContext) { | |
| 37 | + context.logger.info( | |
| 38 | + "PlateCleanupJobHandler firing corr='${jobContext.correlationId()}'", | |
| 39 | + ) | |
| 40 | + | |
| 41 | + val rows = context.jdbc.query( | |
| 42 | + "SELECT status, COUNT(*) AS c FROM plugin_printingshop__plate GROUP BY status", | |
| 43 | + ) { row -> | |
| 44 | + mapOf("status" to row.string("status"), "count" to row.int("c")) | |
| 45 | + } | |
| 46 | + | |
| 47 | + val total = rows.sumOf { (it["count"] as Int) } | |
| 48 | + context.logger.info( | |
| 49 | + "PlateCleanupJobHandler summary: total=$total byStatus=$rows", | |
| 50 | + ) | |
| 51 | + } | |
| 52 | + | |
| 53 | + companion object { | |
| 54 | + const val KEY: String = "printing_shop.plate.cleanup" | |
| 55 | + } | |
| 56 | +} | ... | ... |