Commit 32942f6e4f14232a6d5c1b87e784d95d84948103
1 parent
6789d26b
docs: README, CONTRIBUTING, LICENSE, full docs site, architecture spec, implemen…
…tation plan, updated CLAUDE.md
Showing
12 changed files
with
1872 additions
and
0 deletions
CONTRIBUTING.md
0 → 100644
| 1 | +# Contributing to vibe_erp | ||
| 2 | + | ||
| 3 | +vibe_erp is a framework that other developers will build on. The rules below exist so the framework stays reusable across customers and upgrade-safe across releases. They are not negotiable. | ||
| 4 | + | ||
| 5 | +For the full architectural reasoning, see [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and `CLAUDE.md`. | ||
| 6 | + | ||
| 7 | +## The dependency rule | ||
| 8 | + | ||
| 9 | +The Gradle build enforces this. CI fails on violations. | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only) | ||
| 13 | +platform/* depends on: api/api-v1 + Spring + libs | ||
| 14 | +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc) | ||
| 15 | +plugins (incl. ref) depend on: api/api-v1 only | ||
| 16 | +``` | ||
| 17 | + | ||
| 18 | +Consequences: | ||
| 19 | + | ||
| 20 | +- **PBCs never import each other.** `pbc-orders-sales` cannot declare `pbc-inventory` as a dependency. Cross-PBC interaction goes through (a) the event bus or (b) service interfaces declared in `api.v1.ext.<pbc>`. | ||
| 21 | +- **Plug-ins only see `api.v1`.** Importing anything from `platform.*` or any PBC's internal package fails the plug-in linter at install time. | ||
| 22 | +- **Reaching into `internal` packages via reflection is Grade D** and rejected by the loader. | ||
| 23 | + | ||
| 24 | +## Adding to `api.v1` is a one-way door — don't | ||
| 25 | + | ||
| 26 | +`api.v1` is the only semver-governed surface in the codebase. Once a class, method, or behavior is in `api.v1`, removing or renaming it requires a major version bump and a deprecation window of at least one major release. | ||
| 27 | + | ||
| 28 | +When in doubt, **keep things out of `api.v1`**. It is much easier to grow the API deliberately later than to maintain a regretted symbol forever. | ||
| 29 | + | ||
| 30 | +Adding to `api.v1` is OK when: | ||
| 31 | + | ||
| 32 | +- The reference printing-shop plug-in (or a plausible second customer plug-in) cannot express its requirement without it. | ||
| 33 | +- The addition is a new type or a default-implemented method on an existing interface (binary-compatible). | ||
| 34 | +- The addition has a written KDoc contract and at least one consuming test. | ||
| 35 | + | ||
| 36 | +Adding to `api.v1` is **not** OK when: | ||
| 37 | + | ||
| 38 | +- It exists only to make one PBC easier to write — that belongs in `platform.*`. | ||
| 39 | +- It leaks an implementation detail (Hibernate type, Spring annotation, JPA entity). | ||
| 40 | +- It is "we might need it later" — wait until you do. | ||
| 41 | + | ||
| 42 | +## Commit message convention | ||
| 43 | + | ||
| 44 | +Conventional Commits, scoped by module: | ||
| 45 | + | ||
| 46 | +``` | ||
| 47 | +feat(pbc-identity): add SCIM provisioning endpoint | ||
| 48 | +fix(api-v1): tolerate missing locale on Translator.format | ||
| 49 | +chore(platform-plugins): bump PF4J to 3.x | ||
| 50 | +docs(architecture): clarify outbox seam | ||
| 51 | +test(pbc-catalog): cover JSONB ext field round-trip | ||
| 52 | +refactor(platform-persistence): extract tenant filter | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +Allowed types: `feat`, `fix`, `chore`, `docs`, `test`, `refactor`, `build`, `ci`, `perf`, `revert`. Scope is the module name. Breaking changes use `!` after the scope (e.g. `feat(api-v1)!: …`) and require a `BREAKING CHANGE:` footer. | ||
| 56 | + | ||
| 57 | +## Pull request checklist | ||
| 58 | + | ||
| 59 | +Every PR must satisfy all of these: | ||
| 60 | + | ||
| 61 | +- [ ] Tests cover the new behavior. New PBC code has unit tests; new endpoints have integration tests against a real Postgres. | ||
| 62 | +- [ ] Every public `api.v1` type has KDoc on the type and on every method, including parameter, return, and exception contracts. | ||
| 63 | +- [ ] Liquibase changesets ship with **rollback blocks**. CI rejects PRs without them. | ||
| 64 | +- [ ] Any user-facing string is referenced via an i18n key, never concatenated. Default `en-US` translation is added in the same PR. | ||
| 65 | +- [ ] No printing-specific terminology in `pbc/*` or `platform/*`. Printing concepts live in `reference-customer/plugin-printing-shop/`. | ||
| 66 | +- [ ] No new cross-PBC dependency. If you reached for one, design an `api.v1.ext.<pbc>` interface or a `DomainEvent` instead. | ||
| 67 | +- [ ] Commit messages follow Conventional Commits. | ||
| 68 | +- [ ] Documentation under `docs/` is updated in the same PR if you added or changed a public seam. | ||
| 69 | + | ||
| 70 | +## Build, test, and plug-in load commands | ||
| 71 | + | ||
| 72 | +```bash | ||
| 73 | +# Build everything | ||
| 74 | +./gradlew build | ||
| 75 | + | ||
| 76 | +# Run the full test suite | ||
| 77 | +./gradlew test | ||
| 78 | + | ||
| 79 | +# Run a single PBC's tests | ||
| 80 | +./gradlew :pbc:pbc-identity:test | ||
| 81 | + | ||
| 82 | +# Build the api-v1 jar (the contract that plug-ins consume) | ||
| 83 | +./gradlew :api:api-v1:jar | ||
| 84 | + | ||
| 85 | +# Build the reference plug-in | ||
| 86 | +./gradlew :reference-customer:plugin-printing-shop:jar | ||
| 87 | + | ||
| 88 | +# Boot the distribution and load the reference plug-in | ||
| 89 | +cp reference-customer/plugin-printing-shop/build/libs/*.jar /tmp/vibeerp/plugins/ | ||
| 90 | +./gradlew :distribution:bootRun | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +The plug-in loader logs every plug-in it scans, accepts, and rejects, with the reason. If a plug-in fails to load, the cause is in the boot log under the `platform-plugins` logger. | ||
| 94 | + | ||
| 95 | +## Style notes | ||
| 96 | + | ||
| 97 | +- Kotlin: idiomatic Kotlin, no `!!`, no `lateinit` outside test fixtures, prefer data classes for value objects. | ||
| 98 | +- Public API: KDoc is required, not optional. Internal helpers may skip KDoc but should still be self-explanatory. | ||
| 99 | +- No printing terminology in core, ever. "Item", "document", "operation", "work order" are generic. "Plate", "ink", "press", "color proof" are not. |
LICENSE
0 → 100644
| 1 | + Apache License | ||
| 2 | + Version 2.0, January 2004 | ||
| 3 | + http://www.apache.org/licenses/ | ||
| 4 | + | ||
| 5 | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||
| 6 | + | ||
| 7 | + 1. Definitions. | ||
| 8 | + | ||
| 9 | + "License" shall mean the terms and conditions for use, reproduction, | ||
| 10 | + and distribution as defined by Sections 1 through 9 of this document. | ||
| 11 | + | ||
| 12 | + "Licensor" shall mean the copyright owner or entity authorized by | ||
| 13 | + the copyright owner that is granting the License. | ||
| 14 | + | ||
| 15 | + "Legal Entity" shall mean the union of the acting entity and all | ||
| 16 | + other entities that control, are controlled by, or are under common | ||
| 17 | + control with that entity. For the purposes of this definition, | ||
| 18 | + "control" means (i) the power, direct or indirect, to cause the | ||
| 19 | + direction or management of such entity, whether by contract or | ||
| 20 | + otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||
| 21 | + outstanding shares, or (iii) beneficial ownership of such entity. | ||
| 22 | + | ||
| 23 | + "You" (or "Your") shall mean an individual or Legal Entity | ||
| 24 | + exercising permissions granted by this License. | ||
| 25 | + | ||
| 26 | + "Source" form shall mean the preferred form for making modifications, | ||
| 27 | + including but not limited to software source code, documentation | ||
| 28 | + source, and configuration files. | ||
| 29 | + | ||
| 30 | + "Object" form shall mean any form resulting from mechanical | ||
| 31 | + transformation or translation of a Source form, including but | ||
| 32 | + not limited to compiled object code, generated documentation, | ||
| 33 | + and conversions to other media types. | ||
| 34 | + | ||
| 35 | + "Work" shall mean the work of authorship, whether in Source or | ||
| 36 | + Object form, made available under the License, as indicated by a | ||
| 37 | + copyright notice that is included in or attached to the work | ||
| 38 | + (an example is provided in the Appendix below). | ||
| 39 | + | ||
| 40 | + "Derivative Works" shall mean any work, whether in Source or Object | ||
| 41 | + form, that is based on (or derived from) the Work and for which the | ||
| 42 | + editorial revisions, annotations, elaborations, or other modifications | ||
| 43 | + represent, as a whole, an original work of authorship. For the purposes | ||
| 44 | + of this License, Derivative Works shall not include works that remain | ||
| 45 | + separable from, or merely link (or bind by name) to the interfaces of, | ||
| 46 | + the Work and Derivative Works thereof. | ||
| 47 | + | ||
| 48 | + "Contribution" shall mean any work of authorship, including | ||
| 49 | + the original version of the Work and any modifications or additions | ||
| 50 | + to that Work or Derivative Works thereof, that is intentionally | ||
| 51 | + submitted to Licensor for inclusion in the Work by the copyright owner | ||
| 52 | + or by an individual or Legal Entity authorized to submit on behalf of | ||
| 53 | + the copyright owner. For the purposes of this definition, "submitted" | ||
| 54 | + means any form of electronic, verbal, or written communication sent | ||
| 55 | + to the Licensor or its representatives, including but not limited to | ||
| 56 | + communication on electronic mailing lists, source code control systems, | ||
| 57 | + and issue tracking systems that are managed by, or on behalf of, the | ||
| 58 | + Licensor for the purpose of discussing and improving the Work, but | ||
| 59 | + excluding communication that is conspicuously marked or otherwise | ||
| 60 | + designated in writing by the copyright owner as "Not a Contribution." | ||
| 61 | + | ||
| 62 | + "Contributor" shall mean Licensor and any individual or Legal Entity | ||
| 63 | + on behalf of whom a Contribution has been received by Licensor and | ||
| 64 | + subsequently incorporated within the Work. | ||
| 65 | + | ||
| 66 | + 2. Grant of Copyright License. Subject to the terms and conditions of | ||
| 67 | + this License, each Contributor hereby grants to You a perpetual, | ||
| 68 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
| 69 | + copyright license to reproduce, prepare Derivative Works of, | ||
| 70 | + publicly display, publicly perform, sublicense, and distribute the | ||
| 71 | + Work and such Derivative Works in Source or Object form. | ||
| 72 | + | ||
| 73 | + 3. Grant of Patent License. Subject to the terms and conditions of | ||
| 74 | + this License, each Contributor hereby grants to You a perpetual, | ||
| 75 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||
| 76 | + (except as stated in this section) patent license to make, have made, | ||
| 77 | + use, offer to sell, sell, import, and otherwise transfer the Work, | ||
| 78 | + where such license applies only to those patent claims licensable | ||
| 79 | + by such Contributor that are necessarily infringed by their | ||
| 80 | + Contribution(s) alone or by combination of their Contribution(s) | ||
| 81 | + with the Work to which such Contribution(s) was submitted. If You | ||
| 82 | + institute patent litigation against any entity (including a | ||
| 83 | + cross-claim or counterclaim in a lawsuit) alleging that the Work | ||
| 84 | + or a Contribution incorporated within the Work constitutes direct | ||
| 85 | + or contributory patent infringement, then any patent licenses | ||
| 86 | + granted to You under this License for that Work shall terminate | ||
| 87 | + as of the date such litigation is filed. | ||
| 88 | + | ||
| 89 | + 4. Redistribution. You may reproduce and distribute copies of the | ||
| 90 | + Work or Derivative Works thereof in any medium, with or without | ||
| 91 | + modifications, and in Source or Object form, provided that You | ||
| 92 | + meet the following conditions: | ||
| 93 | + | ||
| 94 | + (a) You must give any other recipients of the Work or | ||
| 95 | + Derivative Works a copy of this License; and | ||
| 96 | + | ||
| 97 | + (b) You must cause any modified files to carry prominent notices | ||
| 98 | + stating that You changed the files; and | ||
| 99 | + | ||
| 100 | + (c) You must retain, in the Source form of any Derivative Works | ||
| 101 | + that You distribute, all copyright, patent, trademark, and | ||
| 102 | + attribution notices from the Source form of the Work, | ||
| 103 | + excluding those notices that do not pertain to any part of | ||
| 104 | + the Derivative Works; and | ||
| 105 | + | ||
| 106 | + (d) If the Work includes a "NOTICE" text file as part of its | ||
| 107 | + distribution, then any Derivative Works that You distribute must | ||
| 108 | + include a readable copy of the attribution notices contained | ||
| 109 | + within such NOTICE file, excluding those notices that do not | ||
| 110 | + pertain to any part of the Derivative Works, in at least one | ||
| 111 | + of the following places: within a NOTICE text file distributed | ||
| 112 | + as part of the Derivative Works; within the Source form or | ||
| 113 | + documentation, if provided along with the Derivative Works; or, | ||
| 114 | + within a display generated by the Derivative Works, if and | ||
| 115 | + wherever such third-party notices normally appear. The contents | ||
| 116 | + of the NOTICE file are for informational purposes only and | ||
| 117 | + do not modify the License. You may add Your own attribution | ||
| 118 | + notices within Derivative Works that You distribute, alongside | ||
| 119 | + or as an addendum to the NOTICE text from the Work, provided | ||
| 120 | + that such additional attribution notices cannot be construed | ||
| 121 | + as modifying the License. | ||
| 122 | + | ||
| 123 | + You may add Your own copyright statement to Your modifications and | ||
| 124 | + may provide additional or different license terms and conditions | ||
| 125 | + for use, reproduction, or distribution of Your modifications, or | ||
| 126 | + for any such Derivative Works as a whole, provided Your use, | ||
| 127 | + reproduction, and distribution of the Work otherwise complies with | ||
| 128 | + the conditions stated in this License. | ||
| 129 | + | ||
| 130 | + 5. Submission of Contributions. Unless You explicitly state otherwise, | ||
| 131 | + any Contribution intentionally submitted for inclusion in the Work | ||
| 132 | + by You to the Licensor shall be under the terms and conditions of | ||
| 133 | + this License, without any additional terms or conditions. | ||
| 134 | + Notwithstanding the above, nothing herein shall supersede or modify | ||
| 135 | + the terms of any separate license agreement you may have executed | ||
| 136 | + with Licensor regarding such Contributions. | ||
| 137 | + | ||
| 138 | + 6. Trademarks. This License does not grant permission to use the trade | ||
| 139 | + names, trademarks, service marks, or product names of the Licensor, | ||
| 140 | + except as required for describing the origin of the Work and | ||
| 141 | + reproducing the content of the NOTICE file. | ||
| 142 | + | ||
| 143 | + 7. Disclaimer of Warranty. Unless required by applicable law or | ||
| 144 | + agreed to in writing, Licensor provides the Work (and each | ||
| 145 | + Contributor provides its Contributions) on an "AS IS" BASIS, | ||
| 146 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||
| 147 | + implied, including, without limitation, any warranties or conditions | ||
| 148 | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||
| 149 | + PARTICULAR PURPOSE. You are solely responsible for determining the | ||
| 150 | + appropriateness of using or redistributing the Work and assume any | ||
| 151 | + risks associated with Your exercise of permissions under this License. | ||
| 152 | + | ||
| 153 | + 8. Limitation of Liability. In no event and under no legal theory, | ||
| 154 | + whether in tort (including negligence), contract, or otherwise, | ||
| 155 | + unless required by applicable law (such as deliberate and grossly | ||
| 156 | + negligent acts) or agreed to in writing, shall any Contributor be | ||
| 157 | + liable to You for damages, including any direct, indirect, special, | ||
| 158 | + incidental, or consequential damages of any character arising as a | ||
| 159 | + result of this License or out of the use or inability to use the | ||
| 160 | + Work (including but not limited to damages for loss of goodwill, | ||
| 161 | + work stoppage, computer failure or malfunction, or any and all | ||
| 162 | + other commercial damages or losses), even if such Contributor | ||
| 163 | + has been advised of the possibility of such damages. | ||
| 164 | + | ||
| 165 | + 9. Accepting Warranty or Additional Liability. While redistributing | ||
| 166 | + the Work or Derivative Works thereof, You may choose to offer, | ||
| 167 | + and charge a fee for, acceptance of support, warranty, indemnity, | ||
| 168 | + or other liability obligations and/or rights consistent with this | ||
| 169 | + License. However, in accepting such obligations, You may act only | ||
| 170 | + on Your own behalf and on Your sole responsibility, not on behalf | ||
| 171 | + of any other Contributor, and only if You agree to indemnify, | ||
| 172 | + defend, and hold each Contributor harmless for any liability | ||
| 173 | + incurred by, or claims asserted against, such Contributor by reason | ||
| 174 | + of your accepting any such warranty or additional liability. | ||
| 175 | + | ||
| 176 | + END OF TERMS AND CONDITIONS | ||
| 177 | + | ||
| 178 | + APPENDIX: How to apply the Apache License to your work. | ||
| 179 | + | ||
| 180 | + To apply the Apache License to your work, attach the following | ||
| 181 | + boilerplate notice, with the fields enclosed by brackets "[]" | ||
| 182 | + replaced with your own identifying information. (Don't include | ||
| 183 | + the brackets!) The text should be enclosed in the appropriate | ||
| 184 | + comment syntax for the file format. We also recommend that a | ||
| 185 | + file or class name and description of purpose be included on the | ||
| 186 | + same "printed page" as the copyright notice for easier | ||
| 187 | + identification within third-party archives. | ||
| 188 | + | ||
| 189 | + Copyright [yyyy] [name of copyright owner] | ||
| 190 | + | ||
| 191 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 192 | + you may not use this file except in compliance with the License. | ||
| 193 | + You may obtain a copy of the License at | ||
| 194 | + | ||
| 195 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
| 196 | + | ||
| 197 | + Unless required by applicable law or agreed to in writing, software | ||
| 198 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
| 199 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||
| 200 | + implied. See the License for the specific language governing | ||
| 201 | + permissions and limitations under the License. |
README.md
0 → 100644
| 1 | +# vibe_erp | ||
| 2 | + | ||
| 3 | +vibe_erp is an ERP/EBC framework for the printing industry, sold worldwide and deployed self-hosted-first. It is not an ERP application: it is the substrate on which any printing shop's workflows, forms, roles, and rules can be assembled by configuration and plug-ins instead of by forking the core. The reference business under `raw/` is one example customer, used as an executable acceptance test, never as the spec. | ||
| 4 | + | ||
| 5 | +## Why a framework, not an app | ||
| 6 | + | ||
| 7 | +Printing shops differ in process, terminology, paperwork, and order of operations. An app that hard-codes one shop's workflow becomes a fork farm the moment a second customer signs up. vibe_erp follows the **Clean Core** philosophy borrowed from SAP S/4HANA: the core stays generic and upgrade-safe, and every customer-specific concept lives in metadata rows or plug-ins. The core never knows what a "plate" or a "press" is — those concepts are introduced by a plug-in. Upgrading the core does not touch the customer's extensions. | ||
| 8 | + | ||
| 9 | +## Architecture in one picture | ||
| 10 | + | ||
| 11 | +``` | ||
| 12 | +┌──────────────────────────────────────────────────────────────────────┐ | ||
| 13 | +│ Customer's network │ | ||
| 14 | +│ │ | ||
| 15 | +│ Browser (React SPA) ─┐ │ | ||
| 16 | +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│ | ||
| 17 | +│ 3rd-party system ─┘ │ │ | ||
| 18 | +│ │ │ | ||
| 19 | +│ Inside the image (one Spring Boot process): │ │ | ||
| 20 | +│ ┌─────────────────────────────────────┐ │ │ | ||
| 21 | +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │ | ||
| 22 | +│ ├─────────────────────────────────────┤ │ │ | ||
| 23 | +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │ | ||
| 24 | +│ │ — the only stable contract │ │ ./plugins/*.jar │ | ||
| 25 | +│ ├─────────────────────────────────────┤ │ via PF4J │ | ||
| 26 | +│ │ Core PBCs (modular monolith): │ │ │ | ||
| 27 | +│ │ identity · catalog · partners · │ │ │ | ||
| 28 | +│ │ inventory · warehousing · │ │ │ | ||
| 29 | +│ │ orders-sales · orders-purchase · │ │ │ | ||
| 30 | +│ │ production · quality · finance │ │ │ | ||
| 31 | +│ ├─────────────────────────────────────┤ │ │ | ||
| 32 | +│ │ Cross-cutting: │ │ │ | ||
| 33 | +│ │ • Flowable (workflows-as-data) │ │ │ | ||
| 34 | +│ │ • Metadata store (Doctype-style) │ │ │ | ||
| 35 | +│ │ • i18n (ICU MessageFormat) │ │ │ | ||
| 36 | +│ │ • Reporting (JasperReports) │ │ │ | ||
| 37 | +│ │ • Job scheduler (Quartz) │ │ │ | ||
| 38 | +│ │ • Audit, security, events │ │ │ | ||
| 39 | +│ └─────────────────────────────────────┘ │ │ | ||
| 40 | +│ ▼ │ | ||
| 41 | +│ PostgreSQL (mandatory) │ | ||
| 42 | +│ File store (local or S3) │ | ||
| 43 | +└──────────────────────────────────────────────────────────────────────┘ | ||
| 44 | + | ||
| 45 | +Optional sidecars for larger deployments (off by default): | ||
| 46 | + • Keycloak (OIDC) • Redis (cache + queue) | ||
| 47 | + • OpenSearch (search) • SMTP relay | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +The full architecture lives at [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 51 | + | ||
| 52 | +## Stack | ||
| 53 | + | ||
| 54 | +- Kotlin on the JVM, Spring Boot, single fat-JAR / single Docker image | ||
| 55 | +- PostgreSQL (the only mandatory external dependency) | ||
| 56 | +- Embedded Flowable (BPMN 2.0) for workflows-as-data | ||
| 57 | +- PF4J + Spring Boot child contexts for plug-in classloader isolation | ||
| 58 | +- ICU4J + Spring `MessageSource` for i18n | ||
| 59 | +- JasperReports for reporting | ||
| 60 | +- React + TypeScript SPA for the web client | ||
| 61 | + | ||
| 62 | +## Repository layout | ||
| 63 | + | ||
| 64 | +``` | ||
| 65 | +vibe-erp/ | ||
| 66 | +├── api/ | ||
| 67 | +│ └── api-v1/ ← THE CONTRACT (semver, published to Maven Central) | ||
| 68 | +├── platform/ ← Framework runtime (internal) | ||
| 69 | +├── pbc/ ← Core PBCs (one Gradle subproject each) | ||
| 70 | +├── reference-customer/ | ||
| 71 | +│ └── plugin-printing-shop/ ← Reference plug-in, built and CI-tested, not loaded by default | ||
| 72 | +├── web/ ← React + TypeScript SPA | ||
| 73 | +├── docs/ ← Framework documentation | ||
| 74 | +└── distribution/ ← Bootable assembly: fat JAR + Docker image | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +## Building | ||
| 78 | + | ||
| 79 | +This is a v0.1 skeleton. Not all modules ship yet — expect commands below to work for the modules that exist and to be no-ops or stubs for the rest. | ||
| 80 | + | ||
| 81 | +```bash | ||
| 82 | +# Build everything that currently exists | ||
| 83 | +./gradlew build | ||
| 84 | + | ||
| 85 | +# Run the bootable distribution against a local Postgres | ||
| 86 | +./gradlew :distribution:bootRun | ||
| 87 | + | ||
| 88 | +# Or bring everything (Postgres + vibe_erp) up with Docker | ||
| 89 | +docker compose up | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +## Status | ||
| 93 | + | ||
| 94 | +**v0.1** — buildable skeleton, one PBC implemented end-to-end (`pbc-identity`), plug-in loading proven with a hello-world reference plug-in. Not production. See [`docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](docs/superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) for the full v1.0 plan. | ||
| 95 | + | ||
| 96 | +## Documentation | ||
| 97 | + | ||
| 98 | +- [Documentation index](docs/index.md) | ||
| 99 | +- [Architecture overview](docs/architecture/overview.md) | ||
| 100 | +- [Plug-in API overview](docs/plugin-api/overview.md) | ||
| 101 | +- [Plug-in author getting started](docs/plugin-author/getting-started.md) | ||
| 102 | +- [Workflow authoring guide](docs/workflow-authoring/guide.md) | ||
| 103 | +- [Form authoring guide](docs/form-authoring/guide.md) | ||
| 104 | +- [i18n guide](docs/i18n/guide.md) | ||
| 105 | +- [Customer onboarding guide](docs/customer-onboarding/guide.md) | ||
| 106 | + | ||
| 107 | +## License | ||
| 108 | + | ||
| 109 | +Apache License 2.0. See [`LICENSE`](LICENSE). |
docs/architecture/overview.md
0 → 100644
| 1 | +# Architecture overview | ||
| 2 | + | ||
| 3 | +This document distills the vibe_erp architecture for someone who has not read the full spec. The complete design lives at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). Read that when you need the full reasoning, the foundational decisions table, the v1.0 cut line, or the risk register. | ||
| 4 | + | ||
| 5 | +## What vibe_erp is | ||
| 6 | + | ||
| 7 | +vibe_erp is an **ERP/EBC framework** — not an ERP application — targeting the **printing industry**, sold worldwide and deployed self-hosted-first. The whole point of the project is **reusability across customers**: any printing shop, with its own workflows, forms, roles, and rules, should be onboardable by configuration and plug-ins, without forking the core. | ||
| 8 | + | ||
| 9 | +The reference printing-shop business documented under `raw/业务流程设计文档/` is **one example customer**, treated as a fixture and an executable acceptance test, never as a specification. No part of its workflow is hard-coded into the core. | ||
| 10 | + | ||
| 11 | +## Clean Core philosophy | ||
| 12 | + | ||
| 13 | +vibe_erp adopts SAP S/4HANA's **Clean Core** vocabulary: **extensions never modify the core**. The core is stable, generic, upgrade-safe, and domain-agnostic. Everything customer-specific lives in metadata rows or in plug-ins. This is enforced, not aspirational: | ||
| 14 | + | ||
| 15 | +1. **Core stays domain-agnostic.** No printing-specific entity, field, status, or workflow step belongs in the framework core. "Plate", "ink", "press", "color proof" live in plug-ins or in configuration. | ||
| 16 | +2. **Workflows are data, not code.** State machines, approval chains, and form definitions are declarative, so a new customer is onboarded by editing definitions, not source. | ||
| 17 | +3. **Extensibility seams come first.** Before any feature is added, the extension point it hangs off of must exist. If no seam exists, the seam is designed first. | ||
| 18 | +4. **The reference customer is a test, not a requirement.** Anything implemented for the reference plug-in must be expressible by a different printing shop with different rules, without code changes. | ||
| 19 | +5. **Multi-tenant in spirit from day one.** Even single-tenant deployments use the same multi-tenant code path. | ||
| 20 | +6. **Global / i18n from day one.** No hard-coded user-facing strings, currencies, date formats, time zones, address shapes, or tax models. | ||
| 21 | + | ||
| 22 | +## Two-tier extensibility | ||
| 23 | + | ||
| 24 | +vibe_erp offers **two extension paths**, both first-class. The same outcome can often be reached through either path; Tier 1 is preferred when expressive enough. | ||
| 25 | + | ||
| 26 | +### Tier 1 — Key user, no-code | ||
| 27 | + | ||
| 28 | +Business analysts customize the system through the web UI. Everything they create is stored as **rows in the metadata tables**, scoped to their tenant, tagged `source = 'user'`, and preserved across plug-in install/uninstall and core upgrades. | ||
| 29 | + | ||
| 30 | +| Capability | Stored in | | ||
| 31 | +|---|---| | ||
| 32 | +| Custom field on an existing entity | `metadata__custom_field` → JSONB `ext` column at runtime | | ||
| 33 | +| Custom form layout | `metadata__form` (JSON Schema + UI Schema) | | ||
| 34 | +| Custom list view, filter, column set | `metadata__list_view` | | ||
| 35 | +| Custom workflow | `metadata__workflow` → deployed to Flowable as BPMN | | ||
| 36 | +| Simple "if X then Y" automation | `metadata__rule` | | ||
| 37 | +| Custom entity (Doctype-style) | `metadata__entity` → auto-generated table at apply time | | ||
| 38 | +| Custom report | `metadata__report` | | ||
| 39 | +| Translations override | `metadata__translation` | | ||
| 40 | + | ||
| 41 | +No build, no restart, no deploy. The OpenAPI spec, the AI-agent function catalog, and the REST API auto-update from the metadata. | ||
| 42 | + | ||
| 43 | +### Tier 2 — Developer, pro-code | ||
| 44 | + | ||
| 45 | +Software developers ship a **PF4J plug-in JAR**. The plug-in: | ||
| 46 | + | ||
| 47 | +- Sees only `org.vibeerp.api.v1.*` — the public, semver-governed contract. | ||
| 48 | +- Cannot import `org.vibeerp.platform.*` or any PBC's internal classes. | ||
| 49 | +- Lives in its own classloader, its own Spring child context, its own DB schema namespace (`plugin_<id>__*`), its own metadata-source tag. | ||
| 50 | +- Can register: new entities, REST endpoints, workflow tasks, form widgets, report templates, event listeners, permissions, menu entries, and React micro-frontends. | ||
| 51 | + | ||
| 52 | +### Extension grading | ||
| 53 | + | ||
| 54 | +Borrowed from SAP. The plug-in loader knows these grades and acts on them. | ||
| 55 | + | ||
| 56 | +| Grade | Definition | Upgrade safety | | ||
| 57 | +|---|---|---| | ||
| 58 | +| **A** | Tier 1 only (metadata) | Always safe across any core version | | ||
| 59 | +| **B** | Tier 2, uses only `api.v1` stable surface | Safe within a major version | | ||
| 60 | +| **C** | Tier 2, uses deprecated-but-supported `api.v1` symbols | Safe until next major; loader emits warnings | | ||
| 61 | +| **D** | Tier 2, reaches into internal classes via reflection | UNSUPPORTED; loader rejects unless explicitly overridden; will break | | ||
| 62 | + | ||
| 63 | +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough. | ||
| 64 | + | ||
| 65 | +## Modular monolith of PBCs | ||
| 66 | + | ||
| 67 | +vibe_erp is a **modular monolith**: one Spring Boot process per instance, one Docker image per release, but internally divided into strict bounded contexts called **Packaged Business Capabilities (PBCs)**. Each PBC is its own Gradle subproject, its own table-name prefix, its own bounded context, its own public API surface. | ||
| 68 | + | ||
| 69 | +The v1.0 core PBCs: | ||
| 70 | + | ||
| 71 | +``` | ||
| 72 | +pbc-identity pbc-catalog pbc-partners | ||
| 73 | +pbc-inventory pbc-warehousing | ||
| 74 | +pbc-orders-sales pbc-orders-purchase | ||
| 75 | +pbc-production pbc-quality pbc-finance | ||
| 76 | +``` | ||
| 77 | + | ||
| 78 | +These names are **illustrative core capabilities**. None is printing-specific. The reference printing-shop plug-in lives entirely outside `pbc/` under `reference-customer/plugin-printing-shop/`, built and CI-tested but **not loaded by default**. | ||
| 79 | + | ||
| 80 | +### Per-PBC layout | ||
| 81 | + | ||
| 82 | +Every PBC follows the same shape: | ||
| 83 | + | ||
| 84 | +``` | ||
| 85 | +pbc-orders-sales/ | ||
| 86 | +├── api/ ← service contracts re-exported by api.v1 | ||
| 87 | +├── domain/ ← entities, value objects, domain services | ||
| 88 | +├── application/ ← use cases / application services | ||
| 89 | +├── infrastructure/ ← Hibernate mappings, repositories | ||
| 90 | +├── http/ ← REST controllers | ||
| 91 | +├── workflow/ ← BPMN files, task handlers | ||
| 92 | +├── metadata/ ← seed metadata (default forms, rules) | ||
| 93 | +├── i18n/ ← message bundles | ||
| 94 | +└── migrations/ ← Liquibase changesets (own table prefix) | ||
| 95 | +``` | ||
| 96 | + | ||
| 97 | +## The dependency rule | ||
| 98 | + | ||
| 99 | +The single rule that makes "modular monolith now, splittable later" real instead of aspirational. Enforced by the Gradle build; CI fails on violations. | ||
| 100 | + | ||
| 101 | +``` | ||
| 102 | +api/api-v1 depends on: nothing (Kotlin stdlib + jakarta.validation only) | ||
| 103 | +platform/* depends on: api/api-v1 + Spring + libs | ||
| 104 | +pbc/* depends on: api/api-v1 + platform/* (NEVER another pbc) | ||
| 105 | +plugins (incl. ref) depend on: api/api-v1 only | ||
| 106 | +``` | ||
| 107 | + | ||
| 108 | +**PBCs never import each other.** Cross-PBC interaction goes through one of two channels: | ||
| 109 | + | ||
| 110 | +1. **The event bus.** A PBC publishes a typed `DomainEvent`. Any other PBC (or any plug-in) subscribes via `EventListener`. The default bus is in-process; an outbox table in Postgres is the seam where Kafka or NATS can plug in later without changing PBC code. | ||
| 111 | +2. **Service interfaces declared in `api.v1.ext.<pbc>`.** When a synchronous call is genuinely needed, it goes through a typed interface in `api.v1.ext`, not through a direct class reference. | ||
| 112 | + | ||
| 113 | +If you find yourself wanting to add a `pbc-foo → pbc-bar` Gradle dependency, the seam is wrong. Design an event or an `api.v1.ext.bar` interface instead. | ||
| 114 | + | ||
| 115 | +## The `api.v1` contract | ||
| 116 | + | ||
| 117 | +`api.v1` is the **only stable contract** in the entire codebase. Everything in `org.vibeerp.api.v1.*` is binary-stable within the `1.x` line. Everything not in `api.v1` is internal and may change in any release. | ||
| 118 | + | ||
| 119 | +Package layout: | ||
| 120 | + | ||
| 121 | +``` | ||
| 122 | +org.vibeerp.api.v1 | ||
| 123 | +├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E> | ||
| 124 | +├── entity/ Entity, Field, FieldType, EntityRegistry | ||
| 125 | +├── persistence/ Repository<T>, Query, Page, Transaction | ||
| 126 | +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler | ||
| 127 | +├── form/ FormSchema, UiSchema | ||
| 128 | +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder | ||
| 129 | +├── event/ DomainEvent, EventListener, EventBus | ||
| 130 | +├── security/ Principal, Permission, PermissionCheck | ||
| 131 | +├── i18n/ MessageKey, Translator, LocaleProvider | ||
| 132 | +├── reporting/ ReportTemplate, ReportContext | ||
| 133 | +├── plugin/ Plugin, PluginManifest, ExtensionPoint | ||
| 134 | +└── ext/ Typed extension interfaces a plug-in implements | ||
| 135 | +``` | ||
| 136 | + | ||
| 137 | +`api.v1` is published as `api-v1.jar` to **Maven Central**, so plug-in authors can build against it without pulling the entire vibe_erp source tree. | ||
| 138 | + | ||
| 139 | +### Upgrade contract | ||
| 140 | + | ||
| 141 | +| Change | Allowed within 1.x? | | ||
| 142 | +|---|---| | ||
| 143 | +| Add a class to `api.v1` | yes | | ||
| 144 | +| Add a method to an `api.v1` interface (with default impl) | yes | | ||
| 145 | +| Remove or rename anything in `api.v1` | no — major bump | | ||
| 146 | +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump | | ||
| 147 | +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal | | ||
| 148 | + | ||
| 149 | +When in doubt, **keep things out of `api.v1`**. | ||
| 150 | + | ||
| 151 | +## Topology | ||
| 152 | + | ||
| 153 | +One Spring Boot process per instance, one Docker image per release, one mounted volume (`/opt/vibe-erp/`) for `config/`, `plugins/`, `i18n-overrides/`, `files/`, `logs/`. Customer extensions live **outside** the image. Upgrading vibe_erp = swapping the image; extensions and config stay put. | ||
| 154 | + | ||
| 155 | +``` | ||
| 156 | +┌──────────────────────────────────────────────────────────────────────┐ | ||
| 157 | +│ Customer's network │ | ||
| 158 | +│ │ | ||
| 159 | +│ Browser (React SPA) ─┐ │ | ||
| 160 | +│ AI agent (MCP, v1.1)─┼─► Reverse proxy ──► vibe_erp backend (1 image)│ | ||
| 161 | +│ 3rd-party system ─┘ │ │ | ||
| 162 | +│ │ │ | ||
| 163 | +│ Inside the image (one Spring Boot process): │ │ | ||
| 164 | +│ ┌─────────────────────────────────────┐ │ │ | ||
| 165 | +│ │ HTTP layer (REST + OpenAPI + MCP) │ │ │ | ||
| 166 | +│ ├─────────────────────────────────────┤ │ │ | ||
| 167 | +│ │ Public Plug-in API (api.v1.*) │◄──┤ loaded from │ | ||
| 168 | +│ │ — the only stable contract │ │ ./plugins/*.jar │ | ||
| 169 | +│ ├─────────────────────────────────────┤ │ via PF4J │ | ||
| 170 | +│ │ Core PBCs (modular monolith) │ │ │ | ||
| 171 | +│ ├─────────────────────────────────────┤ │ │ | ||
| 172 | +│ │ Cross-cutting: │ │ │ | ||
| 173 | +│ │ • Flowable (workflows-as-data) │ │ │ | ||
| 174 | +│ │ • Metadata store (Doctype-style) │ │ │ | ||
| 175 | +│ │ • i18n (ICU MessageFormat) │ │ │ | ||
| 176 | +│ │ • Reporting (JasperReports) │ │ │ | ||
| 177 | +│ │ • Job scheduler (Quartz) │ │ │ | ||
| 178 | +│ │ • Audit, security, events │ │ │ | ||
| 179 | +│ └─────────────────────────────────────┘ │ │ | ||
| 180 | +│ ▼ │ | ||
| 181 | +│ PostgreSQL (mandatory) │ | ||
| 182 | +│ File store (local or S3) │ | ||
| 183 | +└──────────────────────────────────────────────────────────────────────┘ | ||
| 184 | +``` | ||
| 185 | + | ||
| 186 | +The only mandatory external dependency is **PostgreSQL**. Optional sidecars for larger deployments — Keycloak, Redis, OpenSearch, SMTP relay — are off by default. | ||
| 187 | + | ||
| 188 | +## Multi-tenancy | ||
| 189 | + | ||
| 190 | +vibe_erp is **multi-tenant in spirit from day one**, even when deployed for a single customer. The same code path serves self-hosted single-tenant and hosted multi-tenant deployments. | ||
| 191 | + | ||
| 192 | +### Schema namespacing | ||
| 193 | + | ||
| 194 | +PBCs and plug-ins use **table name prefixes**, not Postgres schemas: | ||
| 195 | + | ||
| 196 | +``` | ||
| 197 | +identity__user, identity__role | ||
| 198 | +catalog__item, catalog__item_attribute | ||
| 199 | +inventory__stock_item, inventory__movement | ||
| 200 | +orders_sales__order, orders_sales__order_line | ||
| 201 | +production__work_order, production__operation | ||
| 202 | +plugin_printingshop__plate_spec (reference plug-in) | ||
| 203 | +metadata__custom_field, metadata__form, metadata__workflow | ||
| 204 | +flowable_* (Flowable's own tables, untouched) | ||
| 205 | +``` | ||
| 206 | + | ||
| 207 | +This keeps Hibernate, RLS policies, and migrations all in one logical schema (`public`), avoids `search_path` traps, and gives clean uninstall semantics. | ||
| 208 | + | ||
| 209 | +### Tenant isolation — two independent walls | ||
| 210 | + | ||
| 211 | +- Every business table has `tenant_id`, NOT NULL. | ||
| 212 | +- Hibernate `@TenantId` filters every query at the application layer. | ||
| 213 | +- Postgres Row-Level Security policies filter every query at the database layer. | ||
| 214 | + | ||
| 215 | +Two independent walls. A bug in one is not a data leak. Self-hosted single-customer deployments use one tenant row called `default`. Hosted multi-tenant deployments use many tenant rows. **Same code path.** | ||
| 216 | + | ||
| 217 | +### Data sovereignty | ||
| 218 | + | ||
| 219 | +- Self-hosted is automatically compliant — the customer chose where Postgres lives. | ||
| 220 | +- Hosted supports per-region tenant routing: each tenant row carries a region; connections are routed to the right regional Postgres cluster. | ||
| 221 | +- PII tagging on field metadata drives auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17). | ||
| 222 | +- Append-only audit log records access to PII fields when audit-strict mode is on. | ||
| 223 | + | ||
| 224 | +## Custom fields via JSONB | ||
| 225 | + | ||
| 226 | +Every business table has: | ||
| 227 | + | ||
| 228 | +```sql | ||
| 229 | +ext jsonb not null default '{}', | ||
| 230 | +ext_meta text generated | ||
| 231 | +``` | ||
| 232 | + | ||
| 233 | +Custom fields are JSON keys inside `ext`. A GIN index on `ext` makes them queryable. The `metadata__custom_field` table describes the JSON shape per entity per tenant. The form designer, list views, OpenAPI generator, and AI-agent function catalog all read from this table. | ||
| 234 | + | ||
| 235 | +Why JSONB and not EAV: one row, one read, indexable, no migrations needed for additions, no joins. EAV is the wrong tool. | ||
| 236 | + | ||
| 237 | +For the rare hot-path custom field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default. | ||
| 238 | + | ||
| 239 | +## The metadata store | ||
| 240 | + | ||
| 241 | +``` | ||
| 242 | +metadata__entity metadata__form metadata__permission | ||
| 243 | +metadata__custom_field metadata__list_view metadata__role_permission | ||
| 244 | +metadata__workflow metadata__rule metadata__menu | ||
| 245 | +metadata__report metadata__translation metadata__plugin_config | ||
| 246 | +``` | ||
| 247 | + | ||
| 248 | +Every row carries `tenant_id`, `source` (`core` / `plugin:<id>` / `user`), `version`, `is_active`. The `source` column makes uninstall and upgrade safe: removing a plug-in cleans up its metadata, and user-created metadata is sacred and never touched by an upgrade. | ||
| 249 | + | ||
| 250 | +Every consumer reads from this store: the form renderer, list views, OpenAPI generator, AI-agent function catalog, role editor. There are no parallel sources of truth. | ||
| 251 | + | ||
| 252 | +## The workflow engine | ||
| 253 | + | ||
| 254 | +vibe_erp embeds **Flowable** (BPMN 2.0). Workflows are data: `.bpmn` files at design time, `flowable_*` tables at runtime. Two authoring paths exist, both first-class: | ||
| 255 | + | ||
| 256 | +- **Tier 1 (visual designer in the web UI, v1.0):** business analysts draw workflows in a BPMN designer. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'`. | ||
| 257 | +- **Tier 2 (`.bpmn` files in a plug-in JAR):** plug-in authors ship BPMN files in their JAR. The plug-in lifecycle deploys them on plug-in install. | ||
| 258 | + | ||
| 259 | +Workflows interact with the system through: | ||
| 260 | + | ||
| 261 | +- **Service tasks** that call typed `TaskHandler` implementations. A plug-in registers a `TaskHandler` via `@Extension(point = TaskHandler::class)`; the workflow references it by id. There is no scripting language to invent — the `TaskHandler` is just Kotlin code behind a typed interface. | ||
| 262 | +- **User tasks** that render forms from `metadata__form` definitions. The form is referenced by id; rendering goes through the same code path used by Tier 1 forms. | ||
| 263 | + | ||
| 264 | +The temptation to invent a vibe_erp-only workflow language must be rejected. BPMN 2.0 via Flowable is the standard, and the standard is the contract. | ||
| 265 | + | ||
| 266 | +## Cross-cutting concerns | ||
| 267 | + | ||
| 268 | +| Concern | Approach | | ||
| 269 | +|---|---| | ||
| 270 | +| Security | `PermissionCheck` declared in `api.v1.security`; plug-ins register their own permissions, auto-listed in the role editor | | ||
| 271 | +| Transactions | Spring `@Transactional` at the application-service layer; plug-ins use `api.v1.persistence.Transaction`, never Spring directly | | ||
| 272 | +| Audit | `created_at`, `created_by`, `updated_at`, `updated_by`, `tenant_id` on every entity, applied by a JPA listener | | ||
| 273 | +| Events | Typed `DomainEvent`s on every state change; in-process bus by default; outbox table in Postgres for cross-crash reliability and as the seam where Kafka/NATS plugs in later | | ||
| 274 | +| AI-agent surface | Same business operations exposed through REST are exposable through an MCP server; v1.1 ships the MCP endpoint, v1.0 architects the seam | | ||
| 275 | +| Reporting | JasperReports; templates shipped by core or by plug-ins, customer-skinnable | | ||
| 276 | +| i18n | ICU MessageFormat, locale-aware formatting for dates, numbers, and currencies; no string concatenation in user-facing code | | ||
| 277 | + | ||
| 278 | +## Where to read more | ||
| 279 | + | ||
| 280 | +- The full architecture spec: [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) | ||
| 281 | +- The architectural guardrails (the rules that exist because reusability across customers is the entire point of the project): `CLAUDE.md` at the repo root. | ||
| 282 | +- Plug-in API surface: [`../plugin-api/overview.md`](../plugin-api/overview.md) | ||
| 283 | +- Building your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) |
docs/customer-onboarding/guide.md
0 → 100644
| 1 | +# Customer onboarding guide | ||
| 2 | + | ||
| 3 | +This guide is for an **integrator** standing up vibe_erp for a new customer end-to-end. It is written from a Tier 1 perspective: no plug-in code is required to follow it. Where Tier 2 plug-ins are useful, the guide says so. | ||
| 4 | + | ||
| 5 | +For the architectural background, see [`../architecture/overview.md`](../architecture/overview.md). For the full v1.0 cut line, see section 11 of [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 6 | + | ||
| 7 | +## Honest scope note | ||
| 8 | + | ||
| 9 | +vibe_erp is a framework, not a turnkey product. **The Tier 1 customization UIs (custom field designer, form designer, BPMN designer, list view editor, role editor) are v1.0 deliverables.** v0.1 ships only the underlying API surface — the metadata tables, the REST endpoints, the seed data — without the polished UIs on top of them. Until v1.0 ships, the steps below that say "use the Customize UI" mean "POST to the metadata REST endpoints" or "edit a YAML seed file in your plug-in JAR". | ||
| 10 | + | ||
| 11 | +This guide is written against the v1.0 experience. Where v0.1 deviates, the step is annotated **(v0.1: API only)**. | ||
| 12 | + | ||
| 13 | +## 1. Install the host | ||
| 14 | + | ||
| 15 | +vibe_erp ships as a single Docker image with PostgreSQL as the only mandatory external dependency. | ||
| 16 | + | ||
| 17 | +```bash | ||
| 18 | +docker run -d --name vibe-erp \ | ||
| 19 | + -p 8080:8080 \ | ||
| 20 | + -v /srv/vibeerp:/opt/vibe-erp \ | ||
| 21 | + -e DB_URL=jdbc:postgresql://db.internal:5432/vibeerp \ | ||
| 22 | + -e DB_USER=vibeerp \ | ||
| 23 | + -e DB_PASSWORD=... \ | ||
| 24 | + ghcr.io/vibeerp/vibe-erp:1.0.0 | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +What happens on first boot: | ||
| 28 | + | ||
| 29 | +1. The host connects to Postgres. | ||
| 30 | +2. Liquibase runs every core PBC's migrations and creates the `flowable_*` tables. | ||
| 31 | +3. A `default` tenant row is created in `identity__tenant`. | ||
| 32 | +4. A bootstrap admin user is created and its one-time password is printed to the boot log. | ||
| 33 | +5. The host is ready in under 30 seconds. | ||
| 34 | + | ||
| 35 | +The mounted volume `/srv/vibeerp` (mapped to `/opt/vibe-erp` inside the container) holds: | ||
| 36 | + | ||
| 37 | +``` | ||
| 38 | +/opt/vibe-erp/ | ||
| 39 | +├── config/vibe-erp.yaml single config file (closed key set) | ||
| 40 | +├── plugins/ drop *.jar to install | ||
| 41 | +├── i18n-overrides/ tenant-level translation overrides | ||
| 42 | +├── files/ file store (if not using S3) | ||
| 43 | +└── logs/ | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +Customer extensions live entirely outside the image. Upgrading the host is `docker rm` plus `docker run` with the new image tag — extensions and config stay put. | ||
| 47 | + | ||
| 48 | +## 2. Log in as the bootstrap admin | ||
| 49 | + | ||
| 50 | +Open `http://<host>:8080/`, log in with `admin` and the one-time password from the boot log, and change the password immediately. Configure OIDC (Keycloak-compatible) at this point if the customer has an existing identity provider — built-in JWT auth and OIDC SSO are both shipped from day one. | ||
| 51 | + | ||
| 52 | +## 3. Create a tenant (hosted only) | ||
| 53 | + | ||
| 54 | +For a self-hosted single-customer deployment, the `default` tenant is everything you need. Skip this step. | ||
| 55 | + | ||
| 56 | +For a hosted multi-tenant deployment, create one tenant row per customer. Each tenant carries: | ||
| 57 | + | ||
| 58 | +- A unique tenant id. | ||
| 59 | +- A region (used by the per-region routing layer in hosted mode). | ||
| 60 | +- A default locale, currency, and time zone. | ||
| 61 | + | ||
| 62 | +Tenant onboarding is an `INSERT` plus seed metadata, not a migration — sub-second per tenant. | ||
| 63 | + | ||
| 64 | +## 4. Use the Customize UI to model the customer's reality | ||
| 65 | + | ||
| 66 | +This is where most of the integrator's time is spent. Everything here is **Tier 1**: rows in `metadata__*` tables, tagged `source = 'user'`, scoped to the tenant. No build, no restart, no deploy. **(v0.1: API only — POST to the metadata endpoints; the UIs ship in v1.0.)** | ||
| 67 | + | ||
| 68 | +### Custom fields | ||
| 69 | + | ||
| 70 | +Add custom fields to existing entities (item, partner, order, work order, …). Each custom field is a row in `metadata__custom_field` and a JSON key in the entity's `ext` JSONB column at runtime. A GIN index on `ext` keeps custom fields queryable. The form designer, list views, OpenAPI spec, and AI-agent function catalog all auto-update from the metadata. | ||
| 71 | + | ||
| 72 | +For the rare hot-path field, an operator can promote a JSON key to a real generated column via an auto-generated Liquibase changeset. This is an optimization, not the default. | ||
| 73 | + | ||
| 74 | +### Custom forms | ||
| 75 | + | ||
| 76 | +Define how each entity is displayed and edited. Forms are JSON Schema (data shape) plus UI Schema (layout and widgets), stored in `metadata__form`. Forms can reference custom fields by their key. See the [form authoring guide](../form-authoring/guide.md). | ||
| 77 | + | ||
| 78 | +### Custom workflows | ||
| 79 | + | ||
| 80 | +Draw the customer's process flow in the BPMN designer. Workflows are stored in `metadata__workflow` and deployed to the embedded Flowable engine. User tasks render forms; service tasks call typed `TaskHandler` implementations registered by plug-ins. See the [workflow authoring guide](../workflow-authoring/guide.md). | ||
| 81 | + | ||
| 82 | +### Custom list views, rules, menus, reports | ||
| 83 | + | ||
| 84 | +- **List views** (`metadata__list_view`): which columns, which filters, which default sort. | ||
| 85 | +- **Rules** (`metadata__rule`): simple "if X then Y" automations for the cases that do not warrant a full BPMN workflow. | ||
| 86 | +- **Menus** (`metadata__menu`): the navigation tree the user sees. | ||
| 87 | +- **Reports** (`metadata__report`): JasperReports templates the customer can run on demand or on a schedule. | ||
| 88 | + | ||
| 89 | +## 5. Import master data | ||
| 90 | + | ||
| 91 | +vibe_erp accepts master data through two paths: | ||
| 92 | + | ||
| 93 | +- **CSV import** through the Customize UI for one-off bulk loads (catalog items, partners, opening stock, chart of accounts). **(v0.1: API only.)** | ||
| 94 | +- **REST API** for ongoing integration with the customer's existing systems. Every entity, including custom fields, is exposed through OpenAPI-documented REST endpoints. The OpenAPI spec auto-updates as you add custom fields and custom entities, so the integration code is never out of date. | ||
| 95 | + | ||
| 96 | +## 6. Configure roles and permissions | ||
| 97 | + | ||
| 98 | +Define the customer's roles (e.g. *sales clerk*, *production planner*, *warehouse operator*, *finance reviewer*) and assign permissions. Permissions are auto-discovered from: | ||
| 99 | + | ||
| 100 | +- Core PBCs. | ||
| 101 | +- Plug-ins (each plug-in registers its own permissions through `api.v1.security.PermissionCheck`). | ||
| 102 | +- Custom entities (each generates a standard CRUD permission set). | ||
| 103 | + | ||
| 104 | +Bind users to roles, bind roles to tenants. Hosted deployments can also bind roles via OIDC group claims. | ||
| 105 | + | ||
| 106 | +## 7. Add Tier 2 plug-ins as needed | ||
| 107 | + | ||
| 108 | +When Tier 1 metadata is not expressive enough — for example, a printing-specific entity like *plate spec* with its own validation logic, or a workflow service task that calls an external MIS — install a Tier 2 plug-in. | ||
| 109 | + | ||
| 110 | +To install a plug-in: | ||
| 111 | + | ||
| 112 | +1. Drop the JAR into `/opt/vibe-erp/plugins/`. | ||
| 113 | +2. Restart the host (`docker restart vibe-erp`). Hot reload is on the v1.2+ roadmap. | ||
| 114 | +3. The plug-in loader scans the JAR, validates the manifest, runs the plug-in linter, runs the plug-in's Liquibase migrations in `plugin_<id>__*`, seeds the plug-in's metadata, and starts the plug-in. | ||
| 115 | +4. Verify the plug-in shows up under `/actuator/health` and in the boot log. | ||
| 116 | + | ||
| 117 | +The reference printing-shop plug-in (`reference-customer/plugin-printing-shop/`) is a worked example of a non-trivial Tier 2 plug-in. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`** and is built and CI-tested on every PR. It is **not** loaded by default — drop its JAR into `./plugins/` to use it. | ||
| 118 | + | ||
| 119 | +A guiding principle: **anything a Tier 2 plug-in does should also become possible as Tier 1 over time.** Tier 2 is the escape hatch where Tier 1 is not yet expressive enough. When you find yourself reaching for a plug-in to do something a metadata row could express, file an issue against the framework. | ||
| 120 | + | ||
| 121 | +## 8. Go-live checklist | ||
| 122 | + | ||
| 123 | +- [ ] Backups configured against the Postgres instance. Customer data lives there exclusively (plus the file store). | ||
| 124 | +- [ ] Audit log enabled for PII fields if the customer is subject to GDPR or equivalent. | ||
| 125 | +- [ ] DSAR export and erasure jobs tested against a non-production tenant. | ||
| 126 | +- [ ] Locale, currency, time zone defaults set for the tenant. | ||
| 127 | +- [ ] OIDC integration tested with the customer's identity provider. | ||
| 128 | +- [ ] Health check (`/actuator/health`) wired into the customer's monitoring. | ||
| 129 | +- [ ] Documented upgrade procedure for the operator: `docker rm` plus `docker run` with the new image tag, plug-ins and config stay put. | ||
| 130 | + | ||
| 131 | +## What this guide deliberately does not cover | ||
| 132 | + | ||
| 133 | +- **Plug-in development.** That is the [plug-in author getting-started guide](../plugin-author/getting-started.md). | ||
| 134 | +- **Hosted multi-tenant operations** (per-region routing, billing, tenant provisioning UI). Hosted mode is a v2 deliverable; the data model and routing layer are architected for it from day one but the operations UI is not in v1.0. | ||
| 135 | +- **MCP / AI-agent endpoint.** v1.1 deliverable. The seam exists in v1.0; the endpoint does not. |
docs/form-authoring/guide.md
0 → 100644
| 1 | +# Form authoring guide | ||
| 2 | + | ||
| 3 | +Forms in vibe_erp describe how an entity is displayed and edited. They are **data, not code** — stored as `metadata__form` rows, consumed by one renderer, and used identically inside and outside workflows. | ||
| 4 | + | ||
| 5 | +For where forms sit in the architecture, see [`../architecture/overview.md`](../architecture/overview.md). For the workflow side, see the [workflow authoring guide](../workflow-authoring/guide.md). | ||
| 6 | + | ||
| 7 | +## JSON Schema + UI Schema | ||
| 8 | + | ||
| 9 | +A vibe_erp form definition has two halves: | ||
| 10 | + | ||
| 11 | +- **JSON Schema** describes the **data shape**: which fields exist, their types, which are required, what their validation constraints are. | ||
| 12 | +- **UI Schema** describes the **layout and widgets**: which field renders as a dropdown vs. a radio group, which fields are grouped under which tab, which fields are read-only, which are conditionally visible. | ||
| 13 | + | ||
| 14 | +The split matters. The JSON Schema is the contract — server-side validation, OpenAPI generation, and the AI-agent function catalog all read it. The UI Schema is presentation — only the renderer reads it. Two forms can share a JSON Schema but render differently. | ||
| 15 | + | ||
| 16 | +A form definition looks roughly like this (illustrative): | ||
| 17 | + | ||
| 18 | +```json | ||
| 19 | +{ | ||
| 20 | + "id": "orders_sales.order.create", | ||
| 21 | + "schema": { | ||
| 22 | + "type": "object", | ||
| 23 | + "required": ["customerId", "lines"], | ||
| 24 | + "properties": { | ||
| 25 | + "customerId": { "type": "string", "format": "uuid" }, | ||
| 26 | + "deliveryDate": { "type": "string", "format": "date" }, | ||
| 27 | + "lines": { | ||
| 28 | + "type": "array", | ||
| 29 | + "minItems": 1, | ||
| 30 | + "items": { | ||
| 31 | + "type": "object", | ||
| 32 | + "required": ["itemId", "quantity"], | ||
| 33 | + "properties": { | ||
| 34 | + "itemId": { "type": "string", "format": "uuid" }, | ||
| 35 | + "quantity": { "type": "number", "exclusiveMinimum": 0 } | ||
| 36 | + } | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + }, | ||
| 41 | + "uiSchema": { | ||
| 42 | + "type": "VerticalLayout", | ||
| 43 | + "elements": [ | ||
| 44 | + { "type": "Control", "scope": "#/properties/customerId", "options": { "widget": "partner-picker" } }, | ||
| 45 | + { "type": "Control", "scope": "#/properties/deliveryDate" }, | ||
| 46 | + { "type": "Control", "scope": "#/properties/lines", "options": { "widget": "line-items-grid" } } | ||
| 47 | + ] | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +## Two authoring paths | ||
| 53 | + | ||
| 54 | +Both paths are first-class. The renderer does not know which one produced a given form. | ||
| 55 | + | ||
| 56 | +### Tier 1 — Form designer in the web UI (v1.0) | ||
| 57 | + | ||
| 58 | +Business analysts build forms in the form designer that ships in the web SPA. The result is stored as a row in `metadata__form`, scoped to the tenant, tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart. | ||
| 59 | + | ||
| 60 | +This path is the right one for: | ||
| 61 | + | ||
| 62 | +- Tweaking the layout of an existing entity for a specific tenant. | ||
| 63 | +- Adding a tenant-specific custom field to an existing form. | ||
| 64 | +- Building a form for a Tier 1 custom entity. | ||
| 65 | + | ||
| 66 | +The form designer is a **v1.0 deliverable**. | ||
| 67 | + | ||
| 68 | +### Tier 2 — JSON files in a plug-in JAR (v0.1) | ||
| 69 | + | ||
| 70 | +Plug-in authors ship form definitions inside their JAR under `src/main/resources/metadata/forms/`. The plug-in lifecycle upserts them into `metadata__form` on plug-in install, tagged `source = 'plugin:<id>'`. | ||
| 71 | + | ||
| 72 | +``` | ||
| 73 | +src/main/resources/ | ||
| 74 | +├── plugin.yml | ||
| 75 | +└── metadata/ | ||
| 76 | + └── forms/ | ||
| 77 | + ├── plate_spec.json | ||
| 78 | + └── job_card_review.json | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +```yaml | ||
| 82 | +# plugin.yml | ||
| 83 | +metadata: | ||
| 84 | + forms: | ||
| 85 | + - metadata/forms/plate_spec.json | ||
| 86 | + - metadata/forms/job_card_review.json | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +This path is the right one for forms that ship as part of a vertical plug-in (the printing-shop plug-in is the canonical example). | ||
| 90 | + | ||
| 91 | +## Forms can reference custom fields | ||
| 92 | + | ||
| 93 | +A form can reference any custom field defined for the same entity by its key. Because custom fields live as JSON keys in the entity's `ext` JSONB column and are described by `metadata__custom_field` rows, the form designer and the form renderer both already know about them — no additional wiring required. | ||
| 94 | + | ||
| 95 | +The key is the same one the customer chose when creating the custom field. If a printing-shop integrator added a `coating_finish` custom field to the `production.work_order` entity, a form referencing `ext.coating_finish` will render and validate it correctly, whether the form was authored through the Tier 1 designer or shipped inside a plug-in JAR. | ||
| 96 | + | ||
| 97 | +## Validation runs on both sides | ||
| 98 | + | ||
| 99 | +Validation happens in two places, and both come from the same JSON Schema: | ||
| 100 | + | ||
| 101 | +- **Client-side** in the renderer, driven by the UI Schema's widgets and the JSON Schema constraints. This is the responsive, immediate-feedback layer. | ||
| 102 | +- **Server-side** in the host, driven by the JSON Schema. This is the **authoritative** layer. The renderer's checks are a convenience; the server's checks are the contract. | ||
| 103 | + | ||
| 104 | +A form submission that bypasses the UI (a script, an integration, an AI agent calling through MCP) is validated against exactly the same JSON Schema as a form filled in through the SPA. There is no path that skips validation. | ||
| 105 | + | ||
| 106 | +## Where to go next | ||
| 107 | + | ||
| 108 | +- Workflows that render forms in user tasks: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md) | ||
| 109 | +- Plug-in author walkthrough: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) | ||
| 110 | +- Plug-in API surface, including `api.v1.form`: [`../plugin-api/overview.md`](../plugin-api/overview.md) |
docs/i18n/guide.md
0 → 100644
| 1 | +# i18n guide | ||
| 2 | + | ||
| 3 | +vibe_erp is built to be sold worldwide. There are no hard-coded user-facing strings, currencies, date formats, time zones, number formats, address shapes, or tax models in the framework — everything user-facing flows through the i18n and formatting layers from day one. This is guardrail #6 in `CLAUDE.md`, and it applies to the core, every PBC, and every plug-in. | ||
| 4 | + | ||
| 5 | +For the architectural placement of i18n, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 6 | + | ||
| 7 | +## ICU MessageFormat is the backbone | ||
| 8 | + | ||
| 9 | +vibe_erp uses **ICU MessageFormat** (via ICU4J) on top of Spring's `MessageSource`. ICU MessageFormat handles the things naive `printf`-style formatting cannot: | ||
| 10 | + | ||
| 11 | +- Plurals (`{count, plural, one {# item} other {# items}}`) | ||
| 12 | +- Gender selection | ||
| 13 | +- Locale-aware number, date, and currency formatting | ||
| 14 | +- Nested arguments and select clauses | ||
| 15 | + | ||
| 16 | +A message in a bundle file looks like this: | ||
| 17 | + | ||
| 18 | +```properties | ||
| 19 | +orders_sales.cart.summary = {count, plural, =0 {Your cart is empty.} one {# item, total {total, number, currency}} other {# items, total {total, number, currency}}} | ||
| 20 | +``` | ||
| 21 | + | ||
| 22 | +The same key produces correct output in English, German, Japanese, Chinese, and Spanish without changing a line of calling code. | ||
| 23 | + | ||
| 24 | +## Plug-ins ship message bundles | ||
| 25 | + | ||
| 26 | +Each plug-in (and each PBC) ships its message bundles inside its JAR under `i18n/<locale>.properties`. The locale tag follows BCP 47: | ||
| 27 | + | ||
| 28 | +``` | ||
| 29 | +src/main/resources/ | ||
| 30 | +└── i18n/ | ||
| 31 | + ├── en-US.properties | ||
| 32 | + ├── zh-CN.properties | ||
| 33 | + ├── de-DE.properties | ||
| 34 | + ├── ja-JP.properties | ||
| 35 | + └── es-ES.properties | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +The `plugin.yml` manifest lists the bundles so the loader knows to pick them up: | ||
| 39 | + | ||
| 40 | +```yaml | ||
| 41 | +metadata: | ||
| 42 | + i18n: | ||
| 43 | + - i18n/en-US.properties | ||
| 44 | + - i18n/de-DE.properties | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +## How the host merges bundles | ||
| 48 | + | ||
| 49 | +On boot, and on every plug-in install, the host builds a merged `MessageSource` per locale. The merge order, from lowest precedence to highest: | ||
| 50 | + | ||
| 51 | +1. **Core bundles** shipped inside the vibe_erp image. | ||
| 52 | +2. **Plug-in bundles** picked up from `./plugins/*.jar` in plug-in load order. A plug-in may override a core key with the same key in its own bundle, but is not encouraged to. | ||
| 53 | +3. **Tenant overrides** stored in `metadata__translation` rows tagged with the tenant id. These are entered through the Tier 1 customization UI and let an integrator rewrite any string for any tenant without rebuilding anything. | ||
| 54 | + | ||
| 55 | +``` | ||
| 56 | +tenant overrides ← highest precedence | ||
| 57 | + plug-in bundles | ||
| 58 | + core bundles ← lowest precedence | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +The merged `MessageSource` is per-locale and per-tenant. Switching the user's locale or switching tenants picks the right merged bundle without restarting anything. | ||
| 62 | + | ||
| 63 | +## Shipping locales for v1.0 | ||
| 64 | + | ||
| 65 | +vibe_erp v1.0 ships with five locales: | ||
| 66 | + | ||
| 67 | +| Locale tag | Language | | ||
| 68 | +|---|---| | ||
| 69 | +| `en-US` | English (United States) | | ||
| 70 | +| `zh-CN` | Chinese (Simplified) | | ||
| 71 | +| `de-DE` | German | | ||
| 72 | +| `ja-JP` | Japanese | | ||
| 73 | +| `es-ES` | Spanish (Spain) | | ||
| 74 | + | ||
| 75 | +Adding a sixth locale is a Tier 1 operation: drop a `<locale>.properties` file into `i18n-overrides/` on the mounted volume, or add `metadata__translation` rows through the customization UI. No rebuild. | ||
| 76 | + | ||
| 77 | +The reference business documentation under `raw/业务流程设计文档/` is in Chinese. **That does not make Chinese the default.** Chinese is one supported locale among the five. | ||
| 78 | + | ||
| 79 | +## Translation key naming | ||
| 80 | + | ||
| 81 | +Translation keys follow `<plugin-or-pbc>.<area>.<message>`: | ||
| 82 | + | ||
| 83 | +``` | ||
| 84 | +identity.login.title | ||
| 85 | +identity.login.error.invalid_credentials | ||
| 86 | +catalog.item.field.sku.label | ||
| 87 | +orders_sales.order.status.confirmed | ||
| 88 | +plugin_printingshop.plate.field.thickness.label | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +Rules: | ||
| 92 | + | ||
| 93 | +- The first segment is the PBC table prefix (`identity`, `catalog`, `orders_sales`, …) or, for a plug-in, `plugin_<id>`. This is the same prefix used in the database, the metadata `source` column, and the OpenAPI tag. | ||
| 94 | +- Segments are `snake_case`. | ||
| 95 | +- The last segment names the message itself, not the widget that renders it. | ||
| 96 | +- Two plug-ins can never collide: their first segment differs by definition. | ||
| 97 | + | ||
| 98 | +## The `Translator` is the only sanctioned way | ||
| 99 | + | ||
| 100 | +The host injects a `Translator` (from `org.vibeerp.api.v1.i18n`) into every plug-in endpoint, every workflow task handler, every event listener. **There is no string concatenation in user-facing code, anywhere, ever.** | ||
| 101 | + | ||
| 102 | +```kotlin | ||
| 103 | +val message = translator.format( | ||
| 104 | + MessageKey("orders_sales.order.created.notification"), | ||
| 105 | + mapOf( | ||
| 106 | + "orderNumber" to order.number, | ||
| 107 | + "customerName" to order.customer.name, | ||
| 108 | + "total" to order.total, | ||
| 109 | + ), | ||
| 110 | +) | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +What this means in practice: | ||
| 114 | + | ||
| 115 | +- A reviewer who sees `"Order " + number + " was created"` in a PR rejects the PR. | ||
| 116 | +- A reviewer who sees a `String.format` against a hard-coded format string rejects the PR. | ||
| 117 | +- A reviewer who sees `if (locale == "en") "..." else "..."` rejects the PR with prejudice. | ||
| 118 | + | ||
| 119 | +The `Translator` is locale-aware: it picks up the request's resolved locale from the `LocaleProvider`, which in turn resolves from (in order) the user's profile preference, the request's `Accept-Language`, the tenant default, and the system default. | ||
| 120 | + | ||
| 121 | +## PII, dates, numbers, currency | ||
| 122 | + | ||
| 123 | +Locale handling does not stop at strings: | ||
| 124 | + | ||
| 125 | +- **Dates and times** are formatted through the locale, not through hard-coded patterns. The host exposes locale-aware formatters; plug-ins use them. | ||
| 126 | +- **Numbers** use the locale's grouping and decimal separators. | ||
| 127 | +- **Currency** values are `Money` (from `api.v1.core`) — an amount plus an ISO 4217 code — and the formatter renders them in the user's locale (e.g. `$1,234.56` vs. `1.234,56 €` vs. `¥1,235`). | ||
| 128 | +- **Time zones** are per-tenant and per-user, not per-server. | ||
| 129 | +- **PII fields** are tagged in the field metadata. Tagged fields drive auto-generated DSAR exports and erasure jobs (GDPR Articles 15/17), and the audit log records access to them when audit-strict mode is on. | ||
| 130 | +- **Address shapes** are not assumed. There is no "state/province" or "ZIP code" hard-coded in the core; address fields are configurable via metadata so a Japanese address can have prefecture/city/ward and a French address can have arrondissement. | ||
| 131 | + | ||
| 132 | +## Where to go next | ||
| 133 | + | ||
| 134 | +- Plug-in author walkthrough that uses `Translator`: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) | ||
| 135 | +- Plug-in API surface for i18n: [`../plugin-api/overview.md`](../plugin-api/overview.md) |
docs/index.md
0 → 100644
| 1 | +# vibe_erp documentation | ||
| 2 | + | ||
| 3 | +vibe_erp is an ERP/EBC framework for the printing industry, designed so customer-specific workflows are assembled by configuration and plug-ins instead of by forking the core. This index is the entry point to the framework's own documentation. The full architecture spec lives at [`superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](superpowers/specs/2026-04-07-vibe-erp-architecture-design.md) and is the source of truth for everything in this tree. | ||
| 4 | + | ||
| 5 | +## Guides | ||
| 6 | + | ||
| 7 | +| Guide | What it covers | | ||
| 8 | +|---|---| | ||
| 9 | +| [Architecture overview](architecture/overview.md) | Clean Core philosophy, two-tier extensibility, PBCs, the dependency rule, multi-tenancy, custom fields, the workflow engine. | | ||
| 10 | +| [Plug-in API overview](plugin-api/overview.md) | What `api.v1` is, the package layout, the stability contract, the A/B/C/D extension grading. | | ||
| 11 | +| [Plug-in author getting started](plugin-author/getting-started.md) | A concrete walkthrough for building, packaging, and loading your first vibe_erp plug-in. | | ||
| 12 | +| [Workflow authoring guide](workflow-authoring/guide.md) | BPMN 2.0 workflows on embedded Flowable: visual designer (Tier 1) and `.bpmn` files in plug-ins (Tier 2). | | ||
| 13 | +| [Form authoring guide](form-authoring/guide.md) | JSON Schema + UI Schema forms: visual designer (Tier 1) and JSON files in plug-ins (Tier 2). | | ||
| 14 | +| [i18n guide](i18n/guide.md) | ICU MessageFormat, message bundles, locale resolution, tenant overrides, shipping locales. | | ||
| 15 | +| [Customer onboarding guide](customer-onboarding/guide.md) | Integrator's checklist for standing up a new vibe_erp customer end-to-end. | |
docs/plugin-api/overview.md
0 → 100644
| 1 | +# Plug-in API overview | ||
| 2 | + | ||
| 3 | +`org.vibeerp.api.v1` (`api.v1` for short) is the **only stable contract** in vibe_erp. It is the surface that PF4J plug-ins compile against, and the only surface they are permitted to touch. Everything else in the codebase — `platform.*`, every PBC's internal package, every concrete Spring bean — is internal and may change in any release. | ||
| 4 | + | ||
| 5 | +For the architectural reasoning behind `api.v1`, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 6 | + | ||
| 7 | +## What `api.v1` is | ||
| 8 | + | ||
| 9 | +- A Kotlin module published to **Maven Central** as `org.vibeerp:api-v1`. | ||
| 10 | +- Depends on **only** Kotlin stdlib and `jakarta.validation`. No Spring, no Hibernate, no PF4J types leak through it. | ||
| 11 | +- Semver-governed on the `1.x` line. Across a major version, `api.v1` and `api.v2` ship side by side for at least one major release window so plug-ins have time to migrate. | ||
| 12 | +- The single import surface for plug-ins. The plug-in linter rejects any import outside `org.vibeerp.api.v1.*` at install time. | ||
| 13 | + | ||
| 14 | +## Package layout | ||
| 15 | + | ||
| 16 | +``` | ||
| 17 | +org.vibeerp.api.v1 | ||
| 18 | +├── core/ Tenant, Locale, Money, Quantity, Id<T>, Result<T,E> | ||
| 19 | +├── entity/ Entity, Field, FieldType, EntityRegistry | ||
| 20 | +├── persistence/ Repository<T>, Query, Page, Transaction | ||
| 21 | +├── event/ DomainEvent, EventListener, EventBus | ||
| 22 | +├── security/ Principal, Permission, PermissionCheck | ||
| 23 | +├── i18n/ MessageKey, Translator, LocaleProvider | ||
| 24 | +├── http/ @PluginEndpoint, RequestContext, ResponseBuilder | ||
| 25 | +├── plugin/ Plugin, PluginManifest, ExtensionPoint | ||
| 26 | +├── ext/ Typed extension interfaces a plug-in implements | ||
| 27 | +├── workflow/ WorkflowTask, WorkflowEvent, TaskHandler | ||
| 28 | +└── form/ FormSchema, UiSchema | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +A short orientation: | ||
| 32 | + | ||
| 33 | +| Package | What it gives you | | ||
| 34 | +|---|---| | ||
| 35 | +| `core/` | The primitive value types every plug-in needs: `Tenant`, `Locale`, `Money`, `Quantity`, typed `Id<T>`, `Result<T,E>`. No printing concepts. | | ||
| 36 | +| `entity/` | Declarative entity model. Plug-ins describe entities, fields, and field types through `EntityRegistry`; the platform handles persistence and OpenAPI. | | ||
| 37 | +| `persistence/` | `Repository<T>`, `Query`, `Page`, `Transaction`. Plug-ins never see Hibernate or Spring `@Transactional` directly. | | ||
| 38 | +| `event/` | `DomainEvent`, `EventListener`, `EventBus`. The primary cross-PBC and cross-plug-in communication channel. | | ||
| 39 | +| `security/` | `Principal`, `Permission`, `PermissionCheck`. Plug-ins register their own permissions; the role editor auto-discovers them. | | ||
| 40 | +| `i18n/` | `MessageKey`, `Translator`, `LocaleProvider`. The only sanctioned way for a plug-in to produce user-facing text. | | ||
| 41 | +| `http/` | `@PluginEndpoint` and the request/response abstractions for adding REST endpoints from a plug-in. | | ||
| 42 | +| `plugin/` | `Plugin`, `PluginManifest`, `ExtensionPoint`, and the `@Extension` annotation. The plug-in lifecycle entry points. | | ||
| 43 | +| `ext/` | Typed extension interfaces that PBCs declare and plug-ins implement (e.g. `api.v1.ext.inventory.StockReservationStrategy`). The cross-PBC interaction surface. | | ||
| 44 | +| `workflow/` | `WorkflowTask`, `WorkflowEvent`, `TaskHandler`. The hooks BPMN service tasks call into. | | ||
| 45 | +| `form/` | `FormSchema`, `UiSchema`. JSON Schema and UI Schema as Kotlin types, for plug-ins shipping form definitions. | | ||
| 46 | + | ||
| 47 | +## The stability contract | ||
| 48 | + | ||
| 49 | +| Change | Allowed within 1.x? | | ||
| 50 | +|---|---| | ||
| 51 | +| Add a class to `api.v1` | yes | | ||
| 52 | +| Add a method to an `api.v1` interface (with default impl) | yes | | ||
| 53 | +| Remove or rename anything in `api.v1` | no — major bump | | ||
| 54 | +| Change behavior of an `api.v1` symbol in a way plug-ins can observe | no — major bump | | ||
| 55 | +| Anything in `platform.*` or `pbc.*.internal.*` | yes — that is why it is internal | | ||
| 56 | + | ||
| 57 | +Practical consequences for plug-in authors: | ||
| 58 | + | ||
| 59 | +- A plug-in built against `api.v1` version `1.4.0` will load in any vibe_erp `1.x` release. | ||
| 60 | +- A symbol that gets deprecated in `1.x` keeps working until `2.0`. The plug-in loader emits a warning when a plug-in uses a deprecated symbol — that is the **Grade C** signal in the extension grading. | ||
| 61 | +- A plug-in that reaches into `platform.*` or `pbc.*.internal.*` via reflection is **Grade D**, unsupported, and rejected by the plug-in linter at install time. | ||
| 62 | + | ||
| 63 | +When in doubt about whether something belongs in `api.v1`: **keep it out**. Growing the API deliberately later is much cheaper than maintaining a regretted symbol forever. | ||
| 64 | + | ||
| 65 | +## The A/B/C/D extension grading | ||
| 66 | + | ||
| 67 | +From CLAUDE.md guardrail #7. Every extension to vibe_erp falls into one of four grades, ordered from safest to least safe. | ||
| 68 | + | ||
| 69 | +| Grade | What it is | Upgrade safety | | ||
| 70 | +|---|---|---| | ||
| 71 | +| **A** | Tier 1 metadata only — custom fields, forms, workflows, rules, list views, translations entered through the web UI. Stored as rows in `metadata__*` tables, tagged `source = 'user'`. | Always upgrade-safe across **any** core version. | | ||
| 72 | +| **B** | Tier 2 plug-in using only the public `api.v1` surface. | Safe within a major version. Loads cleanly across every `1.x` release. | | ||
| 73 | +| **C** | Tier 2 plug-in using deprecated-but-supported `api.v1` symbols. | Safe until the next major. The plug-in loader emits warnings; the plug-in author should migrate before the next major release. | | ||
| 74 | +| **D** | Tier 2 plug-in reaching into `platform.*` or `pbc.*.internal.*` via reflection. | UNSUPPORTED. The plug-in linter rejects this at install time. Will break on the next core upgrade. | | ||
| 75 | + | ||
| 76 | +Two principles follow from the grading: | ||
| 77 | + | ||
| 78 | +1. Anything a Tier 2 plug-in does should also become possible as a Tier 1 customization over time. Tier 2 is the escape hatch, not the default. | ||
| 79 | +2. If you find yourself wanting to do Grade D, the seam is wrong and `api.v1` needs to grow — deliberately, with a version bump consideration. | ||
| 80 | + | ||
| 81 | +## Reference | ||
| 82 | + | ||
| 83 | +The full reference for every type, method, and contract in `api.v1` is generated from KDoc by **Dokka** and published alongside each release. This document is the conceptual overview; the generated reference is the authoritative per-symbol documentation. (The Dokka site is wired up as part of the v1.0 documentation deliverable; until then, the source under `api/api-v1/src/main/kotlin/org/vibeerp/api/v1/` is the source of truth, and every public type carries a KDoc block — that is part of the PR checklist in [`../../CONTRIBUTING.md`](../../CONTRIBUTING.md).) | ||
| 84 | + | ||
| 85 | +## Where to go next | ||
| 86 | + | ||
| 87 | +- Build your first plug-in: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) | ||
| 88 | +- Author a workflow: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md) | ||
| 89 | +- Author a form: [`../form-authoring/guide.md`](../form-authoring/guide.md) | ||
| 90 | +- Localize a plug-in: [`../i18n/guide.md`](../i18n/guide.md) |
docs/plugin-author/getting-started.md
0 → 100644
| 1 | +# Plug-in author: getting started | ||
| 2 | + | ||
| 3 | +This walkthrough is for a developer building their first vibe_erp plug-in. By the end you will have a JAR that drops into `./plugins/`, registers an extension, exposes a REST endpoint, and shows up in the plug-in loader log on the next restart. | ||
| 4 | + | ||
| 5 | +For the conceptual overview of the plug-in API, read [`../plugin-api/overview.md`](../plugin-api/overview.md) first. For the architectural reasoning behind the constraints, see [`../architecture/overview.md`](../architecture/overview.md) and the full spec at [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 6 | + | ||
| 7 | +## 1. The only dependency you need | ||
| 8 | + | ||
| 9 | +Add `org.vibeerp:api-v1` from Maven Central. **This is the only vibe_erp dependency a plug-in is allowed to declare.** Importing anything from `org.vibeerp.platform.*` or any PBC's internal package will fail the plug-in linter at install time. | ||
| 10 | + | ||
| 11 | +In Gradle (`build.gradle.kts`): | ||
| 12 | + | ||
| 13 | +```kotlin | ||
| 14 | +plugins { | ||
| 15 | + kotlin("jvm") version "2.0.0" | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +repositories { | ||
| 19 | + mavenCentral() | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +dependencies { | ||
| 23 | + compileOnly("org.vibeerp:api-v1:1.0.0") | ||
| 24 | + // Test against the same artifact at runtime | ||
| 25 | + testImplementation("org.vibeerp:api-v1:1.0.0") | ||
| 26 | +} | ||
| 27 | +``` | ||
| 28 | + | ||
| 29 | +`compileOnly` is correct: at runtime the plug-in classloader is given `api.v1` from the host. Bundling it inside your plug-in JAR causes classloader confusion. | ||
| 30 | + | ||
| 31 | +## 2. Implement `org.vibeerp.api.v1.plugin.Plugin` | ||
| 32 | + | ||
| 33 | +The plug-in's entry class is the single point where the host hands you a context and asks you to wire yourself up. | ||
| 34 | + | ||
| 35 | +```kotlin | ||
| 36 | +package com.example.helloprint | ||
| 37 | + | ||
| 38 | +import org.vibeerp.api.v1.plugin.Plugin | ||
| 39 | +import org.vibeerp.api.v1.plugin.PluginContext | ||
| 40 | + | ||
| 41 | +class HelloPrintPlugin : Plugin { | ||
| 42 | + | ||
| 43 | + override fun start(context: PluginContext) { | ||
| 44 | + context.log.info("hello-print plug-in starting") | ||
| 45 | + // Register listeners, seed metadata, etc. | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + override fun stop(context: PluginContext) { | ||
| 49 | + context.log.info("hello-print plug-in stopping") | ||
| 50 | + } | ||
| 51 | +} | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +The host calls `start` after the plug-in's classloader, Spring child context, and Liquibase migrations are ready. Anything the plug-in needs from the host (event bus, translator, repositories, configuration) comes through `PluginContext`. | ||
| 55 | + | ||
| 56 | +## 3. Write a `plugin.yml` manifest | ||
| 57 | + | ||
| 58 | +The manifest sits at the root of the JAR (`src/main/resources/plugin.yml`). It tells the host who you are, what API version you target, and what permissions you need. | ||
| 59 | + | ||
| 60 | +```yaml | ||
| 61 | +id: com.example.helloprint | ||
| 62 | +version: 0.1.0 | ||
| 63 | +requiresApi: "1.x" | ||
| 64 | +name: Hello Print | ||
| 65 | +entryClass: com.example.helloprint.HelloPrintPlugin | ||
| 66 | + | ||
| 67 | +description: > | ||
| 68 | + A minimal worked example for the vibe_erp plug-in author guide. | ||
| 69 | + Registers one extension and exposes one REST endpoint. | ||
| 70 | + | ||
| 71 | +vendor: | ||
| 72 | + name: Example Print Co. | ||
| 73 | + url: https://example.com | ||
| 74 | + | ||
| 75 | +permissions: | ||
| 76 | + - hello_print.greet.read | ||
| 77 | + | ||
| 78 | +metadata: | ||
| 79 | + forms: | ||
| 80 | + - metadata/forms/greeting.json | ||
| 81 | + i18n: | ||
| 82 | + - i18n/en-US.properties | ||
| 83 | + - i18n/de-DE.properties | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +Field notes: | ||
| 87 | + | ||
| 88 | +- `id` is globally unique. Use a reverse-DNS style. The host uses it as the plug-in's schema namespace (`plugin_helloprint__*`) and as the `source` tag (`plugin:com.example.helloprint`) on every metadata row the plug-in seeds. | ||
| 89 | +- `version` is your plug-in's version, not the host's. | ||
| 90 | +- `requiresApi: "1.x"` means "any 1.x release of `api.v1`". A mismatch fails at install time, not at runtime. Across a major version, the host loads `api.v1` and `api.v2` side by side for at least one major release window. | ||
| 91 | +- `permissions` are auto-registered with the role editor. Plug-ins should not invent permissions outside their own namespace. | ||
| 92 | +- `metadata` lists files inside the JAR that the host should pick up on plug-in start: form definitions, BPMN files, message bundles, seed rules. | ||
| 93 | + | ||
| 94 | +## 4. Register an extension via `@Extension` | ||
| 95 | + | ||
| 96 | +Extensions are how a plug-in plugs into a typed seam declared by the core or another PBC. Every seam lives in `org.vibeerp.api.v1.ext.<area>`. | ||
| 97 | + | ||
| 98 | +```kotlin | ||
| 99 | +package com.example.helloprint | ||
| 100 | + | ||
| 101 | +import org.vibeerp.api.v1.plugin.Extension | ||
| 102 | +import org.vibeerp.api.v1.workflow.TaskHandler | ||
| 103 | +import org.vibeerp.api.v1.workflow.WorkflowTask | ||
| 104 | + | ||
| 105 | +@Extension(point = TaskHandler::class) | ||
| 106 | +class GreetCustomerHandler : TaskHandler { | ||
| 107 | + | ||
| 108 | + override val id: String = "hello_print.greet_customer" | ||
| 109 | + | ||
| 110 | + override fun handle(task: WorkflowTask) { | ||
| 111 | + val name = task.variable<String>("customerName") ?: "world" | ||
| 112 | + task.setVariable("greeting", "hello, $name") | ||
| 113 | + task.complete() | ||
| 114 | + } | ||
| 115 | +} | ||
| 116 | +``` | ||
| 117 | + | ||
| 118 | +The host scans `@Extension`-annotated classes inside the plug-in JAR and registers them against the declared extension point. A BPMN service task referencing `hello_print.greet_customer` will now be routed to this handler. | ||
| 119 | + | ||
| 120 | +## 5. Add a `@PluginEndpoint` controller | ||
| 121 | + | ||
| 122 | +Plug-ins add REST endpoints through `@PluginEndpoint`. The endpoint runs inside the plug-in's Spring child context and is auto-listed in the OpenAPI document under the plug-in's namespace. | ||
| 123 | + | ||
| 124 | +```kotlin | ||
| 125 | +package com.example.helloprint | ||
| 126 | + | ||
| 127 | +import org.vibeerp.api.v1.http.PluginEndpoint | ||
| 128 | +import org.vibeerp.api.v1.http.RequestContext | ||
| 129 | +import org.vibeerp.api.v1.http.ResponseBuilder | ||
| 130 | +import org.vibeerp.api.v1.i18n.MessageKey | ||
| 131 | +import org.vibeerp.api.v1.security.Permission | ||
| 132 | + | ||
| 133 | +@PluginEndpoint( | ||
| 134 | + path = "/api/plugin/hello-print/greet", | ||
| 135 | + method = "GET", | ||
| 136 | + permission = "hello_print.greet.read", | ||
| 137 | +) | ||
| 138 | +class GreetEndpoint { | ||
| 139 | + | ||
| 140 | + fun handle(request: RequestContext): ResponseBuilder { | ||
| 141 | + val name = request.query("name") ?: "world" | ||
| 142 | + val greeting = request.translator.format( | ||
| 143 | + MessageKey("hello_print.greet.message"), | ||
| 144 | + mapOf("name" to name), | ||
| 145 | + ) | ||
| 146 | + return ResponseBuilder.ok(mapOf("greeting" to greeting)) | ||
| 147 | + } | ||
| 148 | +} | ||
| 149 | +``` | ||
| 150 | + | ||
| 151 | +A few things this snippet is doing on purpose: | ||
| 152 | + | ||
| 153 | +- The endpoint declares a `permission`. The plug-in loader registers `hello_print.greet.read` with the role editor; an admin grants it before the endpoint becomes callable. | ||
| 154 | +- The greeting goes through the `Translator`. There is **no** string concatenation in user-facing code. See the [i18n guide](../i18n/guide.md). | ||
| 155 | +- The handler returns through `ResponseBuilder`, not through Spring's `ResponseEntity`. Plug-ins do not see Spring directly. | ||
| 156 | + | ||
| 157 | +## 6. Loading the plug-in | ||
| 158 | + | ||
| 159 | +Build the JAR and drop it into the host's plug-in directory: | ||
| 160 | + | ||
| 161 | +```bash | ||
| 162 | +./gradlew :jar | ||
| 163 | +cp build/libs/hello-print-0.1.0.jar /opt/vibe-erp/plugins/ | ||
| 164 | +# or, for a Docker deployment | ||
| 165 | +docker cp build/libs/hello-print-0.1.0.jar vibe-erp:/opt/vibe-erp/plugins/ | ||
| 166 | +``` | ||
| 167 | + | ||
| 168 | +Restart the host: | ||
| 169 | + | ||
| 170 | +```bash | ||
| 171 | +docker restart vibe-erp | ||
| 172 | +``` | ||
| 173 | + | ||
| 174 | +Hot reload of plug-ins without a restart is not in v1.0 — it is on the v1.2+ roadmap. For v1.0 and v1.1, install and uninstall require a restart. | ||
| 175 | + | ||
| 176 | +On boot, the host scans `./plugins/`, validates each manifest, runs the plug-in linter, creates a classloader and Spring child context per plug-in, runs the plug-in's Liquibase changesets in `plugin_<id>__*`, seeds metadata, and finally calls `Plugin.start`. The lifecycle in full is described in section 7 of [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 177 | + | ||
| 178 | +## 7. Where to find logs and errors | ||
| 179 | + | ||
| 180 | +- **Boot log:** `/opt/vibe-erp/logs/vibe-erp.log`. Filter on the `platform-plugins` logger to see exactly which plug-ins were scanned, accepted, rejected, and why. | ||
| 181 | +- **Linter rejections:** if your JAR imports a class outside `org.vibeerp.api.v1.*`, the linter rejects it at install time and logs the offending import. Fix the import; never work around the linter. | ||
| 182 | +- **Migration failures:** Liquibase output is logged under the `platform-persistence` logger, scoped to your plug-in id. | ||
| 183 | +- **Runtime errors inside your plug-in:** logged under the plug-in's id (`com.example.helloprint`). Use the `PluginContext.log` handed to you in `start`; do not pull in your own logging framework. | ||
| 184 | +- **Health endpoint:** `/actuator/health` reports per-plug-in status. A plug-in stuck in a half-loaded state shows up here before it shows up in user-visible failures. | ||
| 185 | + | ||
| 186 | +## 8. The reference printing-shop plug-in is your worked example | ||
| 187 | + | ||
| 188 | +`reference-customer/plugin-printing-shop/` is a real, production-shaped plug-in built and CI-tested on every PR. It expresses the workflows in `raw/业务流程设计文档/` using **only `api.v1`**. When this guide leaves a question unanswered, the answer is almost always "look at how `plugin-printing-shop` does it": | ||
| 189 | + | ||
| 190 | +- The shape of `plugin.yml` for a non-trivial plug-in. | ||
| 191 | +- How a plug-in declares a brand-new entity (printing plates, presses, color proofs) without touching any PBC. | ||
| 192 | +- How a multi-step BPMN workflow is shipped inside a JAR and wired up to typed `TaskHandler` implementations. | ||
| 193 | +- How form definitions reference custom fields. | ||
| 194 | +- How i18n bundles are organized for a plug-in that ships in five locales. | ||
| 195 | + | ||
| 196 | +The plug-in is built by the main Gradle build but **not loaded by default**. Drop its JAR into `./plugins/` to load it locally. | ||
| 197 | + | ||
| 198 | +## What this guide does not yet cover | ||
| 199 | + | ||
| 200 | +- Publishing a plug-in to a marketplace — the marketplace and signed plug-ins are a v2 deliverable. | ||
| 201 | +- Hot reload without restart — v1.2+. | ||
| 202 | +- Calling the MCP endpoint as an AI agent — v1.1. | ||
| 203 | + | ||
| 204 | +For everything else, the source of truth is [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). |
docs/superpowers/specs/2026-04-07-vibe-erp-implementation-plan.md
0 → 100644
| 1 | +# vibe_erp — Implementation Plan (post-v0.1) | ||
| 2 | + | ||
| 3 | +**Status:** Approved roadmap. v0.1 skeleton is built. This document is the source of truth for the work that turns v0.1 into v1.0. | ||
| 4 | +**Date:** 2026-04-07 | ||
| 5 | +**Companion document:** [Architecture spec](2026-04-07-vibe-erp-architecture-design.md) | ||
| 6 | + | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +## What v0.1 (this commit) actually delivers | ||
| 10 | + | ||
| 11 | +A buildable, runnable, tested skeleton of the vibe_erp framework. Concretely: | ||
| 12 | + | ||
| 13 | +| Item | Status | | ||
| 14 | +|---|---| | ||
| 15 | +| Gradle multi-project with 7 subprojects | ✅ Builds | | ||
| 16 | +| `api/api-v1` — public plug-in contract (~30 files, semver-governed) | ✅ Compiles, unit-tested | | ||
| 17 | +| `platform/platform-bootstrap` — Spring Boot main, config, tenant filter | ✅ Compiles | | ||
| 18 | +| `platform/platform-persistence` — multi-tenant JPA scaffolding, audit base | ✅ Compiles | | ||
| 19 | +| `platform/platform-plugins` — PF4J host, lifecycle | ✅ Compiles | | ||
| 20 | +| `pbc/pbc-identity` — User entity end-to-end (entity → repo → service → REST) | ✅ Compiles, unit-tested | | ||
| 21 | +| `reference-customer/plugin-printing-shop` — hello-world PF4J plug-in | ✅ Compiles, packs into a real plug-in JAR with PF4J manifest | | ||
| 22 | +| `distribution` — Spring Boot fat jar (`vibe-erp.jar`, ~59 MB) | ✅ `bootJar` succeeds | | ||
| 23 | +| Liquibase changelogs (platform init + identity init) | ✅ Schema-valid, runs against Postgres on first boot | | ||
| 24 | +| Dockerfile (multi-stage), docker-compose.yaml (app + Postgres), Makefile | ✅ Present, image build untested in this session | | ||
| 25 | +| GitHub Actions workflows (build + architecture-rule check) | ✅ Present, untested in CI | | ||
| 26 | +| Architecture-rule enforcement in `build.gradle.kts` | ✅ Active — fails the build if a PBC depends on another PBC, or if a plug-in depends on `platform/*` | | ||
| 27 | +| README, CONTRIBUTING, LICENSE, 8 docs guides | ✅ Present | | ||
| 28 | +| Architecture spec (this folder) | ✅ Present | | ||
| 29 | +| Implementation plan (this file) | ✅ Present | | ||
| 30 | + | ||
| 31 | +**What v0.1 explicitly does NOT deliver:** | ||
| 32 | + | ||
| 33 | +- The 9 other core PBCs (catalog, partners, inventory, warehousing, orders-sales, orders-purchase, production, quality, finance) | ||
| 34 | +- The metadata store machinery (custom-field application, form designer, list views, rules engine) | ||
| 35 | +- Embedded Flowable workflow engine | ||
| 36 | +- ICU4J `Translator` implementation | ||
| 37 | +- Postgres Row-Level Security `SET LOCAL` hook (the policies exist; the hook that sets `vibeerp.current_tenant` per transaction is deferred) | ||
| 38 | +- JasperReports | ||
| 39 | +- OIDC integration | ||
| 40 | +- React web SPA | ||
| 41 | +- Plug-in linter (rejecting reflection into platform internals at install time) | ||
| 42 | +- Plug-in Liquibase application | ||
| 43 | +- Per-plug-in Spring child context | ||
| 44 | +- MCP server | ||
| 45 | +- Mobile app (v2 anyway) | ||
| 46 | + | ||
| 47 | +The architecture **accommodates** all of these — the seams are in `api.v1` already — they just aren't implemented. | ||
| 48 | + | ||
| 49 | +--- | ||
| 50 | + | ||
| 51 | +## How to read this plan | ||
| 52 | + | ||
| 53 | +Each work unit is sized to be a single coherent implementation pass: roughly 1–5 days of work, one developer, one PR. Units are grouped by theme, ordered roughly by dependency, but parallelizable wherever the prerequisites are satisfied. | ||
| 54 | + | ||
| 55 | +Each unit names: | ||
| 56 | +- the **Gradle modules** it touches (or creates) | ||
| 57 | +- the **api.v1 surface** it consumes (and never modifies, unless explicitly noted) | ||
| 58 | +- the **acceptance test** that proves it landed correctly | ||
| 59 | +- any **upstream dependencies** that must complete first | ||
| 60 | + | ||
| 61 | +The 11 architecture guardrails in `CLAUDE.md` and the 14-section design in `2026-04-07-vibe-erp-architecture-design.md` apply to every unit. This document does not restate them. | ||
| 62 | + | ||
| 63 | +--- | ||
| 64 | + | ||
| 65 | +## Phase 1 — Platform completion (the foundation everything else needs) | ||
| 66 | + | ||
| 67 | +These units finish the platform layer so PBCs can be implemented without inventing scaffolding. | ||
| 68 | + | ||
| 69 | +### P1.1 — Postgres RLS transaction hook | ||
| 70 | +**Module:** `platform-persistence` | ||
| 71 | +**Why:** v0.1 enables RLS policies but never sets `current_setting('vibeerp.current_tenant')`. Without the hook, RLS-enabled tables return zero rows. **Block on this for any read/write integration test.** | ||
| 72 | +**What:** A Hibernate `StatementInspector` (or a Spring `TransactionSynchronization`) that runs `SET LOCAL vibeerp.current_tenant = '<tenant_id>'` at the start of every transaction, reading from `TenantContext`. | ||
| 73 | +**Acceptance:** integration test with Testcontainers Postgres, two tenants, one user per tenant, asserts that tenant A's queries return zero rows from tenant B's data even when the Hibernate filter is bypassed. | ||
| 74 | + | ||
| 75 | +### P1.2 — Plug-in linter | ||
| 76 | +**Module:** `platform-plugins` | ||
| 77 | +**Depends on:** — | ||
| 78 | +**What:** At plug-in load time, scan the JAR's class files for any reference to `org.vibeerp.platform.*` or `org.vibeerp.pbc.*.internal.*` packages. If any are found, refuse to load and report a "grade D extension" error to the operator. Reuse ASM (already a transitive dep of Spring) for bytecode inspection. | ||
| 79 | +**Acceptance:** unit test with a hand-crafted bad JAR that imports a fictitious `org.vibeerp.platform.foo.Bar` — the loader rejects it with a clear message. | ||
| 80 | + | ||
| 81 | +### P1.3 — Per-plug-in Spring child context | ||
| 82 | +**Module:** `platform-plugins` | ||
| 83 | +**Depends on:** P1.2 (linter must run first) | ||
| 84 | +**What:** When a plug-in starts, create a Spring `AnnotationConfigApplicationContext` with the host context as parent. Register the plug-in's `@Component` classes (discovered via classpath scanning of its classloader). The plug-in's `Plugin.start(context: PluginContext)` is invoked with a `PluginContext` whose dependencies are wired from the parent context. | ||
| 85 | +**Acceptance:** the reference printing-shop plug-in declares an `@Extension` and a `@PluginEndpoint` controller; the integration test confirms the endpoint is mounted under `/api/v1/plugins/printing-shop/...` and the extension is invocable. | ||
| 86 | + | ||
| 87 | +### P1.4 — Plug-in Liquibase application | ||
| 88 | +**Module:** `platform-plugins` + `platform-persistence` | ||
| 89 | +**Depends on:** P1.3 | ||
| 90 | +**What:** When a plug-in starts, look for `db/changelog/master.xml` in its JAR; if present, run it through Liquibase against the host's datasource with the plug-in's id as the changelog `contexts` filter. Tables created live under the `plugin_<id>__*` prefix (enforced by lint of the changelog at load time, not runtime). | ||
| 91 | +**Acceptance:** a test plug-in ships a changelog that creates `plugin_test__widget`; after load, the table exists. Uninstall removes the table after operator confirmation. | ||
| 92 | + | ||
| 93 | +### P1.5 — Metadata store seeding | ||
| 94 | +**Module:** new `platform-metadata` module (or expand `platform-persistence`) | ||
| 95 | +**Depends on:** — | ||
| 96 | +**What:** A `MetadataLoader` Spring component that, on boot and on plug-in start, upserts rows into `metadata__entity`, `metadata__form`, `metadata__workflow`, etc. from YAML files in the classpath / plug-in JAR. Each row carries `source = 'core' | 'plugin:<id>' | 'user'`. Idempotent; safe to run on every boot. | ||
| 97 | +**Acceptance:** integration test confirms that booting twice doesn't duplicate rows; uninstalling a plug-in removes its `source = 'plugin:<id>'` rows; user-edited rows (`source = 'user'`) are never touched. | ||
| 98 | + | ||
| 99 | +### P1.6 — `Translator` implementation backed by ICU4J | ||
| 100 | +**Module:** new `platform-i18n` module | ||
| 101 | +**Depends on:** P1.5 (so plug-in message bundles can be loaded as part of metadata) | ||
| 102 | +**What:** Implement `org.vibeerp.api.v1.i18n.Translator` using ICU4J `MessageFormat` with locale fallback. Resolve message bundles in this order: `metadata__translation` (tenant overrides) → plug-in `i18n/messages_<locale>.properties` → core `i18n/messages_<locale>.properties` → fallback locale. | ||
| 103 | +**Acceptance:** unit tests covering plurals, gender, number formatting in en-US, zh-CN, de-DE, ja-JP, es-ES; a test that a tenant override beats a plug-in default beats a core default. | ||
| 104 | + | ||
| 105 | +### P1.7 — Event bus + outbox | ||
| 106 | +**Module:** new `platform-events` module | ||
| 107 | +**Depends on:** — | ||
| 108 | +**What:** Implement `org.vibeerp.api.v1.event.EventBus` with two parts: (a) an in-process Spring `ApplicationEventPublisher` for synchronous delivery to listeners in the same process; (b) an `event_outbox` table written in the same DB transaction as the originating change, scanned by a background poller, marked dispatched after delivery. The outbox is the seam where Kafka/NATS plugs in later. | ||
| 109 | +**Acceptance:** integration test that an OrderCreated event survives a process crash between the original transaction commit and a downstream listener — the listener fires when the process restarts. | ||
| 110 | + | ||
| 111 | +### P1.8 — JasperReports integration | ||
| 112 | +**Module:** new `platform-reporting` module | ||
| 113 | +**Depends on:** P1.5 | ||
| 114 | +**What:** Wrap JasperReports compilation/rendering behind a `ReportRenderer` Spring component. Reports live in `metadata__report` (path or content). API exposes `/api/v1/_meta/reports/{id}.pdf?param=...` for synchronous rendering, with a job-based async path for big reports. | ||
| 115 | +**Acceptance:** ship one core report (a User list) and confirm it renders to PDF with the correct locale-aware date and number formatting. | ||
| 116 | + | ||
| 117 | +### P1.9 — File store (local + S3) | ||
| 118 | +**Module:** new `platform-files` module | ||
| 119 | +**Depends on:** — | ||
| 120 | +**What:** A `FileStore` interface with `LocalFileStore` and `S3FileStore` implementations, configured via `vibeerp.files.backend`. Each stored file has a `tenant_id` namespace baked into its key, so a misconfigured S3 bucket can't accidentally serve cross-tenant files. | ||
| 121 | +**Acceptance:** unit tests for both backends; integration test with MinIO Testcontainer for S3. | ||
| 122 | + | ||
| 123 | +### P1.10 — Job scheduler | ||
| 124 | +**Module:** new `platform-jobs` module | ||
| 125 | +**Depends on:** — | ||
| 126 | +**What:** Quartz integration with the `JDBCJobStore` against the same Postgres. PBCs and plug-ins register jobs through a typed `JobScheduler` API in api.v1 (already has the seam, may need a tiny addition). Each job runs inside a `TenantContext.runAs(tenantId) { ... }` block — never inheriting a stale tenant from the trigger thread. | ||
| 127 | +**Acceptance:** a recurring core job that prunes the audit log; integration test confirms the job runs in the right tenant context. | ||
| 128 | + | ||
| 129 | +--- | ||
| 130 | + | ||
| 131 | +## Phase 2 — Embedded workflow engine | ||
| 132 | + | ||
| 133 | +### P2.1 — Embedded Flowable | ||
| 134 | +**Module:** new `platform-workflow` module | ||
| 135 | +**Depends on:** P1.5 (metadata), P1.6 (i18n for task names), P1.7 (events) | ||
| 136 | +**What:** Pull in `flowable-spring-boot-starter`. Configure Flowable to use the same Postgres datasource (so its tables live alongside ours, prefixed `flowable_*`). Wire `org.vibeerp.api.v1.workflow.TaskHandler` implementations registered by plug-ins as Flowable `JavaDelegate`s. Workflows are deployed from `metadata__workflow` rows (BPMN XML payload) on boot and on plug-in load. | ||
| 137 | +**Acceptance:** the reference printing-shop plug-in registers a "quote-to-job-card" `TaskHandler`; a BPMN process that calls it can be started via REST and returns the expected output. | ||
| 138 | + | ||
| 139 | +### P2.2 — BPMN designer integration (web) | ||
| 140 | +**Module:** the future React SPA | ||
| 141 | +**Depends on:** P2.1, R1 (web bootstrap) | ||
| 142 | +**What:** Embed `bpmn-js` (Camunda's BPMN modeler) in a "Workflow Designer" page. Save as a `metadata__workflow` row; deploy through the Tier 1 path. | ||
| 143 | +**Acceptance:** end-to-end test where a Tier 1 user creates a workflow in the browser and starts an instance of it. | ||
| 144 | + | ||
| 145 | +### P2.3 — User task / form rendering | ||
| 146 | +**Module:** `platform-workflow` + future React SPA | ||
| 147 | +**Depends on:** P2.1, F1 (form designer), R1 | ||
| 148 | +**What:** When a Flowable user task fires, look up the form id from the task definition, render it via the form renderer (P3.x), capture the user's input, and complete the task. | ||
| 149 | +**Acceptance:** end-to-end test of an approval workflow where a human clicks a button. | ||
| 150 | + | ||
| 151 | +--- | ||
| 152 | + | ||
| 153 | +## Phase 3 — Metadata store: forms and rules (the Tier 1 user journeys) | ||
| 154 | + | ||
| 155 | +### P3.1 — JSON Schema form renderer (server) | ||
| 156 | +**Module:** new `platform-forms` module | ||
| 157 | +**Depends on:** P1.5 | ||
| 158 | +**What:** A `FormRenderer` that, given an entity instance and a `FormSchema`, returns a server-side JSON describing what to render. The actual rendering happens in the SPA (P3.2). The server-side piece is needed for non-browser clients (CLI, integration tests, AI agents). | ||
| 159 | +**Acceptance:** snapshot tests of the rendered JSON for a User form with an added custom field. | ||
| 160 | + | ||
| 161 | +### P3.2 — Form renderer (web) | ||
| 162 | +**Module:** future React SPA | ||
| 163 | +**Depends on:** P3.1, R1 | ||
| 164 | +**What:** A React component that consumes JSON Schema + UI Schema and renders an interactive form. Use `react-jsonschema-form` as the base, customize the widget set for vibe_erp's design language. | ||
| 165 | +**Acceptance:** Storybook for every widget type; e2e test that filling and submitting a form persists the data. | ||
| 166 | + | ||
| 167 | +### P3.3 — Form designer (web) | ||
| 168 | +**Module:** future React SPA | ||
| 169 | +**Depends on:** P3.2 | ||
| 170 | +**What:** A drag-and-drop designer that produces JSON Schema + UI Schema and saves into `metadata__form`. This is the Tier 1 entry point for "add a field, change a layout". | ||
| 171 | +**Acceptance:** e2e test where a Tier 1 user adds a "Customer PO Reference" field to the Sales Order form without writing code. | ||
| 172 | + | ||
| 173 | +### P3.4 — Custom field application | ||
| 174 | +**Module:** `platform-metadata` | ||
| 175 | +**Depends on:** P1.5 | ||
| 176 | +**What:** A Hibernate interceptor (or JPA listener) that reads the entity's `metadata__custom_field` rows on save and validates the JSON in the `ext` column against them — required, type, max length, etc. Validation errors come back as 422 with a per-field map. | ||
| 177 | +**Acceptance:** integration test that a User with a missing required custom field cannot be saved; one with a valid one round-trips. | ||
| 178 | + | ||
| 179 | +### P3.5 — Rules engine (simple "if X then Y") | ||
| 180 | +**Module:** new `platform-rules` module | ||
| 181 | +**Depends on:** P1.7 (events) | ||
| 182 | +**What:** Listen to `DomainEvent`s, evaluate `metadata__rule` rows scoped to that event type, run the configured action (send email, set field, call workflow, publish another event). Use a simple expression language (something like `JEXL` or Spring SpEL — bias toward Spring SpEL since we already have it). | ||
| 183 | +**Acceptance:** integration test where a rule "on UserCreated, set ext.greeting = 'Welcome'" actually fires. | ||
| 184 | + | ||
| 185 | +### P3.6 — List view designer (web) | ||
| 186 | +**Module:** future React SPA | ||
| 187 | +**Depends on:** R1 | ||
| 188 | +**What:** A grid component backed by `metadata__list_view` rows. Tier 1 users pick columns, filters, sort, save as a named list view, share with their tenant. | ||
| 189 | +**Acceptance:** e2e test where a user creates a "Active customers in Germany" list view and re-opens it later. | ||
| 190 | + | ||
| 191 | +--- | ||
| 192 | + | ||
| 193 | +## Phase 4 — Authentication and authorization (real) | ||
| 194 | + | ||
| 195 | +### P4.1 — Built-in JWT auth | ||
| 196 | +**Module:** new `pbc-auth` (under `pbc/`) + `platform-security` (might already exist) | ||
| 197 | +**Depends on:** — | ||
| 198 | +**What:** Username/password login backed by `identity__user_credential` (separate table from `identity__user` so the User entity stays small). Argon2id for hashing. Issue a signed JWT with `sub`, `tenant`, `roles`, `iat`, `exp`. Validate on every request via a Spring Security filter. | ||
| 199 | +**Acceptance:** integration test of full login/logout/refresh flow; assertion that the JWT carries `tenant` and that downstream PBC code sees it via `Principal`. | ||
| 200 | + | ||
| 201 | +### P4.2 — OIDC integration | ||
| 202 | +**Module:** `pbc-auth` | ||
| 203 | +**Depends on:** P4.1 | ||
| 204 | +**What:** Plug Spring Security's OIDC client into the same `Principal` resolution. Configurable per tenant: which provider, which client id, which claim maps to `tenant`. Keycloak is the smoke-test target. | ||
| 205 | +**Acceptance:** Keycloak Testcontainer + integration test of a full OIDC code flow; the same `UserController` endpoints work without changes. | ||
| 206 | + | ||
| 207 | +### P4.3 — Permission checking (real) | ||
| 208 | +**Module:** `platform-security` | ||
| 209 | +**Depends on:** P4.1 | ||
| 210 | +**What:** Implement `org.vibeerp.api.v1.security.PermissionCheck`. Resolve permissions from `metadata__role_permission` rows for the current `Principal`'s roles. Plug-ins register their own permissions via `@Extension(point = PermissionRegistrar::class)` (api.v1 may need a tiny addition here — deliberate, with a minor version bump). | ||
| 211 | +**Acceptance:** integration test that a user without `identity.user.create` gets 403 from `POST /api/v1/identity/users`; with the role, 201. | ||
| 212 | + | ||
| 213 | +--- | ||
| 214 | + | ||
| 215 | +## Phase 5 — Core PBCs (the meat of the framework) | ||
| 216 | + | ||
| 217 | +Each PBC follows the same 7-step recipe: | ||
| 218 | + | ||
| 219 | +1. Create the Gradle subproject under `pbc/` | ||
| 220 | +2. Domain entities (extending `AuditedJpaEntity`, with `ext jsonb`) | ||
| 221 | +3. Spring Data JPA repositories | ||
| 222 | +4. Application services | ||
| 223 | +5. REST controllers under `/api/v1/<pbc>/<resource>` | ||
| 224 | +6. Liquibase changelog (`db/changelog/<pbc>/`) with rollback blocks AND RLS policies | ||
| 225 | +7. Cross-PBC facade in `api.v1.ext.<pbc>` + adapter in the PBC | ||
| 226 | + | ||
| 227 | +### P5.1 — `pbc-catalog` — items, units of measure, attributes | ||
| 228 | +Foundation for everything that's bought, sold, made, or stored. Hold off on price lists and configurable products until P5.10. | ||
| 229 | + | ||
| 230 | +### P5.2 — `pbc-partners` — customers, suppliers, contacts | ||
| 231 | +Companies, addresses, contacts, contact channels. PII-tagged from day one (CLAUDE.md guardrail #6 / DSAR). | ||
| 232 | + | ||
| 233 | +### P5.3 — `pbc-inventory` — stock items, lots, locations, movements | ||
| 234 | +Movements as immutable events; on-hand quantity is a projection. Locations are hierarchical. | ||
| 235 | + | ||
| 236 | +### P5.4 — `pbc-warehousing` — receipts, picks, transfers, counts | ||
| 237 | +Built on top of `inventory` via `api.v1.ext.inventory`. The first PBC that proves the cross-PBC dependency rule under load. | ||
| 238 | + | ||
| 239 | +### P5.5 — `pbc-orders-sales` — quotes, sales orders, deliveries | ||
| 240 | +Workflow-heavy. The reference printing-shop plug-in's quote-to-job-card flow exercises this. | ||
| 241 | + | ||
| 242 | +### P5.6 — `pbc-orders-purchase` — RFQs, POs, receipts | ||
| 243 | +Mirror image of sales. Shares the document/approval pattern. | ||
| 244 | + | ||
| 245 | +### P5.7 — `pbc-production` (basic) — work orders, routings, operations | ||
| 246 | +Generic discrete-manufacturing primitives. NOT printing-specific. | ||
| 247 | + | ||
| 248 | +### P5.8 — `pbc-quality` (basic) — inspection plans, results, holds | ||
| 249 | +Generic; the printing-specific QC steps live in the plug-in. | ||
| 250 | + | ||
| 251 | +### P5.9 — `pbc-finance` (basic) — GL, journal entries, AR/AP minimal | ||
| 252 | +Basic double-entry. Tax engines, multi-currency revaluation, consolidation are v1.2+. | ||
| 253 | + | ||
| 254 | +### P5.10 — `pbc-catalog` — pricing, configurable products | ||
| 255 | +Defer until P5.5 (orders-sales) is real, since pricing only matters when orders exist. | ||
| 256 | + | ||
| 257 | +--- | ||
| 258 | + | ||
| 259 | +## Phase 6 — Web SPA | ||
| 260 | + | ||
| 261 | +### R1 — React + TypeScript bootstrap | ||
| 262 | +**Module:** new `web/` directory (Vite + React + TS) | ||
| 263 | +**Depends on:** P4.1 (need a way to log in) | ||
| 264 | +**What:** Vite scaffold, design system (likely Mantine or Radix), routing (React Router), data fetching (TanStack Query), generated TypeScript client from the backend's OpenAPI spec. | ||
| 265 | +**Acceptance:** can log in, see a list of users, log out. | ||
| 266 | + | ||
| 267 | +### R2 — Identity screens | ||
| 268 | +**Depends on:** R1 | ||
| 269 | +**What:** User list, user detail, role list, role detail, permission editor. | ||
| 270 | + | ||
| 271 | +### R3 — Customize / metadata UIs | ||
| 272 | +**Depends on:** R1, P3.3, P3.6 | ||
| 273 | +**What:** The Tier 1 entry point. Form designer, list view designer, custom field designer, workflow designer. | ||
| 274 | + | ||
| 275 | +### R4 — One screen per core PBC | ||
| 276 | +**Depends on:** R1, the matching PBC from Phase 5 | ||
| 277 | +**What:** Per-PBC list / detail / create / edit screens. Generated from metadata wherever possible; hand-built where the metadata-driven approach falls short (and those gaps become future enhancements to the metadata store). | ||
| 278 | + | ||
| 279 | +--- | ||
| 280 | + | ||
| 281 | +## Phase 7 — Reference plug-in (the executable acceptance test) | ||
| 282 | + | ||
| 283 | +### REF.1 — Real workflow handler | ||
| 284 | +**Module:** `reference-customer/plugin-printing-shop` | ||
| 285 | +**Depends on:** P2.1 (Flowable) | ||
| 286 | +**What:** Replace the v0.1 hello-world with a real `TaskHandler` that converts a Quote into a JobCard, registered as `acme.printingshop.quoteToJobCard`. A BPMN file in the plug-in JAR drives a workflow that calls this handler. | ||
| 287 | +**Acceptance:** integration test starts the workflow with a real Quote and asserts the JobCard appears. | ||
| 288 | + | ||
| 289 | +### REF.2 — Plate / ink / press domain | ||
| 290 | +**Module:** same plug-in | ||
| 291 | +**Depends on:** P1.4 (plug-in Liquibase application) | ||
| 292 | +**What:** The plug-in defines its own entities (`plugin_printingshop__plate`, `plugin_printingshop__ink_recipe`, `plugin_printingshop__press`) via its own Liquibase changelog. It registers REST endpoints for them via `@PluginEndpoint`. It registers permissions like `printingshop.plate.approve`. | ||
| 293 | +**Acceptance:** integration test that a printing-shop user can CRUD plates against `/api/v1/plugins/printing-shop/plates`, scoped to their tenant. | ||
| 294 | + | ||
| 295 | +### REF.3 — Real reference forms and metadata | ||
| 296 | +**Module:** same plug-in | ||
| 297 | +**Depends on:** P3.3, P3.4 | ||
| 298 | +**What:** The plug-in ships YAML metadata for: a custom field on Sales Order ("printing job type"), a custom form for plate approval, an automation rule "on plate approved, dispatch to press". All loaded at plug-in start, all tagged `source = 'plugin:printing-shop'`. | ||
| 299 | +**Acceptance:** end-to-end smoke test that loads the plug-in into a fresh tenant and exercises the full quote-to-job-card-to-press flow without any code changes outside the plug-in. | ||
| 300 | + | ||
| 301 | +--- | ||
| 302 | + | ||
| 303 | +## Phase 8 — Hosted, AI agents, mobile (post-v1.0) | ||
| 304 | + | ||
| 305 | +### H1 — Per-region tenant routing (hosted) | ||
| 306 | +Make `platform-persistence` look up the tenant's region from `identity__tenant` and route the connection accordingly. v1.0 doesn't ship hosted, but the routing seam should land in v1.0 so v2.0 can flip a switch. | ||
| 307 | + | ||
| 308 | +### H2 — Tenant provisioning UI | ||
| 309 | +A per-instance "operator console" that creates tenants, manages quotas, watches health. Separate React app, separate URL. | ||
| 310 | + | ||
| 311 | +### A1 — MCP server | ||
| 312 | +**Module:** new `platform-mcp` | ||
| 313 | +**Depends on:** every PBC, every endpoint having a typed OpenAPI definition | ||
| 314 | +**What:** Expose every REST endpoint as an MCP function. Permission-checked, locale-aware, tenant-aware. A Tier 2 plug-in can register additional MCP functions via a new `@McpFunction` annotation in api.v1 (an additive change, minor version bump). | ||
| 315 | +**Acceptance:** an LLM-driven agent can call `vibe_erp.identity.users.create` and the result is identical to a REST call. | ||
| 316 | + | ||
| 317 | +### M1 — React Native skeleton | ||
| 318 | +Shared TypeScript types with the web SPA; first screens cover shop-floor scanning and approvals. | ||
| 319 | + | ||
| 320 | +--- | ||
| 321 | + | ||
| 322 | +## Tracking and ordering | ||
| 323 | + | ||
| 324 | +A coarse dependency view: | ||
| 325 | + | ||
| 326 | +``` | ||
| 327 | +P1.1 — RLS hook ─────┐ | ||
| 328 | +P1.2 — linter ──┐ │ | ||
| 329 | +P1.3 — child ctx ┘ │ | ||
| 330 | +P1.4 — plug-in liquibase ─ depends on P1.3 | ||
| 331 | +P1.5 — metadata seed ──┐ ──┐ | ||
| 332 | +P1.6 — translator ──── (depends on P1.5) | ||
| 333 | +P1.7 — events ─────────┘ │ | ||
| 334 | +P1.8 — reports ── (depends on P1.5) | ||
| 335 | +P1.9 — files | ||
| 336 | +P1.10 — jobs | ||
| 337 | +P2.1 — Flowable ── (depends on P1.5, P1.6, P1.7) | ||
| 338 | +P3.x — metadata Tier 1 ── (depends on P1.5) | ||
| 339 | +P4.x — auth | ||
| 340 | +P5.x — core PBCs ── (depend on P1.x complete + P4.x for permission checks) | ||
| 341 | +R1 — web bootstrap ── (depends on P4.1) | ||
| 342 | +REF.x — reference plug-in ── (depends on P1.4 + P2.1 + P3.3) | ||
| 343 | +``` | ||
| 344 | + | ||
| 345 | +Sensible ordering for one developer: | ||
| 346 | +P1.1 → P1.5 → P1.7 → P1.6 → P1.10 → P1.4 → P1.3 → P1.2 → P2.1 → P3.4 → P3.1 → P4.1 → P5.1 → P5.2 → ... | ||
| 347 | + | ||
| 348 | +Sensible parallel ordering for a team of three: | ||
| 349 | +- Dev A: P1.1, P1.5, P1.7, P1.6, P2.1 | ||
| 350 | +- Dev B: P1.4, P1.3, P1.2, P3.4, P3.1 | ||
| 351 | +- Dev C: P4.1, P4.3, P5.1 (catalog), P5.2 (partners) | ||
| 352 | + | ||
| 353 | +Then converge on R1 (web) and the rest of Phase 5 in parallel. | ||
| 354 | + | ||
| 355 | +--- | ||
| 356 | + | ||
| 357 | +## Definition of "v1.0 ready" | ||
| 358 | + | ||
| 359 | +v1.0 ships when, on a fresh Postgres, an operator can: | ||
| 360 | + | ||
| 361 | +1. `docker run` the image | ||
| 362 | +2. Log in to the web SPA | ||
| 363 | +3. Create a tenant (or use the default tenant in self-host mode) | ||
| 364 | +4. Drop the printing-shop plug-in JAR into `./plugins/` | ||
| 365 | +5. Restart the container | ||
| 366 | +6. See the printing-shop's screens, custom fields, workflows, and permissions in the SPA | ||
| 367 | +7. Walk a quote through the printing shop's full workflow without writing any code | ||
| 368 | +8. Generate a PDF report for that quote in zh-CN | ||
| 369 | +9. Export a DSAR for one of their customers | ||
| 370 | + | ||
| 371 | +…and the same image, against a fresh empty Postgres, also serves a customer who has loaded a *completely different* plug-in expressing a *completely different* business — without any change to the core image. | ||
| 372 | + | ||
| 373 | +That's the bar. Until that bar is met, the framework has not delivered on its premise. |
docs/workflow-authoring/guide.md
0 → 100644
| 1 | +# Workflow authoring guide | ||
| 2 | + | ||
| 3 | +Workflows in vibe_erp are **data, not code**. State machines, approval chains, and document routing are declarative, so a new customer is onboarded by editing definitions instead of editing source. This is guardrail #2 in `CLAUDE.md`, and it shapes everything below. | ||
| 4 | + | ||
| 5 | +For the architectural placement of the workflow engine, see [`../architecture/overview.md`](../architecture/overview.md). For the full reasoning, see [`../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md`](../superpowers/specs/2026-04-07-vibe-erp-architecture-design.md). | ||
| 6 | + | ||
| 7 | +## BPMN 2.0 on embedded Flowable | ||
| 8 | + | ||
| 9 | +vibe_erp embeds the **Flowable** engine and uses **BPMN 2.0** as the workflow language. BPMN is a standard, and the standard is the contract: there is no vibe_erp-specific workflow DSL to learn or to invent. Anything a BPMN 2.0 modeling tool can produce, vibe_erp can run. | ||
| 10 | + | ||
| 11 | +Flowable runs in-process inside the Spring Boot host. Its tables (`flowable_*`) live in the same Postgres database as the core PBCs and are untouched by vibe_erp. | ||
| 12 | + | ||
| 13 | +## Two authoring paths | ||
| 14 | + | ||
| 15 | +Both paths are first-class. Tier 1 is preferred whenever it is expressive enough; Tier 2 is the escape hatch. | ||
| 16 | + | ||
| 17 | +### Tier 1 — Visual designer in the web UI (v1.0) | ||
| 18 | + | ||
| 19 | +Business analysts draw workflows in the BPMN designer that ships in the web SPA. The result is stored as a row in `metadata__workflow`, deployed to Flowable, and tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart, no plug-in. | ||
| 20 | + | ||
| 21 | +This path is the right one for: | ||
| 22 | + | ||
| 23 | +- Approval chains that vary per tenant. | ||
| 24 | +- Document routing rules that the customer wants to tune themselves. | ||
| 25 | +- Workflows that compose existing typed task handlers in a new order. | ||
| 26 | + | ||
| 27 | +The visual designer is a **v1.0 deliverable**; v0.1 ships the underlying API only. | ||
| 28 | + | ||
| 29 | +### Tier 2 — `.bpmn` files in a plug-in JAR (v0.1) | ||
| 30 | + | ||
| 31 | +Plug-in authors ship `.bpmn` files inside their JAR under `src/main/resources/workflow/`. The plug-in lifecycle deploys them to Flowable on plug-in install. The `plugin.yml` manifest lists them so the loader picks them up. | ||
| 32 | + | ||
| 33 | +``` | ||
| 34 | +src/main/resources/ | ||
| 35 | +├── plugin.yml | ||
| 36 | +└── workflow/ | ||
| 37 | + ├── quote_to_job_card.bpmn | ||
| 38 | + └── reprint_request.bpmn | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +```yaml | ||
| 42 | +# plugin.yml | ||
| 43 | +metadata: | ||
| 44 | + workflows: | ||
| 45 | + - workflow/quote_to_job_card.bpmn | ||
| 46 | + - workflow/reprint_request.bpmn | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +This path is the right one for: | ||
| 50 | + | ||
| 51 | +- Workflows that need new typed task handlers shipped alongside them. | ||
| 52 | +- Workflows that the plug-in author wants under version control with the rest of the plug-in code. | ||
| 53 | +- Workflows that ship as part of a vertical-specific plug-in (the printing-shop plug-in is the canonical example). | ||
| 54 | + | ||
| 55 | +## Service tasks call typed `TaskHandler` implementations | ||
| 56 | + | ||
| 57 | +A BPMN service task in vibe_erp does not embed scripting code. It references a typed `TaskHandler` by id, and the host routes the call to the matching implementation registered by a plug-in. | ||
| 58 | + | ||
| 59 | +The plug-in registers a handler: | ||
| 60 | + | ||
| 61 | +```kotlin | ||
| 62 | +@Extension(point = TaskHandler::class) | ||
| 63 | +class ReserveStockHandler : TaskHandler { | ||
| 64 | + | ||
| 65 | + override val id: String = "printing.reserve_stock" | ||
| 66 | + | ||
| 67 | + override fun handle(task: WorkflowTask) { | ||
| 68 | + val itemId = task.variable<String>("itemId") | ||
| 69 | + ?: error("itemId is required") | ||
| 70 | + val quantity = task.variable<Int>("quantity") ?: 0 | ||
| 71 | + | ||
| 72 | + // ... call into the plug-in's services here ... | ||
| 73 | + | ||
| 74 | + task.setVariable("reservationId", reservationId) | ||
| 75 | + task.complete() | ||
| 76 | + } | ||
| 77 | +} | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +The BPMN service task references the handler by its id (`printing.reserve_stock`). The host validates at deploy time that every referenced handler id exists and rejects the deployment otherwise — broken workflows fail at install, not at runtime. | ||
| 81 | + | ||
| 82 | +There is no scripting language to invent. The `TaskHandler` is just Kotlin code behind a typed interface, with the same testability, debuggability, and review surface as the rest of the codebase. | ||
| 83 | + | ||
| 84 | +## User tasks render forms from metadata | ||
| 85 | + | ||
| 86 | +A BPMN user task references a form definition by id. The host looks the id up in `metadata__form` and renders the form using the same code path as Tier 1 forms — there is no parallel form renderer for workflows. | ||
| 87 | + | ||
| 88 | +This means: | ||
| 89 | + | ||
| 90 | +- The same form can be used inside a workflow user task and outside a workflow. | ||
| 91 | +- A custom field added through Tier 1 customization automatically appears on every workflow user task that uses the same form. | ||
| 92 | +- Validation runs in exactly the same place whether the form is rendered inside a workflow or not. | ||
| 93 | + | ||
| 94 | +For details on how forms work, see the [form authoring guide](../form-authoring/guide.md). | ||
| 95 | + | ||
| 96 | +## Worked example: quote-to-job-card | ||
| 97 | + | ||
| 98 | +The reference printing-shop plug-in ships a `quote_to_job_card.bpmn` workflow that exercises every concept in this guide. In rough shape: | ||
| 99 | + | ||
| 100 | +1. **Start event:** a sales clerk creates a quote (user task; uses a form from `metadata__form`). | ||
| 101 | +2. **Service task `printing.price_quote`:** calls a `TaskHandler` registered by the plug-in to compute the price from the customer's price list and the quote's line items. | ||
| 102 | +3. **Exclusive gateway:** routes on whether the quote total exceeds the customer's pre-approved limit. | ||
| 103 | +4. **User task (manager approval):** rendered from a form definition; the manager can approve, reject, or send back for revision. | ||
| 104 | +5. **Service task `printing.reserve_stock`:** another `TaskHandler` that reserves raw materials. | ||
| 105 | +6. **Service task `printing.create_job_card`:** materializes the approved quote as a production job card (a custom entity defined by the plug-in). | ||
| 106 | +7. **End event:** publishes a `JobCardCreated` `DomainEvent` so other PBCs and plug-ins can react without coupling. | ||
| 107 | + | ||
| 108 | +What is worth noticing: | ||
| 109 | + | ||
| 110 | +- Every printing-specific concept — *quote*, *price list*, *job card*, *reserve stock for plates and inks* — lives in the **plug-in**, never in core PBCs. The core only knows about generic documents, workflows, forms, and events. | ||
| 111 | +- The cross-PBC interaction in step 7 goes through a `DomainEvent`, not a direct call. PBCs never import each other. | ||
| 112 | +- Steps 4 and 5 can be reordered, removed, or duplicated through the BPMN designer in the web UI without touching plug-in code, because the typed handlers are decoupled from the workflow shape. | ||
| 113 | + | ||
| 114 | +## Where to go next | ||
| 115 | + | ||
| 116 | +- Plug-in author walkthrough including a `TaskHandler` example: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) | ||
| 117 | +- Form authoring (the other half of any workflow that has user tasks): [`../form-authoring/guide.md`](../form-authoring/guide.md) | ||
| 118 | +- Plug-in API surface, including `api.v1.workflow`: [`../plugin-api/overview.md`](../plugin-api/overview.md) |