• zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • When type is 'enum', show a comma-separated input for allowed values.
    When type is 'string', show a max-length input. Previously these
    type-specific options were missing from the UI.
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • 407 tests, all green. Rules engine with 24 unit tests for condition
    evaluation. User-task form rendering via MetadataFormRenderer.
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • P3.5: event-condition-action rules stored as metadata rows, evaluated
    when events fire. v1.0 actions: log, set-field, publish-event.
    P2.3: SPA pages for listing and completing Flowable user-tasks
    using MetadataFormRenderer. P2.2 BPMN designer deferred to v1.1.
    zichun authored
     
    Browse Code »
  • - P3.2: MetadataFormRenderer + @rjsf/core + 6 custom ERP widgets
    - P3.3: Form designer (structured property editor + live preview)
    - P3.6: List view designer (column/filter/sort configuration)
    - R3: Metadata admin (tabbed CRUD for all metadata types)
    - 382 tests, all green
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • Add the @rjsf-based metadata-driven form renderer with a custom
    Tailwind theme matching the existing SPA styling and six ERP-specific
    widgets: PartnerPicker, ItemPicker, UomSelector, LocationPicker,
    MoneyInput, and QuantityInput.
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • Add three write controllers behind @RequirePermission("admin.metadata.write"):
    
    - FormDefinitionController (PUT/DELETE /api/v1/_meta/metadata/forms/{slug})
    - ListViewDefinitionController (PUT/DELETE /api/v1/_meta/metadata/list-views/{slug})
    - CustomFieldWriteController (POST/PUT/DELETE /api/v1/_meta/metadata/custom-fields)
    
    All three enforce source='user' immutability: rows seeded by core or
    plug-in YAMLs cannot be modified or deleted through the REST surface.
    CustomFieldWriteController calls CustomFieldRegistry.refresh() after
    every successful write so the in-memory index stays current.
    
    MetadataController gains GET endpoints for forms, list views (including
    by-slug lookup), and the aggregate /metadata response now includes
    forms and listViews sections.
    
    Ships platform-metadata.yml declaring admin.metadata.read/write
    permissions and a Metadata Admin menu entry.
    
    22 unit tests cover the full CRUD + source-enforcement matrix.
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • 9 tasks: backend YAML/loader extension, CRUD endpoints with source
    enforcement, @rjsf form renderer + custom widgets, form designer,
    list view designer, metadata admin tabbed UI, smoke test + version bump.
    zichun authored
     
    Browse Code »
  • Sub-project A of remaining v1.0 work. Key decisions:
    - Hybrid: core forms stay handcrafted, renderer for user-task forms
    - @rjsf/core with custom ERP widget registry
    - Structured property editor (not drag-and-drop) for form designer
    - Custom entities deferred to v1.1
    zichun authored
     
    Browse Code »
  • CLAUDE.md "Repository state" was stale (18→25 subprojects,
    246→356 tests, 8→10 PBCs, 9→12 platform services).
    PROGRESS.md "What's not yet live" still listed Flowable,
    JasperReports, file store, job scheduler, OIDC, and web SPA
    as missing — all are live. "Current stage" paragraph updated
    to reflect 10 PBCs and 12 services. "How to run" section
    updated to remove DemoSeedRunner reference (moved to demo
    branch).
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »
  • Tier 1 customization comes alive in the SPA: custom fields
    declared in YAML metadata now render automatically in create
    forms without any compile-time knowledge of the field.
    
    New component: DynamicExtFields
      - Fetches custom field declarations from the existing
        /api/v1/_meta/metadata/custom-fields/{entityName} endpoint
      - Renders one input per declared field, type-matched:
        string → text, integer → number (step=1), decimal/money/
        quantity → number (step=0.01), boolean → checkbox,
        date → date picker, dateTime → datetime-local,
        enum → select dropdown, uuid → text
      - Labels resolve from labelTranslations using the active
        locale (i18n integration)
      - Required fields show a red asterisk
      - Values are collected in the ext map and sent with the
        create request
    
    Wired into: CreateItemPage (entityName="Item"),
    CreatePartnerPage (entityName="Partner"). Both now show a
    "Custom fields" section below the static fields when the
    entity has custom field declarations in metadata.
    
    No new backend code — the existing /api/v1/_meta/metadata/
    custom-fields endpoint already returns exactly the shape
    the component needs. This is P3.1: the runtime form renderer
    for Tier 1 customization.
    zichun authored
     
    Browse Code »
  • Adds edit pages for items and partners — the two entities
    operators update most often. Each form loads the existing
    record, pre-fills all editable fields, and PATCHes on save.
    Code and baseUomCode are read-only after creation (by design).
    
    New pages:
      - EditItemPage: name, type, description, active toggle
      - EditPartnerPage: name, type, email, phone
    
    API client: catalog.updateItem, partners.update (PATCH).
    
    List pages: item/partner codes are now clickable links to the
    edit page instead of plain text. Routes wired at
    /items/:id/edit and /partners/:id/edit.
    zichun authored
     
    Browse Code »
  • Adds client-side i18n infrastructure to the SPA (CLAUDE.md
    guardrail #6: global/i18n from day one).
    
    New files:
      - i18n/messages.ts: flat key-value message bundles for en-US
        and zh-CN. Keys use dot-notation (nav.*, action.*, status.*,
        label.*). ~80 keys per locale covering navigation, actions,
        status badges, and common labels.
      - i18n/LocaleContext.tsx: LocaleProvider + useT() hook + useLocale()
        hook. Active locale stored in localStorage, defaults to the
        browser's navigator.language. Auto-detects zh-* → zh-CN.
    
    Wired into the SPA:
      - main.tsx wraps the app in <LocaleProvider>
      - AppLayout sidebar uses t(key) for every heading and item
      - Top bar has a locale dropdown (English / 中文) that
        switches the entire sidebar + status labels instantly
      - StatusBadge uses t('status.DRAFT') etc. so statuses render
        as '草稿' / '已确认' / '已发货' in Chinese
    
    The i18n system is intentionally simple: plain strings, no ICU
    MessageFormat patterns (those live on the backend via ICU4J).
    A future chunk can adopt @formatjs/intl-messageformat if the
    SPA needs plural/gender/number formatting client-side.
    
    Not yet translated: page-level titles and form labels (they
    still use hard-coded English). The infrastructure is in place;
    translating individual pages is incremental.
    zichun authored
     
    Browse Code »
  • Removes demo-specific code from the main (framework) branch so
    main stays a clean, generic foundation. The demo branch retains
    these files from before this commit.
    
    Removed from main:
      - DemoSeedRunner.kt (printing-company seed data)
      - vibeerp.demo.seed config in application-dev.yaml
      - EBC-PP-001 demo walkthrough in DashboardPage
    
    The dashboard now shows a generic "Getting started" guide that
    walks operators through setting up master data, creating orders,
    and walking the buy-make-sell loop — without referencing any
    specific customer or seed data.
    
    To run the printing-company demo, use the demo worktree:
      cd ~/Desktop/vibe_erp_demo
      ./gradlew :distribution:bootRun
    zichun authored
     
    Browse Code »
  • P5.9 updated: chart of accounts (4d0dd9fe) + double-entry lines
    (3745de3a) + SPA expandable lines view (d2bca486). Version bumped.
    zichun authored
     
    Browse Code »
  • JournalEntriesPage now renders expandable debit/credit lines
    for each entry. Click a row to toggle the line detail view
    showing account code, DR/CR amounts, and description.
    
    Types updated: JournalEntry now includes lines array with
    JournalEntryLine (lineNo, accountCode, debit, credit, description).
    
    This makes the GL growth visible in the demo — confirming an SO
    shows the AR entry with DR 1100 / CR 4100 balanced lines inline.
    zichun authored
     
    Browse Code »
  • Adds JournalEntryLine child entity with debit/credit legs per
    account, completing the pbc-finance GL foundation.
    
    Domain:
      - JournalEntryLine entity: lineNo, accountCode, debit (>=0),
        credit (>=0), description. FK to parent JournalEntry with
        CASCADE delete. Unique (journal_entry_id, line_no).
      - JournalEntry gains @OneToMany lines collection (EAGER fetch,
        ordered by lineNo).
    
    Event subscribers now write balanced double-entry lines:
      - SalesOrderConfirmed: DR 1100 (AR), CR 4100 (Revenue)
      - PurchaseOrderConfirmed: DR 1200 (Inventory), CR 2100 (AP)
    
    The parent JournalEntry.amount is retained as a denormalized
    summary; the lines are the source of truth for accounting. The
    seeded account codes (1100, 1200, 2100, 4100, 5100) match the
    chart from the previous commit.
    
    JournalEntryController.toResponse() now includes the lines array
    so the SPA can display debit/credit legs inline.
    
    Schema: 004-finance-entry-lines.xml adds finance__journal_entry_line
    with FK, unique (entry, lineNo), non-negative check on dr/cr.
    
    Smoke verified on fresh Postgres:
      - Confirm SO-2026-0001 -> AR entry with 2 lines:
        DR 1100 $1950, CR 4100 $1950
      - Confirm PO-2026-0001 -> AP entry with 2 lines:
        DR 1200 $2550, CR 2100 $2550
      - Both entries balanced (sum DR = sum CR)
    
    Caught by smoke: LazyInitializationException on the new lines
    collection — fixed by switching FetchType from LAZY to EAGER
    (entries have 2-4 lines max, eager is appropriate).
    zichun authored
     
    Browse Code »
  • First step of pbc-finance GL growth: the chart of accounts.
    
    Backend:
      - Account entity (code, name, accountType: ASSET/LIABILITY/EQUITY/
        REVENUE/EXPENSE, description, active)
      - AccountJpaRepository + AccountService (list, findById, findByCode,
        create with duplicate-code guard)
      - AccountController at /api/v1/finance/accounts (GET list, GET by id,
        POST create). Permission-gated: finance.account.read, .create.
      - Liquibase 003-finance-accounts.xml: table + unique code index +
        6 seeded accounts (1000 Cash, 1100 AR, 1200 Inventory, 2100 AP,
        4100 Sales Revenue, 5100 COGS)
      - finance.yml updated: Account entity + 2 permissions + menu entry
    
    SPA:
      - AccountsPage with sortable list + inline create form
      - finance.listAccounts + finance.createAccount in typed API client
      - Sidebar: "Chart of Accounts" above "Journal Entries" in Finance
      - Route /accounts wired in App.tsx + SpaController + SecurityConfig
    
    This is the foundation for the next step (JournalEntryLine child
    entity with per-account debit/credit legs + balanced-entry
    validation). The seeded chart covers the 6 accounts the existing
    event subscribers will reference once the double-entry lines land.
    zichun authored
     
    Browse Code »
  • Pins today's 5 feature commits:
      - 25353240 SPA CRUD forms (item, partner, PO, WO)
      - 82c5267d R2 identity screens (users, roles, assignment)
      - c2fab13b S3 file backend (P1.9 complete)
      - 6ad72c7c OIDC federation (P4.2 complete)
      - 17771894 SPA fill (create location, adjust stock)
    
    P1.9 promoted from Partial to DONE. P4.2 promoted from Pending
    to DONE. R2 promoted from Pending to DONE. R4 updated to reflect
    create forms for every manageable entity.
    
    Version bumped 0.29.0 -> 0.30.0-SNAPSHOT.
    zichun authored
     
    Browse Code »
  • Two more operator-facing forms:
      - CreateLocationPage: code, name, type (WAREHOUSE/BIN/VIRTUAL)
      - AdjustStockPage: item dropdown, location dropdown, absolute
        quantity. Creates the balance row if absent; sets it to the
        given value if present. Shows the resulting balance inline.
    
    API client: inventory.createLocation, inventory.adjustBalance.
    Locations list gets "+ New Location"; Balances list gets "Adjust
    Stock". Routes wired at /locations/new and /balances/adjust.
    
    With this commit, every PBC entity that operators need to create
    or manage has a SPA form: items, partners, locations, stock
    balances, sales orders, purchase orders, work orders (with BOM +
    routing), users, and roles. The only create-less entities are
    journal entries (read-only, event-driven) and stock movements
    (append-only ledger).
    zichun authored
     
    Browse Code »
  • The framework now supports federated authentication: operators can
    configure an external OIDC provider (Keycloak, Auth0, or any
    OIDC-compliant issuer) and the API accepts JWTs from both the
    built-in auth (/api/v1/auth/login, HS256) and the OIDC provider
    (RS256, JWKS auto-discovered).
    
    Opt-in via vibeerp.security.oidc.issuer-uri. When blank (default),
    only built-in auth works — exactly the pre-P4.2 behavior. When
    set, the JwtDecoder becomes a composite: tries the built-in HS256
    decoder first (cheap, local HMAC), falls back to the OIDC decoder
    (RS256, cached JWKS fetch from the provider's .well-known endpoint).
    
    Claim mapping: PrincipalContextFilter now handles both formats:
      - Built-in: sub=UUID, username=<claim>, roles=<flat array>
      - OIDC/Keycloak: sub=OIDC subject, preferred_username=<claim>,
        realm_access.roles=<nested array>
    Claim names are configurable via vibeerp.security.oidc.username-claim
    and roles-claim for non-Keycloak providers.
    
    New files:
      - OidcProperties.kt: config properties class for the OIDC block
    
    Modified files:
      - JwtConfiguration.kt: composite decoder, now takes OidcProperties
      - PrincipalContextFilter.kt: dual claim resolution (built-in first,
        OIDC fallback), now takes OidcProperties
      - JwtRoundTripTest.kt: updated to pass OidcProperties (defaults)
      - application.yaml: OIDC config block with env-var interpolation
    
    No new dependencies — uses Spring Security's existing
    JwtDecoders.fromIssuerLocation() which is already on the classpath
    via spring-boot-starter-oauth2-resource-server.
    zichun authored
     
    Browse Code »
  • Adds S3FileStorage alongside the existing LocalDiskFileStorage,
    selected at boot by vibeerp.files.backend (local or s3). The
    local backend is the default (matchIfMissing=true) so existing
    deployments are unaffected. Setting backend=s3 activates the S3
    backend with its own config block.
    
    Works with AWS S3, MinIO, DigitalOcean Spaces, or any
    S3-compatible object store via the endpoint-url override. The
    S3 client is lazy-initialized on first use so the bean loads
    even when S3 is unreachable at boot time (useful for tests and
    for the local-disk default path where the S3 bean is never
    instantiated).
    
    Configuration (vibeerp.files.s3.*):
      - bucket (required when backend=s3)
      - region (default: us-east-1)
      - endpoint-url (optional; for MinIO and non-AWS services)
      - access-key + secret-key (optional; falls back to AWS
        DefaultCredentialsProvider chain)
      - key-prefix (optional; namespaces objects so multiple
        instances can share one bucket)
    
    Implementation notes:
      - put() reads the stream into a byte array for S3 (S3
        requires Content-Length up front; chunked upload is a
        future optimization for large files)
      - get() returns the S3 response InputStream directly;
        caller must close it (same contract as local backend)
      - list() paginates via ContinuationToken for buckets with
        >1000 objects per prefix
      - Content-type is stored as native S3 object metadata
        (no sidecar .meta file unlike local backend)
    
    Dependency: software.amazon.awssdk:s3:2.28.6 (AWS SDK v2)
    added to libs.versions.toml and platform-files build.gradle.kts.
    
    LocalDiskFileStorage gained @ConditionalOnProperty(havingValue
    = "local", matchIfMissing = true) so it's the default but
    doesn't conflict when backend=s3.
    
    application.yaml updated with commented-out S3 config block
    documenting all available properties.
    zichun authored
     
    Browse Code »
  • Closes the R2 gap: an admin can now manage users and roles
    entirely from the SPA without touching curl or Swagger UI.
    
    Backend (pbc-identity):
      - New RoleService with createRole, assignRole, revokeRole,
        findUserRoleCodes, listRoles. Each method validates
        existence + idempotency (duplicate assignment rejected,
        missing role rejected).
      - New RoleController at /api/v1/identity/roles (CRUD) +
        /api/v1/identity/users/{userId}/roles/{roleCode}
        (POST assign, DELETE revoke). All permission-gated:
        identity.role.read, identity.role.create,
        identity.role.assign.
      - identity.yml updated: added identity.role.create permission.
    
    SPA (web/):
      - UsersPage — list with username link to detail, "+ New User"
      - CreateUserPage — username, display name, email form
      - UserDetailPage — shows user info + role toggle list. Each
        role has an Assign/Revoke button that takes effect on the
        user's next login (JWT carries roles from login time).
      - RolesPage — list with inline create form (code + name)
      - Sidebar gains "System" section with Users + Roles links
      - API client + types: identity.listUsers, getUser, createUser,
        listRoles, createRole, getUserRoles, assignRole, revokeRole
    
    Infrastructure:
      - SpaController: added /users/** and /roles/** forwarding
      - SecurityConfiguration: added /users/** and /roles/** to the
        SPA permitAll block
    zichun authored
     
    Browse Code »
  • Extends the R1 SPA with create forms for the four entities operators
    interact with most. Each page follows the same pattern proven by
    CreateSalesOrderPage: a card-scoped form with dropdowns populated
    from the API, inline validation, and a redirect to the detail or
    list page on success.
    
    New pages:
      - CreateItemPage — code, name, type (GOOD/SERVICE/DIGITAL),
        UoM dropdown populated from /api/v1/catalog/uoms
      - CreatePartnerPage — code, name, type (CUSTOMER/SUPPLIER/BOTH),
        optional email + phone
      - CreatePurchaseOrderPage — symmetric to CreateSalesOrderPage;
        supplier dropdown filtered to SUPPLIER/BOTH partners,
        optional expected date, dynamic line items
      - CreateWorkOrderPage — output item + quantity + optional due
        date, dynamic BOM inputs (item + qty/unit + source location
        dropdown), dynamic routing operations (op code + work center
        + std minutes). The most complex form in the SPA — matches
        the EBC-PP-001 work order creation flow
    
    API client additions: catalog.createItem, partners.create,
    purchaseOrders.create, production.createWorkOrder — each a
    typed wrapper around POST to the corresponding endpoint.
    
    List pages updated: Items, Partners, Purchase Orders, Work Orders
    all now show a "+ New" button in the PageHeader that links to
    the create form.
    
    Routes wired: /items/new, /partners/new, /purchase-orders/new,
    /work-orders/new — all covered by the existing SpaController
    wildcard patterns and SecurityConfiguration permitAll rules.
    zichun authored
     
    Browse Code »
  • Reworks the demo seed and SPA to match the reference customer's
    work-order management process (EBC-PP-001 from raw/ docs).
    
    Demo seed (DemoSeedRunner):
      - 7 printing-specific items: paper stock, 4-color ink, CTP plates,
        lamination film, business cards, brochures, posters
      - 4 partners: 2 customers (Wucai Advertising, Globe Marketing),
        2 suppliers (Huazhong Paper, InkPro Industries)
      - 2 warehouses with opening stock for all items
      - Pre-seeded WO-PRINT-0001 with full BOM (3 inputs: paper +
        ink + CTP plates from WH-RAW) and 3-step routing (CTP
        plate-making @ CTP-ROOM-01 -> offset printing @ PRESS-A ->
        post-press finishing @ BIND-01) matching EBC-PP-001 steps
        C-010/C-040
      - 2 DRAFT sales orders: SO-2026-0001 (100x business cards +
        500x brochures, $1950), SO-2026-0002 (200x posters, $760)
      - 1 DRAFT purchase order: PO-2026-0001 (10000x paper + 50kg
        ink, $2550) from Huazhong Paper
    
    SPA additions:
      - New CreateSalesOrderPage with customer dropdown, item
        selector, dynamic line add/remove, quantity + price inputs.
        Navigates to the detail page on creation.
      - "+ New Order" button on the SalesOrdersPage header
      - Dashboard "Try the demo" section rewritten to walk the
        EBC-PP-001 flow: create SO -> confirm (auto-spawns WOs) ->
        walk WO routing -> complete (material issue + production
        receipt) -> ship SO (stock debit + AR settle)
      - salesOrders.create() added to the typed API client
    
    The key demo beat: confirming SO-2026-0001 auto-spawns
    WO-FROM-SO-2026-0001-L1 and -L2 via SalesOrderConfirmedSubscriber
    (EBC-PP-001 step B-010). The pre-seeded WO-PRINT-0001 shows
    the full BOM + routing story separately. Together they
    demonstrate that the framework expresses the customer's
    production workflow through configuration, not code.
    
    Smoke verified on fresh Postgres: all 7 items seeded, WO with
    3 BOM + 3 ops created, SO confirm spawns 2 WOs with source
    traceability, SPA /sales-orders/new renders and creates orders.
    zichun authored
     
    Browse Code »

  • Updates the "at a glance" row to v0.29.0-SNAPSHOT + fc62d6d7,
    bumps the Phase 6 R1 row to DONE with the commit ref and an
    overview of what landed (Gradle wrapper, SpaController, security
    reordering, bundled fat-jar, 16 pages), and rewrites the
    "How to run" section to walk the click-through demo instead of
    just curl. README's status table updated to reflect 10/10 PBCs
    + 356 tests + SPA status; building section now mentions that
    `./gradlew build` compiles the SPA too.
    
    No code changes.
    zichun authored
     
    Browse Code »
  • The R1 SPA chunk added a :web Gradle subproject whose npmBuild
    Exec task runs Vite during :distribution:bootJar. The Dockerfile's
    build stage uses eclipse-temurin:21-jdk-alpine which has no
    node/npm, so the docker image CI job fails with:
    
      process "/bin/sh -c chmod +x ./gradlew && ./gradlew
      :distribution:bootJar --no-daemon" did not complete successfully:
      exit code: 1
    
    Fix: apk add --no-cache nodejs npm before the Gradle build. Alpine
    3.21 ships node v22 + npm 10 which Vite 5 + React 18 handle fine.
    
    The runtime stage stays a pure JRE image — node is only needed at
    build time and never makes it into the shipping container.
    zichun authored
     
    Browse Code »
  • First runnable end-to-end demo: open the browser, log in, click
    through every PBC, and walk a sales order DRAFT → CONFIRMED →
    SHIPPED. Stock balances drop, the SALES_SHIPMENT row appears in the
    ledger, and the AR journal entry settles — all visible in the SPA
    without touching curl.
    
    Bumps version to 0.29.0-SNAPSHOT.
    
    What landed
    -----------
    
    * New `:web` Gradle subproject — Vite + React 18 + TypeScript +
      Tailwind 3.4. The Gradle wrapper is two `Exec` tasks
      (`npmInstall`, `npmBuild`) with proper inputs/outputs declared
      for incremental builds. Deliberately no node-gradle plugin —
      one less moving piece.
    
    * SPA architecture: hand-written typed REST client over `fetch`
      (auth header injection + 401 handler), AuthContext that decodes
      the JWT for display, ProtectedRoute, AppLayout with sidebar
      grouped by PBC, 16 page components covering the full v1 surface
      (Items, UoMs, Partners, Locations, Stock Balances + Movements,
      Sales Orders + detail w/ confirm/ship/cancel, Purchase Orders +
      detail w/ confirm/receive/cancel, Work Orders + detail w/
      start/complete, Shop-Floor dashboard with 5s polling, Journal
      Entries). 211 KB JS / 21 KB CSS gzipped.
    
    * Sales-order detail page: confirm/ship/cancel verbs each refresh
      the order, the (SO-filtered) movements list, and the
      (SO-filtered) journal entries — so an operator watches the
      ledger row appear and the AR row settle in real time after a
      single click. Same pattern on the purchase-order detail page
      for the AP/RECEIPT side.
    
    * Shop-floor dashboard polls /api/v1/production/work-orders/shop-
      floor every 5s and renders one card per IN_PROGRESS WO with
      current operation, planned vs actual minutes (progress bar),
      and operations-completed.
    
    * `:distribution` consumes the SPA dist via a normal Gradle
      outgoing/incoming configuration: `:web` exposes
      `webStaticBundle`, `:distribution`'s `bundleWebStatic` Sync
      task copies it into `${buildDir}/web-static/static/`, and
      that parent directory is added to the main resources source
      set so Spring Boot serves the SPA from `classpath:/static/`
      out of the same fat-jar. Single artifact, no nginx, no CORS.
    
    * New `SpaController` in platform-bootstrap forwards every
      known SPA route prefix to `/index.html` so React Router's
      HTML5 history mode works on hard refresh / deep-link entry.
      Explicit list (12 prefixes) rather than catch-all so typoed
      API URLs still get an honest 404 instead of the SPA shell.
    
    * SecurityConfiguration restructured: keeps the public allowlist
      for /api/v1/auth + /api/v1/_meta + /v3/api-docs + /swagger-ui,
      then `/api/**` is `.authenticated()`, then SPA static assets
      + every SPA route prefix are `.permitAll()`. The order is
      load-bearing — putting `.authenticated()` for /api/** BEFORE
      the SPA permitAll preserves the framework's "API is always
      authenticated" invariant even with the SPA bundled in the
      same fat-jar. The SPA bundle itself is just HTML+CSS+JS so
      permitting it is correct; secrets are gated by /api/**.
    
    * New `DemoSeedRunner` in `:distribution` (gated behind
      `vibeerp.demo.seed=true`, set in application-dev.yaml only).
      Idempotent — the runner short-circuits if its sentinel item
      (DEMO-PAPER-A4) already exists. Seeds 5 items, 2 warehouses,
      4 partners, opening stock for every item, one open
      DEMO-SO-0001 (50× business cards + 20× brochures, $720), one
      open DEMO-PO-0001 (10000× paper, $400). Every row carries
      the DEMO- prefix so it's trivially distinguishable from
      hand-created data; a future "delete demo data" command has
      an obvious filter. Production deploys never set the property,
      so the @ConditionalOnProperty bean stays absent from the
      context.
    
    How to run
    ----------
    
      docker compose up -d db
      ./gradlew :distribution:bootRun
      open http://localhost:8080
    
      # Read the bootstrap admin password from the boot log,
      # log in as admin, and walk DEMO-SO-0001 through the
      # confirm + ship flow to see the buy-sell loop in the UI.
    
    What was caught by the smoke test
    ---------------------------------
    
    * TypeScript strict mode + `error: unknown` in React state →
      `{error && <X/>}` evaluates to `unknown` and JSX rejects it.
      Fixed by typing the state as `Error | null` and converting
      in catches with `e instanceof Error ? e : new Error(String(e))`.
      Affected 16 page files; the conversion is now uniform.
    
    * DataTable's `T extends Record<string, unknown>` constraint was
      too restrictive for typed row interfaces; relaxed to
      unconstrained `T` with `(row as unknown as Record<…>)[key]`
      for the unkeyed cell read fallback.
    
    * `vite.config.ts` needs `@types/node` for `node:path` +
      `__dirname`; added to devDependencies and tsconfig.node.json
      declares `"types": ["node"]`.
    
    * KDoc nested-comment trap (4th time): SpaController's KDoc
      had `/api/v1/...` in backticks; the `/*` inside backticks
      starts a nested block comment and breaks Kotlin compilation
      with "Unclosed comment". Rephrased to "the api-v1 prefix".
    
    * SecurityConfiguration order: a draft version that put the
      SPA permit-all rules BEFORE `/api/**` authenticated() let
      unauthenticated requests reach API endpoints. Caught by an
      explicit smoke test (curl /api/v1/some-bogus-endpoint should
      return 401, not 404 from a missing static file). Reordered
      so /api/** authentication runs first.
    
    End-to-end smoke (real Postgres, fresh DB)
    ------------------------------------------
    
      - bootRun starts in 7.6s
      - DemoSeedRunner reports "populating starter dataset… done"
      - GET / returns the SPA HTML
      - GET /sales-orders, /sales-orders/<uuid>, /journal-entries
        all return 200 (SPA shell — React Router takes over)
      - GET /assets/index-*.js / /assets/index-*.css both 200
      - GET /api/v1/some-bogus-endpoint → 401 (Spring Security
        rejects before any controller mapping)
      - admin login via /api/v1/auth/login → 200 + JWT
      - GET /catalog/items → 5 DEMO-* rows
      - GET /partners/partners → 4 DEMO-* rows
      - GET /inventory/locations → 2 DEMO-* warehouses
      - GET /inventory/balances → 5 starting balances
      - POST /orders/sales-orders/<id>/confirm → CONFIRMED;
        GET /finance/journal-entries shows AR POSTED 720 USD
      - POST /orders/sales-orders/<id>/ship {"shippingLocationCode":
        "DEMO-WH-FG"} → SHIPPED; balances drop to 150 + 80;
        journal entry flips to SETTLED
      - POST /orders/purchase-orders/<id>/confirm → CONFIRMED;
        AP POSTED 400 USD appears
      - POST /orders/purchase-orders/<id>/receive → RECEIVED;
        PAPER balance grows from 5000 to 15000; AP row SETTLED
      - 8 stock_movement rows in the ledger total
    zichun authored
     
    Browse Code »
  • zichun authored
     
    Browse Code »