@sparkerp/plugin-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/bundle/blocks.json +12229 -0
  4. package/bundle/catalog.json +32475 -0
  5. package/bundle/docs/applications/hcm/employee-profile.md +144 -0
  6. package/bundle/docs/concepts/artifact-types.md +80 -0
  7. package/bundle/docs/concepts/glossary.md +124 -0
  8. package/bundle/docs/concepts/overview.md +88 -0
  9. package/bundle/docs/concepts/page-data-flow.md +84 -0
  10. package/bundle/docs/concepts/plugin-lifecycle.md +81 -0
  11. package/bundle/docs/concepts/sdk-modes.md +71 -0
  12. package/bundle/docs/docs.json +109 -0
  13. package/bundle/docs/guides/add-a-cadence-job.md +134 -0
  14. package/bundle/docs/guides/add-a-compliance-job.md +139 -0
  15. package/bundle/docs/guides/add-a-create-edit-form.md +167 -0
  16. package/bundle/docs/guides/add-a-custom-block.md +225 -0
  17. package/bundle/docs/guides/add-a-data-provider.md +216 -0
  18. package/bundle/docs/guides/add-a-kpi.md +154 -0
  19. package/bundle/docs/guides/add-a-reminder-job.md +181 -0
  20. package/bundle/docs/guides/add-an-aggregation-job.md +153 -0
  21. package/bundle/docs/guides/add-an-approval-workflow.md +222 -0
  22. package/bundle/docs/guides/add-an-entity.md +170 -0
  23. package/bundle/docs/guides/add-business-rules.md +147 -0
  24. package/bundle/docs/guides/add-i18n.md +121 -0
  25. package/bundle/docs/guides/add-menus.md +149 -0
  26. package/bundle/docs/guides/build-a-code-plugin.md +141 -0
  27. package/bundle/docs/guides/build-a-page.md +213 -0
  28. package/bundle/docs/guides/build-a-polyglot-service-plugin.md +201 -0
  29. package/bundle/docs/guides/build-a-tenant-extension-service.md +149 -0
  30. package/bundle/docs/guides/create-a-plugin.md +172 -0
  31. package/bundle/docs/guides/expose-a-plugin-operation-as-an-ai-tool.md +114 -0
  32. package/bundle/docs/guides/index.md +53 -0
  33. package/bundle/docs/guides/publish-and-upgrade.md +152 -0
  34. package/bundle/docs/guides/responsive-plugin.md +117 -0
  35. package/bundle/docs/guides/seed-data-on-install.md +127 -0
  36. package/bundle/docs/guides/set-up-the-sdk.md +120 -0
  37. package/bundle/docs/guides/use-the-mcp-server.md +104 -0
  38. package/bundle/docs/guides/validate-and-test.md +119 -0
  39. package/bundle/docs/guides/wire-a-pages-data.md +153 -0
  40. package/bundle/docs/quickstart/index.md +118 -0
  41. package/bundle/docs/recipes/condition-based-compliance-flag.md +70 -0
  42. package/bundle/docs/recipes/cross-plugin-offboarding-fanout.md +68 -0
  43. package/bundle/docs/recipes/cross-row-aggregation.md +67 -0
  44. package/bundle/docs/recipes/entity-with-approval-workflow.md +72 -0
  45. package/bundle/docs/recipes/index.md +40 -0
  46. package/bundle/docs/recipes/install-lifecycle-data-seeding.md +76 -0
  47. package/bundle/docs/recipes/kpi-dashboard-page.md +92 -0
  48. package/bundle/docs/recipes/master-detail-page.md +84 -0
  49. package/bundle/docs/recipes/n-days-before-reminder.md +72 -0
  50. package/bundle/docs/recipes/per-row-document-generator.md +65 -0
  51. package/bundle/docs/recipes/related-record-lookup-field.md +74 -0
  52. package/bundle/docs/recipes/resolved-reference-grid-column.md +63 -0
  53. package/bundle/docs/recipes/scheduled-record-generation.md +68 -0
  54. package/bundle/docs/reference/action.md +18 -0
  55. package/bundle/docs/reference/block-definition.md +35 -0
  56. package/bundle/docs/reference/block-instance.md +33 -0
  57. package/bundle/docs/reference/composite-pack-manifest.md +33 -0
  58. package/bundle/docs/reference/connector-definition.md +36 -0
  59. package/bundle/docs/reference/data-service-definition.md +22 -0
  60. package/bundle/docs/reference/data-view-definition.md +26 -0
  61. package/bundle/docs/reference/entity-aggregation-config.md +38 -0
  62. package/bundle/docs/reference/entity-cadence-config.md +33 -0
  63. package/bundle/docs/reference/entity-compliance-config.md +37 -0
  64. package/bundle/docs/reference/entity-cross-plugin-action-config.md +37 -0
  65. package/bundle/docs/reference/entity-definition.md +18 -0
  66. package/bundle/docs/reference/entity-document-generator-config.md +35 -0
  67. package/bundle/docs/reference/entity-rule-definition.md +29 -0
  68. package/bundle/docs/reference/entity-status-date-sweep-config.md +27 -0
  69. package/bundle/docs/reference/form.md +35 -0
  70. package/bundle/docs/reference/index.md +37 -0
  71. package/bundle/docs/reference/menu.md +26 -0
  72. package/bundle/docs/reference/mobile-nav.md +22 -0
  73. package/bundle/docs/reference/page-template.md +29 -0
  74. package/bundle/docs/reference/page.md +44 -0
  75. package/bundle/docs/reference/plugin-manifest.md +54 -0
  76. package/bundle/docs/reference/plugin-seed-data.md +21 -0
  77. package/bundle/docs/reference/print-template-definition.md +22 -0
  78. package/bundle/docs/reference/provider-definition.md +22 -0
  79. package/bundle/docs/reference/report-definition.md +22 -0
  80. package/bundle/docs/reference/rule.md +30 -0
  81. package/bundle/docs/reference/workflow-definition.md +28 -0
  82. package/bundle/docs/troubleshooting/grid-shows-zero-rows.md +82 -0
  83. package/bundle/docs/troubleshooting/job-never-runs.md +89 -0
  84. package/bundle/docs/troubleshooting/known-gotchas.md +193 -0
  85. package/bundle/docs/troubleshooting/publishing-errors.md +119 -0
  86. package/bundle/docs/tutorial/01-create-the-plugin.md +157 -0
  87. package/bundle/docs/tutorial/02-category-entity-and-seed.md +94 -0
  88. package/bundle/docs/tutorial/03-equipment-entity.md +121 -0
  89. package/bundle/docs/tutorial/04-equipment-register-page.md +153 -0
  90. package/bundle/docs/tutorial/05-checkout-entity-and-page.md +116 -0
  91. package/bundle/docs/tutorial/06-checkout-approval-workflow.md +139 -0
  92. package/bundle/docs/tutorial/07-return-due-reminder-job.md +129 -0
  93. package/bundle/docs/tutorial/08-menus-i18n-publish.md +123 -0
  94. package/bundle/docs/tutorial/index.md +72 -0
  95. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-aggregation-configs.json +18 -0
  96. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cadence-configs.json +9 -0
  97. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-compliance-configs.json +74 -0
  98. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-cross-plugin-configs.json +9 -0
  99. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-document-configs.json +9 -0
  100. package/bundle/examples/backend/modules/hcm-assets/spk-assembly/metadata/seed-data/hcm-assets-sweep-configs.json +260 -0
  101. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/entities/hcm_salary_planning_entry.json +41 -0
  102. package/bundle/examples/backend/modules/hcm-compensation/spk-assembly/metadata/page/salary-planning.json +1 -0
  103. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/page/promotions.json +10944 -0
  104. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.movement.json +44 -0
  105. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.promotion-acknowledge.json +19 -0
  106. package/bundle/examples/backend/modules/hcm-employee/spk-assembly/metadata/workflow/employee.resignation.json +29 -0
  107. package/bundle/examples/example-patterns.mjs +94 -0
  108. package/bundle/manifest.json +14 -0
  109. package/bundle/schemas/action.schema.json +102 -0
  110. package/bundle/schemas/block-definition.schema.json +222 -0
  111. package/bundle/schemas/block-instance.schema.json +139 -0
  112. package/bundle/schemas/composite-pack-manifest.schema.json +160 -0
  113. package/bundle/schemas/connector-definition.schema.json +29 -0
  114. package/bundle/schemas/data-service-definition.schema.json +64 -0
  115. package/bundle/schemas/data-view-definition.schema.json +148 -0
  116. package/bundle/schemas/entity-aggregation-config.schema.json +32 -0
  117. package/bundle/schemas/entity-cadence-config.schema.json +27 -0
  118. package/bundle/schemas/entity-compliance-config.schema.json +31 -0
  119. package/bundle/schemas/entity-cross-plugin-action-config.schema.json +31 -0
  120. package/bundle/schemas/entity-definition.schema.json +126 -0
  121. package/bundle/schemas/entity-document-generator-config.schema.json +29 -0
  122. package/bundle/schemas/entity-rule-definition.schema.json +75 -0
  123. package/bundle/schemas/entity-status-date-sweep-config.schema.json +21 -0
  124. package/bundle/schemas/form.schema.json +281 -0
  125. package/bundle/schemas/menu.schema.json +106 -0
  126. package/bundle/schemas/mobile-nav.schema.json +53 -0
  127. package/bundle/schemas/page-template.schema.json +46 -0
  128. package/bundle/schemas/page.schema.json +292 -0
  129. package/bundle/schemas/plugin-manifest.schema.json +139 -0
  130. package/bundle/schemas/plugin-seed-data.schema.json +24 -0
  131. package/bundle/schemas/print-template-definition.schema.json +90 -0
  132. package/bundle/schemas/provider-definition.schema.json +27 -0
  133. package/bundle/schemas/report-definition.schema.json +96 -0
  134. package/bundle/schemas/rule.schema.json +82 -0
  135. package/bundle/schemas/workflow-definition.schema.json +42 -0
  136. package/bundle/validators/block-engine.mjs +23222 -0
  137. package/bundle/validators/page-engine.mjs +21645 -0
  138. package/erp-cli/authoring-root.mjs +344 -0
  139. package/erp-cli/erp.mjs +4065 -0
  140. package/erp-cli/example-patterns.mjs +94 -0
  141. package/erp-mcp-server/index.mjs +818 -0
  142. package/package.json +27 -0
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: "6. The checkout approval workflow"
3
+ audience: tenant
4
+ ---
5
+
6
+ # 6. The checkout approval workflow
7
+
8
+ ## What we're adding
9
+
10
+ When a user clicks **Submit for approval** on a checkout, its status becomes
11
+ `PENDING_APPROVAL`. That should start an approval process; when the Office
12
+ Equipment Manager approves, the status becomes `APPROVED`; if rejected,
13
+ `REJECTED`. No approval code — a workflow definition, an entity rule, and the
14
+ platform's generic callback.
15
+
16
+ ## 1. The workflow
17
+
18
+ `spk-assembly/metadata/workflow/office-equipment.checkout-approval.json`
19
+ ([real file](./example-plugin/spk-assembly/metadata/workflow/office-equipment.checkout-approval.json)):
20
+
21
+ ```json
22
+ {
23
+ "name": "office-equipment.checkout-approval",
24
+ "description": "Single-stage Office Equipment Manager approval for a checkout submitted for approval.",
25
+ "stagesJson": "[\"manager\"]",
26
+ "tasksJson": "[{\"taskKey\":\"manager-approve\",\"stage\":\"manager\",\"taskType\":\"approval\",\"kind\":\"human\",\"payload\":{\"approvalObject\":\"office-equipment.checkout-approval\",\"amount\":0,\"dueInSeconds\":259200}}]",
27
+ "transitionsJson": "[]",
28
+ "approversJson": "{}",
29
+ "slasJson": "[]",
30
+ "escalationsJson": "[]",
31
+ "notificationsJson": "[]",
32
+ "metadataJson": "{}",
33
+ "approvalPermissions": [
34
+ { "roleCode": "OFFICE_EQUIPMENT_MANAGER", "approvalObject": "office-equipment.checkout-approval", "maxAmount": null }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ - `stagesJson`, `tasksJson`, `transitionsJson` are **JSON-encoded strings**, not
40
+ objects.
41
+ - One stage `["manager"]`, one human approval task, no transitions (reaching the
42
+ only stage's decision ends the instance).
43
+ - `approvalPermissions` is a real array; `roleCode` references the role by its
44
+ stable `code` (the one we prefixed in chapter 1).
45
+
46
+ ## 2. The rule
47
+
48
+ `spk-assembly/metadata/rules/oeq_checkout_submit_workflow.json`
49
+ ([real file](./example-plugin/spk-assembly/metadata/rules/oeq_checkout_submit_workflow.json)):
50
+
51
+ ```json
52
+ {
53
+ "entityType": "oeq_checkout",
54
+ "name": "oeq_checkout_submit_workflow",
55
+ "description": "On transition INTO PENDING_APPROVAL, start office-equipment.checkout-approval; the generic callback flips status to APPROVED/REJECTED.",
56
+ "triggerEvent": "AFTER_UPDATE",
57
+ "conditions": "{\"all\":[{\"field\":\"status\",\"op\":\"eq\",\"value\":\"PENDING_APPROVAL\"},{\"field\":\"status__previous\",\"op\":\"neq\",\"value\":\"PENDING_APPROVAL\"}]}",
58
+ "actions": "[{\"type\":\"START_WORKFLOW\",\"workflowName\":\"office-equipment.checkout-approval\",\"callbackUrl\":\"http://localhost:8080/api/v1/entities/workflow-callback\",\"config\":{\"entityType\":\"oeq_checkout\",\"idField\":\"id\",\"fields\":[{\"name\":\"status\",\"approved\":\"APPROVED\",\"rejected\":\"REJECTED\"}]}}]",
59
+ "priority": 20,
60
+ "active": true
61
+ }
62
+ ```
63
+
64
+ - The `all` condition fires exactly on the transition *into*
65
+ `PENDING_APPROVAL` — `status__previous` is the value before the update.
66
+ Without that guard the rule would re-fire on every later save.
67
+ - `config.fields` maps the workflow decision back onto the row:
68
+ approved → `status = APPROVED`, rejected → `status = REJECTED`.
69
+ - `callbackUrl` — the generic entity workflow-callback endpoint. Its base
70
+ (`http://localhost:8080`) is environment-specific; use your ERP's base URL.
71
+ - `priority: 20` leaves room below for validation rules.
72
+
73
+ ## 3. The Submit button
74
+
75
+ On the checkout detail dialog (chapter 5), the button `PUT`s the status:
76
+
77
+ ```json
78
+ "events": { "clicked": { "source": "action-chain", "actions": [
79
+ { "id": "a0", "order": 0, "type": "callApi",
80
+ "config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_checkout/records/${page.detailRecord.id}", "httpMethod": "PUT", "params": { "status": "PENDING_APPROVAL" } } },
81
+ { "id": "a1", "order": 1, "type": "callApi",
82
+ "config": { "connectionRef": "self", "path": "/api/v1/entities/oeq_checkout/records/${page.detailRecord.id}", "httpMethod": "GET", "params": {} }, "output": "refreshed" },
83
+ { "id": "a2", "order": 2, "type": "setValue", "config": { "field": "page.detailRecord", "value": "${refreshed}" } },
84
+ { "id": "a3", "order": 3, "type": "setValue", "config": { "field": "page.gridRefreshTick", "value": "${!page.gridRefreshTick}" } },
85
+ { "id": "a4", "order": 4, "type": "showToast", "config": { "message": "Submitted for approval." } }
86
+ ] } }
87
+ ```
88
+
89
+ ## Verify
90
+
91
+ ```bash
92
+ erp workflow validate spk-assembly/metadata/workflow/office-equipment.checkout-approval.json
93
+ erp schema validate spk-assembly/metadata/rules/oeq_checkout_submit_workflow.json --schema entity-rule-definition
94
+ ```
95
+
96
+ Both `OK`. After publish, the rule is installed:
97
+
98
+ ```bash
99
+ $ erp api get "/api/v1/entity-rules?entityType=oeq_checkout"
100
+ [ { "id": 388, "entityType": "oeq_checkout", "name": "oeq_checkout_submit_workflow",
101
+ "triggerEvent": "AFTER_UPDATE", "active": true } ]
102
+ ```
103
+
104
+ Submit a checkout and confirm it transitions and then *stays* pending until a
105
+ human decides it:
106
+
107
+ ```bash
108
+ $ erp api put "/api/v1/entities/oeq_checkout/records/3" --body '{"status":"PENDING_APPROVAL"}'
109
+ { "id": 3, "checkout_number": "CO-200", "status": "PENDING_APPROVAL" }
110
+ ```
111
+
112
+ `PENDING_APPROVAL` — correct. Confirm a real workflow instance started and find
113
+ its pending task:
114
+
115
+ ```bash
116
+ $ erp workflow list --definition oeq_checkout_submit_workflow --record 3
117
+ [ { "id": 5012, "status": "RUNNING", "currentStage": "manager-approval", "correlationId": "3" } ]
118
+ 1 instance(s) for record 3 of "oeq_checkout_submit_workflow".
119
+
120
+ $ erp workflow tasks 5012
121
+ [ { "id": 88, "stage": "manager-approval", "status": "PENDING", "candidateApprovers": ["MANAGER"] } ]
122
+ ```
123
+
124
+ (`erp workflow list/instance/tasks/history` reach the workflow engine through a
125
+ read-proxy on `engine-api`'s own base URL, so they work even though the workflow
126
+ engine runs as a separate service here.)
127
+
128
+ The Office Equipment Manager now sees the task in the ERP's **approval inbox**;
129
+ approving it triggers the generic callback, which sets `status = APPROVED` with
130
+ zero code from you.
131
+
132
+ ## Common mistakes
133
+
134
+ - `stagesJson`/`tasksJson` authored as objects → install fails. They're strings.
135
+ - No `status__previous` guard → workflow starts on every save.
136
+ - `approvalPermissions` with a role *name* instead of `roleCode` → nobody can
137
+ approve.
138
+
139
+ **Next:** [7. The return-due reminder job](./07-return-due-reminder-job.md)
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: "7. The return-due reminder job"
3
+ audience: tenant
4
+ ---
5
+
6
+ # 7. The return-due reminder job
7
+
8
+ ## What we're adding
9
+
10
+ A checkout that is `CHECKED_OUT` and:
11
+
12
+ - within **3 days** of its `due_date` → status `DUE_SOON`
13
+ - past its `due_date` → status `OVERDUE`
14
+
15
+ Zero Java. The platform's generic `engine-entity.status-date-sweep` job runs
16
+ daily and applies config rows we seed.
17
+
18
+ ## 1. Ship the shared config entity
19
+
20
+ So a fresh environment that predates the shared table still gets it.
21
+ `spk-assembly/metadata/entities/entity_status_date_sweep_config.json`
22
+ ([real file](./example-plugin/spk-assembly/metadata/entities/entity_status_date_sweep_config.json)) —
23
+ ships only the additive columns (`day_offset`, `seeded_by`); the installer is
24
+ idempotent-additive, so an existing table just gains anything missing.
25
+
26
+ ## 2. Seed the config rows
27
+
28
+ `spk-assembly/metadata/seed-data/office-equipment-sweep-configs.json`
29
+ ([real file](./example-plugin/spk-assembly/metadata/seed-data/office-equipment-sweep-configs.json)):
30
+
31
+ ```json
32
+ {
33
+ "entity": "entity_status_date_sweep_config",
34
+ "keyFields": ["entity_name", "date_field", "set_status_to", "day_offset"],
35
+ "source": "office-equipment",
36
+ "rows": [
37
+ {
38
+ "entity_name": "oeq_checkout",
39
+ "status_field": "status",
40
+ "when_status_in": "CHECKED_OUT",
41
+ "date_field": "due_date",
42
+ "compare_op": "lte",
43
+ "day_offset": 3,
44
+ "set_status_to": "DUE_SOON",
45
+ "active": true,
46
+ "seeded_by": "office-equipment"
47
+ },
48
+ {
49
+ "entity_name": "oeq_checkout",
50
+ "status_field": "status",
51
+ "when_status_in": "CHECKED_OUT,DUE_SOON",
52
+ "date_field": "due_date",
53
+ "compare_op": "lt",
54
+ "day_offset": 0,
55
+ "set_status_to": "OVERDUE",
56
+ "active": true,
57
+ "seeded_by": "office-equipment"
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ - Row 1: `day_offset: 3` + `compare_op: lte` → "fires when `due_date` is within
64
+ 3 days" — the "N days before" reminder.
65
+ - Row 2: `day_offset: 0` + `compare_op: lt` → "fires once `due_date` has
66
+ passed". `when_status_in` includes `DUE_SOON` so a warned checkout still
67
+ escalates to overdue.
68
+ - Neither sets `set_field`, so they write `status_field` (`status`) itself.
69
+ - `keyFields` includes `day_offset` so the two rows for the same date column are
70
+ distinct on re-install.
71
+
72
+ ### Status value vs. boolean flag
73
+
74
+ This module flips a `status` enum because that reads well on the checkout grid.
75
+ A boolean flag works too: since 2026-09-10 the generic job coerces
76
+ `set_status_to: "true"` / `"false"` to a real boolean (and numeric strings to
77
+ numbers) before the write, so a config targeting a `boolean`/`integer`/`numeric`
78
+ `set_field` is fully supported. An earlier draft of this tutorial hit a SQL type
79
+ error doing exactly that — that platform bug is fixed.
80
+
81
+ ## 3. The job registers itself
82
+
83
+ The platform ships an `AFTER_CREATE` rule on `entity_status_date_sweep_config`
84
+ that calls `ensureEntityStatusDateSweepJobRegistered`. So the first row you seed
85
+ auto-registers the job for your tenant — **you ship no register rule for this
86
+ job** (unlike the aggregation/cadence/compliance jobs).
87
+
88
+ ## Verify
89
+
90
+ ```bash
91
+ erp schema validate spk-assembly/metadata/seed-data/office-equipment-sweep-configs.json --schema plugin-seed-data
92
+ ```
93
+
94
+ `OK`. After publish, the job is enabled:
95
+
96
+ ```bash
97
+ $ erp api get "/api/v1/jobs/engine-entity.status-date-sweep"
98
+ { "jobCode": "engine-entity.status-date-sweep", "status": "ENABLED",
99
+ "cronExpression": "0 5 0 * * *", "concurrencyPolicy": "PER_TENANT" }
100
+ ```
101
+
102
+ **This exact sequence was run against the tutorial module:**
103
+
104
+ ```bash
105
+ # a CHECKED_OUT checkout whose due_date is in the past
106
+ $ erp api post "/api/v1/entities/oeq_checkout/records" \
107
+ --body '{"checkout_number":"CO-100","equipment_id":1,"employee_id":1,"checkout_date":"2026-08-01","due_date":"2026-09-04","status":"CHECKED_OUT"}'
108
+ { "id": 2, "checkout_number": "CO-100", "status": "CHECKED_OUT" }
109
+
110
+ $ erp api post "/api/v1/jobs/engine-entity.status-date-sweep/execute" --body "{}"
111
+ { "executionId": 534 }
112
+
113
+ $ erp api get "/api/v1/jobs/engine-entity.status-date-sweep/executions?size=1"
114
+ ... "resultJson": "{\"swept\": 4, \"failed\": 0, \"rowsScanned\": 102, \"configsScanned\": 30}" ...
115
+
116
+ $ erp api get "/api/v1/entities/oeq_checkout/records/2"
117
+ { "checkout_number": "CO-100", "status": "OVERDUE" }
118
+ ```
119
+
120
+ `CHECKED_OUT` → `OVERDUE`. `"failed": 0`. Verified end to end.
121
+
122
+ ## Common mistakes
123
+
124
+ - `set_field` a boolean/numeric column with an *unparseable* `set_status_to`
125
+ (e.g. `"yes"` into a boolean) → that row counts `failed`. Use `"true"`/`"false"`.
126
+ - `when_status_in` casing not matching the enum → nothing sweeps.
127
+ - Forgot `day_offset` → the "3 days before" rule fires only on the exact day.
128
+
129
+ **Next:** [8. Menus, i18n, validate, publish](./08-menus-i18n-publish.md)
@@ -0,0 +1,123 @@
1
+ ---
2
+ title: "8. Menus, i18n, validate, publish"
3
+ audience: tenant
4
+ ---
5
+
6
+ # 8. Menus, i18n, validate, publish
7
+
8
+ ## The menu
9
+
10
+ `spk-assembly/metadata/menu/office-equipment-menu.json`
11
+ ([real file](./example-plugin/spk-assembly/metadata/menu/office-equipment-menu.json)) —
12
+ one group node `Office Equipment` with three children pointing at the three
13
+ pages' `route.pattern` values, `modules: ["office-equipment"]`.
14
+
15
+ Because this is a **standalone** plugin (it ships its own application, chapter
16
+ 1), there is no master menu scaffold to keep in sync — one menu file is enough.
17
+
18
+ ## The translations
19
+
20
+ `spk-assembly/metadata/i18n/en.json`
21
+ ([real file](./example-plugin/spk-assembly/metadata/i18n/en.json)) — a flat map
22
+ of every i18n key the three pages, the menu, the app and the module reference
23
+ (~70 keys):
24
+
25
+ ```json
26
+ {
27
+ "office-equipment.application.title": "Office Equipment",
28
+ "office-equipment.module.title": "Equipment",
29
+ "office-equipment.equipment-register.title": "Equipment Register",
30
+ "office-equipment.equipment-register.newBtn": "New",
31
+ "office-equipment.equipment-register.column.asset_tag": "Asset Tag",
32
+ "office-equipment.equipment-checkout.status.OVERDUE": "Overdue",
33
+ "office-equipment.equipment-checkout.status.DUE_SOON": "Due Soon"
34
+ }
35
+ ```
36
+
37
+ A missing key is caught by `erp plugin test` as `I18N-001`.
38
+
39
+ ## Validate everything
40
+
41
+ ```bash
42
+ erp plugin validate example-plugin/spk-assembly
43
+ erp plugin test example-plugin/spk-assembly
44
+ ```
45
+
46
+ ```
47
+ validated 3 page(s) — clean
48
+
49
+ Plugin Tests
50
+ ────────────────────────────────────────
51
+ ✓ page:equipment-catalog.json
52
+ ✓ page:equipment-checkout.json
53
+ ✓ page:equipment-register.json
54
+ ✓ plugin.json:valid-json
55
+ 4 passed, 0 failed, 0 semantic warning(s), 3 skipped
56
+ ```
57
+
58
+ And the docs example runner, which also schema-validates every artifact:
59
+
60
+ ```bash
61
+ node developer-docs/examples/test-examples.mjs
62
+ # ... All examples valid.
63
+ ```
64
+
65
+ ## Build
66
+
67
+ ```bash
68
+ erp plugin build example-plugin/spk-assembly -o example-plugin/office-equipment-1.0.0.spk
69
+ ```
70
+
71
+ ```
72
+ validated 3 page(s) — clean
73
+ packaged 33 files -> example-plugin/office-equipment-1.0.0.spk (231986 bytes)
74
+ sha256: ...
75
+ ```
76
+
77
+ ## Publish
78
+
79
+ ```bash
80
+ erp plugin publish example-plugin/office-equipment-1.0.0.spk --tenant 2
81
+ ```
82
+
83
+ ```
84
+ installed:
85
+ {"pluginId":"office-equipment","version":"1.0.0","state":"installed","pf4jState":"STARTED", ...}
86
+ ```
87
+
88
+ ## Confirm it's all live
89
+
90
+ ```bash
91
+ # pages
92
+ $ erp api get "/api/v1/authoring/pages?module=office-equipment"
93
+ [ {"name":"equipment-catalog", "route":{"pattern":"/office-equipment/equipment-catalog"}, ...},
94
+ {"name":"equipment-checkout", ...}, {"name":"equipment-register", ...} ]
95
+
96
+ # seeded reference data
97
+ $ erp api get "/api/v1/entities/oeq_category/records/query?size=10"
98
+ {"rows":[{"category_code":"LAPTOP", ...}, ... ],"total":4}
99
+
100
+ # the reminder job
101
+ $ erp api get "/api/v1/jobs/engine-entity.status-date-sweep"
102
+ {"status":"ENABLED", ...}
103
+
104
+ # the approval rule
105
+ $ erp api get "/api/v1/entity-rules?entityType=oeq_checkout"
106
+ [{"name":"oeq_checkout_submit_workflow","active":true, ...}]
107
+ ```
108
+
109
+ Open `/app/office-equipment/equipment/office-equipment/equipment-register` in
110
+ your ERP. The module is complete: three working pages, seeded categories, an
111
+ approval workflow, and a nightly reminder sweep — and you wrote one line of Java.
112
+
113
+ ## Iterating from here
114
+
115
+ To change anything: edit the file, **bump `plugin.json` `version`**, rebuild,
116
+ re-publish. Don't uninstall to iterate — see
117
+ [Publish and upgrade](../guides/publish-and-upgrade.md) for why.
118
+
119
+ ## Where to go next
120
+
121
+ - [Guides](../guides/index.md) — every task as its own page
122
+ - [Recipes](../recipes/index.md) — copy-paste solutions
123
+ - [Reference](../reference/index.md) — every schema
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: "Tutorial: build the Office Equipment module"
3
+ audience: tenant
4
+ ---
5
+
6
+ # Tutorial: build the Office Equipment module
7
+
8
+ By the end of this tutorial you will have built, from an empty folder, a small
9
+ real module and published it to your ERP. Every step is something you run, and
10
+ every result shown here was produced by actually running it against a live ERP
11
+ (tenant 2).
12
+
13
+ ## What you're building
14
+
15
+ **Office Equipment** — a 3-page module for tracking company laptops, monitors and
16
+ phones, and who has them.
17
+
18
+ | Page | Backed by | Demonstrates |
19
+ | --- | --- | --- |
20
+ | **Equipment Catalog** | `oeq_category` entity, **seeded** with 4 categories on install | an entity + install-time reference data |
21
+ | **Equipment Register** | `oeq_equipment` entity | a full list page: header, KPI row, search/filter, grid, create dialog, a category **lookup** field |
22
+ | **Equipment Checkout** | `oeq_checkout` entity | a **reference-flagged** employee field (auto `_label`), a create dialog, a detail dialog with a **"Submit for approval"** action |
23
+
24
+ Plus, with no Java:
25
+
26
+ - a **workflow** — a submitted checkout needs manager approval before it's
27
+ `APPROVED`;
28
+ - a **scheduled reminder job** — a `CHECKED_OUT` checkout past its due date is
29
+ swept to `OVERDUE`, and one within 3 days to `DUE_SOON`;
30
+ - **install seeding** — the 4 categories and the 2 sweep configs travel with the
31
+ `.spk`;
32
+ - **menus** and **i18n**.
33
+
34
+ ## What it looks like at the end
35
+
36
+ ```
37
+ $ erp plugin publish office-equipment-1.0.2.spk --tenant 2
38
+ installed:
39
+ {"pluginId":"office-equipment","version":"1.0.2","state":"installed","pf4jState":"STARTED", ...}
40
+
41
+ $ erp api get "/api/v1/authoring/pages?module=office-equipment"
42
+ [ {"name":"equipment-catalog", ...}, {"name":"equipment-checkout", ...}, {"name":"equipment-register", ...} ]
43
+
44
+ $ erp api get "/api/v1/entities/oeq_category/records/query?size=10"
45
+ {"rows":[{"category_code":"LAPTOP","category_name":"Laptops", ...}, ... ],"total":4}
46
+ ```
47
+
48
+ Open `/app/office-equipment/equipment/office-equipment/equipment-register` in the
49
+ ERP and the module is there.
50
+
51
+ ## The finished source
52
+
53
+ The whole module lives at
54
+ [`example-plugin/`](./example-plugin/) in this docs tree — read any file as you
55
+ go, or diff yours against it. It is the same module the guides reference.
56
+
57
+ ## Chapters
58
+
59
+ 1. [Create the plugin](./01-create-the-plugin.md) — the scaffold, the manifest, the one Java class, the application/module
60
+ 2. [The equipment category entity + seeded data](./02-category-entity-and-seed.md)
61
+ 3. [The equipment register entity](./03-equipment-entity.md)
62
+ 4. [The equipment register page](./04-equipment-register-page.md) — header, KPIs, filter, grid, create dialog
63
+ 5. [The checkout entity, page, and lookups](./05-checkout-entity-and-page.md)
64
+ 6. [The checkout approval workflow](./06-checkout-approval-workflow.md)
65
+ 7. [The return-due reminder job](./07-return-due-reminder-job.md)
66
+ 8. [Menus, i18n, validate, publish](./08-menus-i18n-publish.md)
67
+
68
+ ## Before you start
69
+
70
+ Work through [Set up the SDK](../guides/set-up-the-sdk.md) first — you need
71
+ `erp whoami` returning `"valid": true`. This tutorial assumes the `erp` command
72
+ is on your path (or aliased to `node .../tools/erp-cli/erp.mjs`).
@@ -0,0 +1,18 @@
1
+ {
2
+ "entity": "entity_aggregation_config",
3
+ "keyFields": ["sweep_code"],
4
+ "source": "hcm-assets",
5
+ "rows": [
6
+ {"sweep_code": "asset-inventory-reconciliation", "source_entity": "hcm_asset", "aggregation": "count", "group_by_field": "status", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "asset_status:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
7
+ {"sweep_code": "asset-allocation-reconciliation", "source_entity": "hcm_asset_allocation", "aggregation": "count", "group_by_field": "status", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "allocation_status:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
8
+ {"sweep_code": "asset-return-reconciliation", "source_entity": "hcm_asset_return", "aggregation": "count", "group_by_field": "status", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "return_status:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
9
+ {"sweep_code": "asset-audit-reconciliation", "source_entity": "hcm_asset_audit", "aggregation": "count", "group_by_field": "status", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "audit_status:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
10
+ {"sweep_code": "maintenance-history-aggregation", "source_entity": "hcm_asset_maintenance", "aggregation": "count", "group_by_field": "asset_id", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "maint_events_asset:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
11
+ {"sweep_code": "maintenance-cost-sync", "source_entity": "hcm_asset_maintenance_cost", "aggregation": "sum", "agg_field": "total_cost", "group_by_field": "maintenance_id", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "maint_cost:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
12
+ {"sweep_code": "software-license-usage-sync", "source_entity": "hcm_software_license_assignment", "aggregation": "count", "when_status_in": "ASSIGNED,ACTIVE", "status_field": "status", "group_by_field": "license_id", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "license_seats_used:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
13
+ {"sweep_code": "software-license-cost-sync", "source_entity": "hcm_software_license", "aggregation": "sum", "agg_field": "cost_amount", "group_by_field": "status", "target_entity": "hcm_asset_reconciliation", "target_key_field": "metric_key", "target_key_prefix": "license_cost_status:", "target_value_field": "metric_value", "target_timestamp_field": "last_reconciled_at", "seeded_by": "hcm-assets", "active": true},
14
+ {"sweep_code": "maintenance-cost-rollup", "source_entity": "hcm_asset_maintenance_cost", "aggregation": "sum", "agg_field": "total_cost", "group_by_field": "maintenance_id", "target_entity": "hcm_asset_maintenance", "target_key_field": "id", "target_value_field": "synced_cost", "target_timestamp_field": "cost_synced_at", "seeded_by": "hcm-assets", "active": true},
15
+ {"sweep_code": "predictive-maintenance-event-count", "source_entity": "hcm_asset_maintenance", "aggregation": "count", "date_field": "scheduled_start", "date_compare_op": "gte", "day_offset": -180, "group_by_field": "asset_id", "target_entity": "hcm_asset", "target_key_field": "id", "target_value_field": "recent_maintenance_count", "target_timestamp_field": "maintenance_risk_scored_at", "seeded_by": "hcm-assets", "active": true},
16
+ {"sweep_code": "predictive-open-maintenance-count", "source_entity": "hcm_asset_maintenance", "aggregation": "count", "when_status_in": "REQUESTED,PENDING_APPROVAL,APPROVED,SCHEDULED,IN_PROGRESS,WAITING_FOR_PARTS", "group_by_field": "asset_id", "target_entity": "hcm_asset", "target_key_field": "id", "target_value_field": "open_maintenance_count", "seeded_by": "hcm-assets", "active": true}
17
+ ]
18
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "entity": "entity_cadence_config",
3
+ "keyFields": ["cadence_code"],
4
+ "source": "hcm-assets",
5
+ "rows": [
6
+ {"cadence_code": "asset-audit-schedule", "target_entity": "hcm_asset_audit", "interval_days": 90, "dedupe_target_field": "cadence_marker", "template_json": {"audit_number": "AUDIT-${date}-${ts}", "audit_name": "Scheduled Quarterly Asset Audit ${date}", "audit_type": "QUARTERLY", "status": "DRAFT"}, "seeded_by": "hcm-assets", "active": true},
7
+ {"cadence_code": "preventive-maintenance-generation", "target_entity": "hcm_asset_maintenance", "interval_days": 1, "source_entity": "hcm_asset", "source_status_field": "status", "source_status_in": "AVAILABLE,ASSIGNED,IN_USE", "source_date_field": "next_pm_date", "due_within_days": 7, "dedupe_target_field": "pm_source_asset_id", "field_map_json": {"asset_id": "id", "asset_number": "asset_number", "pm_source_asset_id": "id"}, "template_json": {"work_order_number": "PM-${sourceId}-${ts}", "maintenance_type": "PREVENTIVE", "priority": "MEDIUM", "problem_title": "Scheduled preventive maintenance", "status": "REQUESTED"}, "seeded_by": "hcm-assets", "active": true}
8
+ ]
9
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "entity": "entity_compliance_config",
3
+ "keyFields": ["compliance_code"],
4
+ "source": "hcm-assets",
5
+ "rows": [
6
+ {
7
+ "compliance_code": "asset-assignment-compliance",
8
+ "source_entity": "hcm_asset",
9
+ "when_status_in": "ASSIGNED,IN_USE",
10
+ "condition_expr": "custody_acknowledged != true",
11
+ "set_field": "compliance_flag",
12
+ "set_value": "NON_COMPLIANT",
13
+ "seeded_by": "hcm-assets",
14
+ "active": true
15
+ },
16
+ {
17
+ "compliance_code": "asset-allocation-policy-compliance",
18
+ "source_entity": "hcm_asset_allocation",
19
+ "when_status_in": "ALLOCATED,ACKNOWLEDGED,IN_USE",
20
+ "condition_expr": "allocation_type == \"PERMANENT\" && agreement_file_ref_id == null",
21
+ "set_field": "compliance_flag",
22
+ "set_value": "POLICY_VIOLATION",
23
+ "seeded_by": "hcm-assets",
24
+ "active": true
25
+ },
26
+ {
27
+ "compliance_code": "software-license-compliance",
28
+ "source_entity": "hcm_software_license",
29
+ "when_status_in": "ACTIVE,EXPIRING",
30
+ "related_count_entity": "hcm_software_license_assignment",
31
+ "related_count_key_field": "license_id",
32
+ "related_count_status_field": "status",
33
+ "related_count_status_in": "ASSIGNED,ACTIVE",
34
+ "condition_expr": "related_count > entitlement_quantity",
35
+ "set_field": "compliance_flag",
36
+ "set_value": "OVER_ALLOCATED",
37
+ "seeded_by": "hcm-assets",
38
+ "active": true
39
+ },
40
+ {
41
+ "compliance_code": "software-license-unused-detection",
42
+ "source_entity": "hcm_software_license_assignment",
43
+ "when_status_in": "ASSIGNED,ACTIVE",
44
+ "condition_expr": "(last_used_date == null && daysBetween(start_date, now()) > 90) || (last_used_date != null && daysBetween(last_used_date, now()) > 90)",
45
+ "set_field": "usage_flag",
46
+ "set_value": "UNUSED",
47
+ "seeded_by": "hcm-assets",
48
+ "active": true
49
+ },
50
+ {
51
+ "compliance_code": "asset-audit-exception-escalation",
52
+ "source_entity": "hcm_asset_return_exception",
53
+ "when_status_in": "OPEN,IN_PROGRESS",
54
+ "condition_expr": "resolution_due_date != null && daysBetween(resolution_due_date, now()) > 0",
55
+ "escalation_entity": "hcm_asset_compliance_escalation",
56
+ "escalation_dedupe_field": "source_record_id",
57
+ "escalation_timestamp_field": "raised_at",
58
+ "field_map_json": {"severity": "severity", "details": "description"},
59
+ "escalation_template_json": {"source_entity": "hcm_asset_return_exception", "escalation_type": "RESOLUTION_OVERDUE", "status": "OPEN"},
60
+ "seeded_by": "hcm-assets",
61
+ "active": true
62
+ },
63
+ {
64
+ "compliance_code": "predictive-maintenance-risk",
65
+ "source_entity": "hcm_asset",
66
+ "when_status_in": "AVAILABLE,ASSIGNED,IN_USE",
67
+ "condition_expr": "coalesce(recent_maintenance_count, 0) >= 3 || coalesce(open_maintenance_count, 0) >= 2",
68
+ "set_field": "maintenance_risk",
69
+ "set_value": "HIGH",
70
+ "seeded_by": "hcm-assets",
71
+ "active": true
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "entity": "entity_cross_plugin_action_config",
3
+ "keyFields": ["action_code"],
4
+ "source": "hcm-assets",
5
+ "rows": [
6
+ {"action_code": "asset-separation-return", "trigger_entity": "employee", "trigger_status_field": "employment_status", "trigger_status_in": "TERMINATED,SEPARATED", "trigger_key_field": "id", "related_entity": "hcm_asset_allocation", "related_match_field": "employee_id", "related_status_field": "status", "related_status_in": "ALLOCATED,ACKNOWLEDGED,IN_USE", "create_entity": "hcm_asset_return", "create_dedupe_field": "allocation_id", "create_template_json": {"return_number": "ASR-${relatedId}-${ts}", "status": "REQUESTED", "reason": "EMPLOYEE_SEPARATION"}, "create_field_map_json": {"asset_id": "asset_id", "employee_id": "employee_id"}, "related_set_field": "status", "related_set_value": "RETURN_PENDING", "seeded_by": "hcm-assets", "active": true},
7
+ {"action_code": "software-license-reclamation", "trigger_entity": "employee", "trigger_status_field": "employment_status", "trigger_status_in": "TERMINATED,SEPARATED", "trigger_key_field": "id", "related_entity": "hcm_software_license_assignment", "related_match_field": "employee_id", "related_status_field": "status", "related_status_in": "ASSIGNED,ACTIVE", "related_set_field": "status", "related_set_value": "RECLAMATION_PENDING", "seeded_by": "hcm-assets", "active": true}
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "entity": "entity_document_generator_config",
3
+ "keyFields": ["document_code"],
4
+ "source": "hcm-assets",
5
+ "rows": [
6
+ {"document_code": "asset-audit-report", "source_entity": "hcm_asset_audit", "status_field": "status", "when_status_in": "COMPLETED", "target_file_field": "report_file_ref_id", "format": "json", "cabinet_name": "HCM Assets Documents", "file_name_template": "asset-audit-report-${id}-${date}.json", "child_entity": "hcm_asset_audit_item", "child_match_field": "audit_id", "seeded_by": "hcm-assets", "active": true},
7
+ {"document_code": "asset-audit-certificate", "source_entity": "hcm_asset_audit", "status_field": "status", "when_status_in": "COMPLETED", "target_file_field": "certificate_file_ref_id", "format": "json", "cabinet_name": "HCM Assets Documents", "file_name_template": "asset-audit-certificate-${id}-${date}.json", "include_fields": "id,audit_number,audit_name,status,start_date,end_date,scope,location", "seeded_by": "hcm-assets", "active": true}
8
+ ]
9
+ }