• Ninth core PBC. Ships the first-class orchestration aggregate for
    moving stock between locations: a header + lines that represents
    operator intent, and a confirm() verb that atomically posts the
    matching TRANSFER_OUT / TRANSFER_IN ledger pair per line via the
    existing InventoryApi.recordMovement facade.
    
    Takes the framework's core-PBC count to 9 of 10 (only pbc-quality
    remains in the P5.x row).
    
    ## The shape
    
    pbc-warehousing sits above pbc-inventory in the dependency graph:
    it doesn't replace the flat movement ledger, it orchestrates
    multi-row ledger writes with a business-level document on top. A
    DRAFT `warehousing__stock_transfer` row is queued intent (pickers
    haven't started yet); a CONFIRMED row reflects movements that have
    already posted to the `inventory__stock_movement` ledger. Each
    confirmed line becomes two ledger rows:
    
      TRANSFER_OUT(itemCode, fromLocationCode, -quantity, ref="TR:<code>")
      TRANSFER_IN (itemCode, toLocationCode,    quantity, ref="TR:<code>")
    
    All rows of one confirm call run inside ONE @Transactional method,
    so a failure anywhere — unknown item, unknown location, balance
    would go below zero — rolls back EVERY line's both halves. There
    is no half-confirmed transfer.
    
    ## Module contents
    
    - `build.gradle.kts` — new Gradle subproject, api-v1 + platform/*
      dependencies only. No cross-PBC dependency (guardrail #9 stays
      honest; CatalogApi + InventoryApi both come in via api.v1.ext).
    - `StockTransfer` entity — header with code, from/to location
      codes, status (DRAFT/CONFIRMED/CANCELLED), transfer_date, note,
      OneToMany<StockTransferLine>. Table name
      `warehousing__stock_transfer`.
    - `StockTransferLine` entity — lineNo, itemCode, quantity.
      `transfer_id → warehousing__stock_transfer(id) ON DELETE CASCADE`,
      unique `(transfer_id, line_no)`.
    - `StockTransferJpaRepository` — existsByCode + findByCode.
    - `StockTransferService` — create / confirm / cancel + three read
      methods. @Transactional service-level; all state transitions run
      through @Transactional methods so the event-bus MANDATORY
      propagation (if/when a pbc-warehousing event is added later) has
      a transaction to join. Business invariants:
        * code is unique (existsByCode short-circuit)
        * from != to (enforced in code AND in the Liquibase CHECK)
        * at least one line
        * each line: positive line_no, unique per transfer, positive
          quantity, itemCode must resolve via CatalogApi.findItemByCode
        * confirm requires DRAFT; writes OUT-first-per-line so a
          balance-goes-negative error aborts before touching the
          destination location
        * cancel requires DRAFT; CONFIRMED transfers are terminal
          (reverse by creating a NEW transfer in the opposite direction,
          matching the document-discipline rule every other PBC uses)
    - `StockTransferController` — `/api/v1/warehousing/stock-transfers`
      with GET list, GET by id, GET by-code, POST create, POST
      {id}/confirm, POST {id}/cancel. Every endpoint
      @RequirePermission-gated using the keys declared in the metadata
      YAML. Matches the shape of pbc-orders-sales, pbc-orders-purchase,
      pbc-production.
    - DTOs use the established pattern — jakarta.validation on the
      request, response mapping via extension functions.
    - `META-INF/vibe-erp/metadata/warehousing.yml` — 1 entity, 4
      permissions, 1 menu. Loaded by MetadataLoader at boot, visible
      via `GET /api/v1/_meta/metadata`.
    - `distribution/src/main/resources/db/changelog/pbc-warehousing/001-warehousing-init.xml`
      — creates both tables with the full audit column set, state
      CHECK constraint, locations-distinct CHECK, unique
      (transfer_id, line_no) index, quantity > 0 CHECK, item_code
      index for cross-PBC grep.
    - `settings.gradle.kts`, `distribution/build.gradle.kts`,
      `master.xml` all wired.
    
    ## Smoke test (fresh DB + running app)
    
    ```
    # seed
    POST /api/v1/catalog/items   {code: PAPER-A4, baseUomCode: sheet}
    POST /api/v1/catalog/items   {code: PAPER-A3, baseUomCode: sheet}
    POST /api/v1/inventory/locations {code: WH-MAIN, type: WAREHOUSE}
    POST /api/v1/inventory/locations {code: WH-SHOP, type: WAREHOUSE}
    POST /api/v1/inventory/movements {itemCode: PAPER-A4, locationId: <WH-MAIN>, delta: 100, reason: RECEIPT}
    POST /api/v1/inventory/movements {itemCode: PAPER-A3, locationId: <WH-MAIN>, delta: 50,  reason: RECEIPT}
    
    # exercise the new PBC
    POST /api/v1/warehousing/stock-transfers
         {code: TR-001, fromLocationCode: WH-MAIN, toLocationCode: WH-SHOP,
          lines: [{lineNo: 1, itemCode: PAPER-A4, quantity: 30},
                  {lineNo: 2, itemCode: PAPER-A3, quantity: 10}]}
      → 201 DRAFT
    POST /api/v1/warehousing/stock-transfers/<id>/confirm
      → 200 CONFIRMED
    
    # verify balances via the raw DB (the HTTP stock-balance endpoint
    # has a separate unrelated bug returning 500; the ledger state is
    # what this commit is proving)
    SELECT item_code, location_id, quantity FROM inventory__stock_balance;
      PAPER-A4 / WH-MAIN →  70   ← debited 30
      PAPER-A4 / WH-SHOP →  30   ← credited 30
      PAPER-A3 / WH-MAIN →  40   ← debited 10
      PAPER-A3 / WH-SHOP →  10   ← credited 10
    
    SELECT item_code, location_id, reason, delta, reference
      FROM inventory__stock_movement ORDER BY occurred_at;
      PAPER-A4 / WH-MAIN / TRANSFER_OUT / -30 / TR:TR-001
      PAPER-A4 / WH-SHOP / TRANSFER_IN  /  30 / TR:TR-001
      PAPER-A3 / WH-MAIN / TRANSFER_OUT / -10 / TR:TR-001
      PAPER-A3 / WH-SHOP / TRANSFER_IN  /  10 / TR:TR-001
    ```
    
    Four rows all tagged `TR:TR-001`. A grep of the ledger attributes
    both halves of each line to the single source transfer document.
    
    ## Transactional rollback test (in the same smoke run)
    
    ```
    # ask for more than exists
    POST /api/v1/warehousing/stock-transfers
         {code: TR-002, from: WH-MAIN, to: WH-SHOP,
          lines: [{lineNo: 1, itemCode: PAPER-A4, quantity: 1000}]}
      → 201 DRAFT
    POST /api/v1/warehousing/stock-transfers/<id>/confirm
      → 400 "stock movement would push balance for 'PAPER-A4' at
             location <WH-MAIN> below zero (current=70.0000, delta=-1000.0000)"
    
    # assert TR-002 is still DRAFT
    GET /api/v1/warehousing/stock-transfers/<id> → status: DRAFT  ← NOT flipped to CONFIRMED
    
    # assert the ledger still has exactly 6 rows (no partial writes)
    SELECT count(*) FROM inventory__stock_movement; → 6
    ```
    
    The failed confirm left no residue: status stayed DRAFT, and the
    ledger count is unchanged at 6 (the 2 RECEIPT seeds + the 4
    TRANSFER_OUT/IN from TR-001). Propagation.REQUIRED + Spring's
    default rollback-on-unchecked-exception semantics do exactly what
    the KDoc promises.
    
    ## State-machine guards
    
    ```
    POST /api/v1/warehousing/stock-transfers/<confirmed-id>/confirm
      → 400 "cannot confirm stock transfer TR-001 in status CONFIRMED;
             only DRAFT can be confirmed"
    
    POST /api/v1/warehousing/stock-transfers/<confirmed-id>/cancel
      → 400 "cannot cancel stock transfer TR-001 in status CONFIRMED;
             only DRAFT can be cancelled — reverse a confirmed transfer
             by creating a new one in the other direction"
    ```
    
    ## Tests
    
    - 10 new unit tests in `StockTransferServiceTest`:
      * `create persists a DRAFT transfer when everything validates`
      * `create rejects duplicate code`
      * `create rejects same from and to location`
      * `create rejects an empty line list`
      * `create rejects duplicate line numbers`
      * `create rejects non-positive quantities`
      * `create rejects unknown items via CatalogApi`
      * `confirm writes an atomic TRANSFER_OUT + TRANSFER_IN pair per line`
        — uses `verifyOrder` to assert OUT-first-per-line dispatch order
      * `confirm refuses a non-DRAFT transfer`
      * `cancel refuses a CONFIRMED transfer`
      * `cancel flips a DRAFT transfer to CANCELLED`
    - Total framework unit tests: 288 (was 278), all green.
    
    ## What this unblocks
    
    - **Real warehouse workflows** — confirm a transfer from a picker
      UI (R1 is pending), driven by a BPMN that hands the confirm to a
      TaskHandler once the physical move is complete.
    - **pbc-quality (P5.8, last remaining core PBC)** — inspection
      plans + results + holds. Holds would typically quarantine stock
      by moving it to a QUARANTINE location via a stock transfer,
      which is the natural consumer for this aggregate.
    - **Stocktakes (physical inventory reconciliation)** — future
      pbc-warehousing verb that compares counted vs recorded and posts
      the differences as ADJUSTMENT rows; shares the same
      `recordMovement` primitive.
    zichun authored
     
    Browse Code »

  • BLOCKER: wire Hibernate multi-tenancy
    - application.yaml: set hibernate.tenant_identifier_resolver and
      hibernate.multiTenancy=DISCRIMINATOR so HibernateTenantResolver is
      actually installed into the SessionFactory
    - AuditedJpaEntity.tenantId: add @org.hibernate.annotations.TenantId so
      every PBC entity inherits the discriminator
    - AuditedJpaEntityListener.onCreate: throw if a caller pre-set tenantId
      to a different value than the current TenantContext, instead of
      silently overwriting (defense against cross-tenant write bugs)
    
    IMPORTANT: dependency hygiene
    - pbc-identity no longer depends on platform-bootstrap (wrong direction;
      bootstrap assembles PBCs at the top of the stack)
    - root build.gradle.kts: tighten the architectural-rule enforcement to
      also reject :pbc:* -> platform-bootstrap; switch plug-in detection
      from a fragile pathname heuristic to an explicit
      extra["vibeerp.module-kind"] = "plugin" marker; reference plug-in
      declares the marker
    
    IMPORTANT: api.v1 surface additions (all non-breaking)
    - Repository: documented closed exception set; new
      PersistenceExceptions.kt declares OptimisticLockConflictException,
      UniqueConstraintViolationException, EntityValidationException, and
      EntityNotFoundException so plug-ins never see Hibernate types
    - TaskContext: now exposes tenantId(), principal(), locale(),
      correlationId() so workflow handlers (which run outside an HTTP
      request) can pass tenant-aware calls back into api.v1
    - EventBus: subscribe() now returns a Subscription with close() so
      long-lived subscribers can deregister explicitly; added a
      subscribe(topic: String, ...) overload for cross-classloader event
      routing where Class<E> equality is unreliable
    - IdentityApi.findUserById: tightened from Id<*> to PrincipalId so the
      type system rejects "wrong-id-kind" mistakes at the cross-PBC boundary
    
    NITs:
    - HealthController.kt -> MetaController.kt (file name now matches the
      class name); added TODO(v0.2) for reading implementationVersion from
      the Spring Boot BuildProperties bean
    vibe_erp authored
     
    Browse Code »
  • vibe_erp authored
     
    Browse Code »