diff --git a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt index 0ee8fb2..021226e 100644 --- a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt +++ b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt @@ -275,7 +275,13 @@ class PrintingShopPlugin(wrapper: PluginWrapper) : Pf4jPlugin(wrapper), VibeErpP // scoped registrar that tags every registration with this plug-in's // id, so shutdown automatically strips them from the framework's // TaskHandlerRegistry via unregisterAllByOwner("printing-shop"). - context.taskHandlers.register(PlateApprovalTaskHandler()) + // + // Note the captured context: each handler instance holds the + // PluginContext in a private field, so inside `execute` it has + // the same toolkit (jdbc, logger, eventBus, translator, etc.) + // the HTTP lambdas above use. This is the "handler-side plug-in + // context access" pattern — no new api.v1 surface required. + context.taskHandlers.register(PlateApprovalTaskHandler(context)) context.logger.info( "registered 1 TaskHandler: ${PlateApprovalTaskHandler.KEY}", ) diff --git a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/workflow/PlateApprovalTaskHandler.kt b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/workflow/PlateApprovalTaskHandler.kt index 1e0fe33..d2153fa 100644 --- a/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/workflow/PlateApprovalTaskHandler.kt +++ b/reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/workflow/PlateApprovalTaskHandler.kt @@ -1,26 +1,44 @@ package org.vibeerp.reference.printingshop.workflow +import org.vibeerp.api.v1.plugin.PluginContext import org.vibeerp.api.v1.security.Principal import org.vibeerp.api.v1.workflow.TaskContext import org.vibeerp.api.v1.workflow.TaskHandler import org.vibeerp.api.v1.workflow.WorkflowTask import java.time.Instant +import java.util.UUID /** - * Reference plug-in-contributed TaskHandler — the executable - * acceptance test for the P2.1 plug-in-loaded TaskHandler registration - * seam. + * Reference plug-in TaskHandler — the executable acceptance test for + * the "handler-side plug-in context access" pattern. * - * The handler is deliberately minimal: it takes a `plateId` variable - * off the BPMN process, "approves" it by writing `plateApproved=true` - * + the approving principal + timestamp, and exits. Real printing-shop - * plate approval would also UPDATE the `plugin_printingshop__plate` - * table via `context.jdbc` — but the plug-in loader doesn't give - * TaskHandlers access to the full [org.vibeerp.api.v1.plugin.PluginContext] - * yet, so v0.7 of this handler is a pure functional bridge. Giving - * handlers a context (or a narrowed projection thereof) is the - * obvious next chunk once REF.1 lands a real BPMN process that needs - * plug-in state mutation. + * **The pattern.** The framework deliberately does NOT add a new api.v1 + * surface to pass a PluginContext through [TaskContext] — instead, a + * plug-in instantiates its handlers inside `start(context)` and passes + * the captured [PluginContext] into each handler's constructor. The + * handler then holds the context in a private field and uses it from + * inside [execute] exactly the same way the plug-in's HTTP lambdas do. + * + * Benefits over threading a context through api.v1: + * • Zero new api.v1 surface. No new interface, no new default method. + * • Plug-in authors already understand `context.jdbc`, `context.logger`, + * `context.eventBus`, etc. from writing HTTP endpoint lambdas in + * the same file — the handler gets the identical toolkit. + * • A pure handler (no state mutation, no side effects beyond + * `ctx.set(...)`) simply omits the constructor parameter. Nothing + * forces a handler to take a context when it doesn't need one. + * • The plug-in loader's owner-tagged `unregisterAllByOwner` cleanup + * still works unchanged because registration happens inside + * `start(context)` via the scoped registrar. + * + * **What this handler does.** Reads a `plateId` variable off the BPMN + * process, updates the `plugin_printingshop__plate` row's status from + * 'DRAFT' to 'APPROVED' via `context.jdbc`, publishes the approving + * principal + timestamp as process output variables, and logs via the + * plug-in's own `PluginLogger` (tagged `[plugin:printing-shop]`). + * Idempotent: if the plate is already 'APPROVED' the update is a no-op + * (the WHERE clause's status filter means the second invocation + * updates zero rows) and the process continues normally. * * Key naming follows the convention documented on * [org.vibeerp.api.v1.workflow.TaskHandler.key]: @@ -30,13 +48,20 @@ import java.time.Instant * ids don't allow hyphens inside nested parts so we use the * underscored form for workflow keys throughout the framework). */ -class PlateApprovalTaskHandler : TaskHandler { +class PlateApprovalTaskHandler( + private val context: PluginContext, +) : TaskHandler { override fun key(): String = KEY override fun execute(task: WorkflowTask, ctx: TaskContext) { - val plateId = task.variables["plateId"] as? String + val plateIdString = task.variables["plateId"] as? String ?: error("PlateApprovalTaskHandler: BPMN process missing 'plateId' variable") + val plateId = try { + UUID.fromString(plateIdString) + } catch (ex: IllegalArgumentException) { + error("PlateApprovalTaskHandler: plateId '$plateIdString' is not a valid UUID") + } val approverLabel = when (val p = ctx.principal()) { is Principal.User -> "user:${p.username}" @@ -44,10 +69,32 @@ class PlateApprovalTaskHandler : TaskHandler { is Principal.PluginPrincipal -> "plugin:${p.pluginId}" } + // Mutate the plug-in's own table. Filter on status='DRAFT' so + // a second execution against the same plate is a clean no-op + // rather than silently overwriting a later status. + val updatedRows = context.jdbc.update( + """ + UPDATE plugin_printingshop__plate + SET status = 'APPROVED', + updated_at = :now + WHERE id = :id + AND status = 'DRAFT' + """.trimIndent(), + mapOf( + "id" to plateId, + "now" to java.sql.Timestamp.from(Instant.now()), + ), + ) + + context.logger.info( + "plate ${plateId} approved by ${approverLabel} (rows updated: ${updatedRows})", + ) + ctx.set("plateApproved", true) - ctx.set("plateId", plateId) + ctx.set("plateId", plateIdString) ctx.set("approvedBy", approverLabel) ctx.set("approvedAt", Instant.now().toString()) + ctx.set("rowsUpdated", updatedRows) } companion object {