Commit 7bb7d9bc0f37e5087115c356d7143f6b0c4a437a

Authored by zichun
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.
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: 占地面积(平方米)