Commit 7bb7d9bc0f37e5087115c356d7143f6b0c4a437a
1 parent
88344134
feat(inventory): Location core custom fields + CLAUDE.md state update
Two small closer items that tidy up the end of the HasExt rollout:
1. inventory.yml gains a `customFields:` section with two core
declarations for Location: `inventory_address_city` (string,
maxLength 128) and `inventory_floor_area_sqm` (decimal 10,2).
Completes the "every HasExt entity has at least one declared
field" symmetry. Printing-shop plug-in already adds its own
`printing_shop_press_id` etc. on top.
2. CLAUDE.md "Repository state" section updated to reflect this
session's milestones:
- pbc-production v2 (IN_PROGRESS + BOM + scrap) now called out
explicitly in the PBC list.
- MATERIAL_ISSUE added to the buy-sell-MAKE loop description —
the work-order completion now consumes raw materials per BOM
line AND credits finished goods atomically.
- New bullet: "Tier 1 customization is universal across every
core entity with an ext column" — HasExt on Partner, Location,
SalesOrder, PurchaseOrder, WorkOrder, Item; every service uses
applyTo/parseExt helpers, zero duplication.
- New bullet: "Clean Core extensibility is executable" — the
reference printing-shop plug-in's metadata YAML ships
customFields on Partner/Item/SalesOrder/WorkOrder and the
MetadataLoader merges them with core declarations at load
time. Executable grade-A extension under the A/B/C/D safety
scale.
- Printing-shop plug-in description updated to note that its
metadata YAML now carries custom fields on core entities, not
just its own entities.
Smoke verified end-to-end against real Postgres with the plug-in
staged:
- GET /_meta/metadata/custom-fields/Location returns 2 core
fields.
- POST /inventory/locations with `{inventory_address_city:
"Shenzhen", inventory_floor_area_sqm: "1250.50"}` → 201,
canonical form persisted, ext round-trips.
- POST with `inventory_floor_area_sqm: "123456789012345.678"` →
400 "ext.inventory_floor_area_sqm: decimal scale 3 exceeds
declared scale 2" — the validator's precision/scale rules fire
exactly as designed.
No code changes. 246 unit tests, all green. 18 Gradle subprojects.
Showing
2 changed files
with
35 additions
and
3 deletions
CLAUDE.md
| @@ -98,9 +98,12 @@ plugins (incl. ref) depend on: api/api-v1 only | @@ -98,9 +98,12 @@ plugins (incl. ref) depend on: api/api-v1 only | ||
| 98 | - **18 Gradle subprojects** built and tested. The dependency rule (PBCs never import each other; plug-ins only see `api.v1`) is enforced at configuration time by the root `build.gradle.kts`. | 98 | - **18 Gradle subprojects** built and tested. The dependency rule (PBCs never import each other; plug-ins only see `api.v1`) is enforced at configuration time by the root `build.gradle.kts`. |
| 99 | - **246 unit tests across 18 modules**, all green. `./gradlew build` is the canonical full build. | 99 | - **246 unit tests across 18 modules**, all green. `./gradlew build` is the canonical full build. |
| 100 | - **All 9 cross-cutting platform services live and smoke-tested end-to-end** against real Postgres: auth (P4.1), authorization with `@RequirePermission` + JWT roles claim + AOP enforcement (P4.3), plug-in HTTP endpoints (P1.3), plug-in linter (P1.2), plug-in-owned Liquibase + typed SQL (P1.4), event bus + transactional outbox (P1.7), metadata loader + custom-field validation (P1.5 + P3.4), ICU4J translator with per-plug-in locale chain (P1.6), plus the audit/principal context bridge. | 100 | - **All 9 cross-cutting platform services live and smoke-tested end-to-end** against real Postgres: auth (P4.1), authorization with `@RequirePermission` + JWT roles claim + AOP enforcement (P4.3), plug-in HTTP endpoints (P1.3), plug-in linter (P1.2), plug-in-owned Liquibase + typed SQL (P1.4), event bus + transactional outbox (P1.7), metadata loader + custom-field validation (P1.5 + P3.4), ICU4J translator with per-plug-in locale chain (P1.6), plus the audit/principal context bridge. |
| 101 | -- **8 of 10 core PBCs implemented end-to-end** (`pbc-identity`, `pbc-catalog`, `pbc-partners`, `pbc-inventory`, `pbc-orders-sales`, `pbc-orders-purchase`, `pbc-finance`, `pbc-production`). **The full buy-sell-make loop works**: a purchase order receives stock via `PURCHASE_RECEIPT`, a sales order ships stock via `SALES_SHIPMENT`, and a work order produces stock via `PRODUCTION_RECEIPT`. All three PBCs feed the same `inventory__stock_movement` ledger via the same `InventoryApi.recordMovement` facade. | ||
| 102 | -- **Event-driven cross-PBC integration is live in BOTH directions and the consumer reacts to the full lifecycle.** Six typed events under `org.vibeerp.api.v1.event.orders.*` are published from `SalesOrderService` and `PurchaseOrderService` inside the same `@Transactional` method as their state changes. **pbc-finance** subscribes to ALL SIX of them via the api.v1 `EventBus.subscribe(eventType, listener)` typed-class overload: confirm events POST AR/AP rows; ship/receive events SETTLE them; cancel events REVERSE them. Each transition is idempotent under at-least-once delivery — re-applying the same destination status is a no-op. Cancel-from-DRAFT is a clean no-op because no `*ConfirmedEvent` was ever published. pbc-finance has no source dependency on pbc-orders-*; it reaches them only through events. | ||
| 103 | -- **Reference printing-shop plug-in** owns its own DB schema, CRUDs plates and ink recipes via REST through `context.jdbc`, ships its own metadata YAML, and registers 7 HTTP endpoints. It is the executable acceptance test for the framework. | 101 | +- **8 of 10 core PBCs implemented end-to-end** (`pbc-identity`, `pbc-catalog`, `pbc-partners`, `pbc-inventory`, `pbc-orders-sales`, `pbc-orders-purchase`, `pbc-finance`, `pbc-production`). **The full buy-sell-make loop works**: a purchase order receives stock via `PURCHASE_RECEIPT`, a sales order ships stock via `SALES_SHIPMENT`, and a work order with a bill of materials consumes raw materials via `MATERIAL_ISSUE` and produces finished goods via `PRODUCTION_RECEIPT`, all atomically in one transaction. All four write paths feed the same `inventory__stock_movement` ledger via the same `InventoryApi.recordMovement` facade. |
| 102 | +- **pbc-production v2** adds an IN_PROGRESS state between DRAFT and COMPLETED, a `WorkOrderInput` child entity carrying per-unit BOM consumption rates, and a scrap verb that writes a negative `ADJUSTMENT` ledger row for post-completion corrections without leaving the terminal COMPLETED state. | ||
| 103 | +- **Event-driven cross-PBC integration is live in BOTH directions and the consumer reacts to the full lifecycle.** Six typed events under `org.vibeerp.api.v1.event.orders.*` are published from `SalesOrderService` and `PurchaseOrderService` inside the same `@Transactional` method as their state changes. **pbc-finance** subscribes to ALL SIX of them via the api.v1 `EventBus.subscribe(eventType, listener)` typed-class overload: confirm events POST AR/AP rows; ship/receive events SETTLE them; cancel events REVERSE them. Each transition is idempotent under at-least-once delivery. pbc-finance has no source dependency on pbc-orders-*; it reaches them only through events. | ||
| 104 | +- **Tier 1 customization is universal across every core entity with an ext column.** Partner, Location, SalesOrder, PurchaseOrder, WorkOrder, and Item all implement the `org.vibeerp.api.v1.entity.HasExt` marker interface. Their services go through one `ExtJsonValidator.applyTo(entity, map)` helper that validates, canonicalises, and serialises the JSON in one call. Response mappers use `parseExt(entity)` for the symmetric read. No PBC services duplicate ext handling code. | ||
| 105 | +- **Clean Core extensibility is executable.** The reference printing-shop plug-in ships a `customFields:` section in its metadata YAML that extends four CORE entities (Partner, Item, SalesOrder, WorkOrder) with printing-specific fields (e.g. `printing_shop_color_count`, `printing_shop_paper_gsm`, `printing_shop_quote_number`, `printing_shop_press_id`). The MetadataLoader merges plug-in declarations with core ones; the validator enforces the merged set on every save. Uninstalling the plug-in makes the fields disappear from both the UI and the validation — no migration, no code change. This is the executable grade-A extension under the A/B/C/D scale. | ||
| 106 | +- **Reference printing-shop plug-in** owns its own DB schema, CRUDs plates and ink recipes via REST through `context.jdbc`, ships its own metadata YAML (entities, permissions, menus, AND custom fields on core entities), and registers 7 HTTP endpoints. It is the executable acceptance test for the framework. | ||
| 104 | - **Package root** is `org.vibeerp`. | 107 | - **Package root** is `org.vibeerp`. |
| 105 | - **Build commands:** `./gradlew build` (full), `./gradlew test` (unit tests), `./gradlew :distribution:bootRun` (run dev profile, stages plug-in JAR automatically), `docker compose up -d db` (Postgres). The bootstrap admin password is printed to the application logs on first boot. | 108 | - **Build commands:** `./gradlew build` (full), `./gradlew test` (unit tests), `./gradlew :distribution:bootRun` (run dev profile, stages plug-in JAR automatically), `docker compose up -d db` (Postgres). The bootstrap admin password is printed to the application logs on first boot. |
| 106 | - **Documentation:** site under `docs/`. Architecture spec and implementation plan under `docs/superpowers/specs/`. Live progress tracker in [`PROGRESS.md`](PROGRESS.md). | 109 | - **Documentation:** site under `docs/`. Architecture spec and implementation plan under `docs/superpowers/specs/`. Live progress tracker in [`PROGRESS.md`](PROGRESS.md). |
pbc/pbc-inventory/src/main/resources/META-INF/vibe-erp/metadata/inventory.yml
| @@ -39,3 +39,32 @@ menus: | @@ -39,3 +39,32 @@ menus: | ||
| 39 | icon: layers | 39 | icon: layers |
| 40 | section: Inventory | 40 | section: Inventory |
| 41 | order: 410 | 41 | order: 410 |
| 42 | + | ||
| 43 | +# Core custom fields for Location. These are the ones every | ||
| 44 | +# warehouse-running business tends to need regardless of industry; | ||
| 45 | +# vertical-specific fields (e.g. "temperature zone" for cold chain, | ||
| 46 | +# "licensed for bonded goods" for customs) belong in customer | ||
| 47 | +# plug-ins. | ||
| 48 | +customFields: | ||
| 49 | + - key: inventory_address_city | ||
| 50 | + targetEntity: Location | ||
| 51 | + type: | ||
| 52 | + kind: string | ||
| 53 | + maxLength: 128 | ||
| 54 | + required: false | ||
| 55 | + pii: false | ||
| 56 | + labelTranslations: | ||
| 57 | + en: City | ||
| 58 | + zh-CN: 城市 | ||
| 59 | + | ||
| 60 | + - key: inventory_floor_area_sqm | ||
| 61 | + targetEntity: Location | ||
| 62 | + type: | ||
| 63 | + kind: decimal | ||
| 64 | + precision: 10 | ||
| 65 | + scale: 2 | ||
| 66 | + required: false | ||
| 67 | + pii: false | ||
| 68 | + labelTranslations: | ||
| 69 | + en: Floor area (m²) | ||
| 70 | + zh-CN: 占地面积(平方米) |