Skip to content

Implementation Changes: Inventory v00

Precise code modifications for Inventory v00, keyed to the phases in specification.md and the requirements in requirements.md. Paths are repo-relative. Line numbers are from the branch baseline and are indicative — confirm against current code.

Naming: DB columns total_inventory_count_amount, total_inventory_count_unit, last_count_date_timestamp, last_count_date_time_zone. Kotlin fields totalInventoryCount: Quantity.Value?, lastCountDate: DateTime?.

1. Entity + serializer — src/main/kotlin/.../reference/item/business/Item.kt

Section titled “1. Entity + serializer — src/main/kotlin/.../reference/item/business/Item.kt”
  • Add to the Item sealed interface (near minQuantity, ~line 81) and the Entity data class (near ~line 135), with = null defaults:
    • val totalInventoryCount: Quantity.Value?
    • val lastCountDate: DateTime? (import cards.arda.common.lib.domain.general.time.DateTime)
  • Add one element<...>("totalInventoryCount", isOptional = true) and one element<...>("lastCountDate", isOptional = true) to the hand-written ItemSerializer SerialDescriptor (~lines 28–52), and read/write them in the serializer’s encode/decode blocks alongside the existing fields. This is the step most easily missed.
  • No validate() change (REQ-INV-007).

2. DTO — src/main/kotlin/.../reference/item/api/Model.kt

Section titled “2. DTO — src/main/kotlin/.../reference/item/api/Model.kt”
  • In ItemInput (~lines 36–106), add val totalInventoryCount: Quantity.Value? = null and val lastCountDate: DateTime? = null (mirror minQuantity at ~line 45).
  • In toItem() (~lines 60–105), apply the REQ-INV-006 normalization when constructing totalInventoryCount:
    • if amount != null && unit == nullunit = "each"
    • if unit != null && amount == nullamount = 0.0
    • if both null → null Pass lastCountDate through directly.
  • Keep the normalization in a small pure helper so ItemInputModelTest can exercise it directly.

3. Persistence — src/main/kotlin/.../reference/item/persistence/ItemPersistence.kt

Section titled “3. Persistence — src/main/kotlin/.../reference/item/persistence/ItemPersistence.kt”
  • In ITEM_TABLE (~lines 29–52) add:
    • val totalInventoryCount = root.quantityComponent<...>("total_inventory_count") (copy min_quantity)
    • val lastCountDate = root.dateTimeComponent<...>("last_count_date") (copy Order.deliverBy in OrderPersistence.kt:53)
  • In ItemRecord (~lines 54–131): add var delegates for both; add them to the companion insert lambda (~82–105); set them in fromEntity (~107–130); rebuild them in fillPayload() (~133–163).

4. Migration — src/main/resources/reference/item/database/migrations/V022__inventory_count.sql (new)

Section titled “4. Migration — src/main/resources/reference/item/database/migrations/V022__inventory_count.sql (new)”
-- Inventory v00: add totalInventoryCount (Quantity) and lastCountDate (DateTime) to item.
begin;
alter table item add column total_inventory_count_amount DOUBLE PRECISION NULL;
alter table item add column total_inventory_count_unit VARCHAR(255) NULL;
alter table item add column last_count_date_timestamp TIMESTAMP NULL;
alter table item add column last_count_date_time_zone VARCHAR(255) NULL;
commit;

Confirm the exact DateTime component column suffixes/types against DateTimeComponent.kt (forName) before finalizing.

  • ItemDDLTest — assert the four new columns.
  • ItemInputModelTest — round-trip both fields + normalization cases (amount-only ⇒ unit “each”; unit-only ⇒ amount 0; both null ⇒ null).
  • ItemUniverseTestData — add values to shared fixtures.
  • AddItemTest / ItemUniverseTest / ItemEndpointTest — create, read (null when unset), update, and clear-to-null via omission.

Domain carries the date as an ISO string (Decision D3); the mapper converts to/from the ARDA DateTime shape. Grid paths: totalInventoryCountAmount, totalInventoryCountUnit, lastCountDate.

DateTime interchange contract (from common-module DateTime / TzIana): JSON is { timestamp: number /* epoch millis */, tz: string /* IANA name via @SerialName, e.g. "America/New_York" */ }. The frontend has no existing precedent for this shape, so define an ArdaDateTime DTO type and two helpers:

  • to domain: dt => new Date(dt.timestamp).toISOString() (carry ISO string; keep dt.tz if the display needs the original zone).
  • from domain (Detail panel, full datetime): parse the local datetime-local value in the browser tz → { timestamp: <millis>, tz: <browser IANA> }.
  • from domain (inline, date only — REQ-INV-012): given the picked date and the hinted tz, compute the epoch millis of 12:00:00 that date in that tz (noon avoids date roll under tz/DST) → { timestamp, tz }.
  • src/types/items.ts (Item, ~lines 213–237): add totalInventoryCount?: Quantity and lastCountDate?: string.
  • src/types/arda-api.ts: add both to ArdaItemPayload and ArdaCreateItemRequest (count as amount/unit; date as the ARDA DateTime representation used by Order.deliverBy).
  • src/constants/types.ts: add to ItemFormState (count amount as number | '', unit as string, date as ISO string) and to ItemCard.

2. Mappers — src/lib/mappers/ardaMappers.ts

Section titled “2. Mappers — src/lib/mappers/ardaMappers.ts”
  • mapArdaItemToItem (~299–389): read both fields into the domain shape (convert DTO DateTime → ISO string).
  • mapItemToArdaCreateRequest (~395+) and mapItemToArdaUpdateRequest (~597–643): write both; use the existing emptyToNull / nullToUndefined helpers so an empty value is omitted (clear-to-null, REQ-INV-005/022). Convert ISO string → ARDA DateTime.

3. Grid columns — src/components/table/columnPresets.itemColumns.detail.tsx

Section titled “3. Grid columns — src/components/table/columnPresets.itemColumns.detail.tsx”
  • Add three colDefs (copy the Min Qty / Min Unit shape at columnPresets.itemColumns.tsx:265–286 and the Created date column at columnPresets.itemColumns.detail.tsx:285–293):
    • { headerName: 'Total Inventory Count', field: 'totalInventoryCount.amount', colId: 'totalInventoryCountAmount', hide: true, valueFormatter: p => String(p.value ?? '-') }
    • { headerName: 'Count Unit', field: 'totalInventoryCount.unit', colId: 'totalInventoryCountUnit', hide: true, cellRenderer: … ?? '-' }
    • { headerName: 'Last Count Date', field: 'lastCountDate', colId: 'lastCountDate', hide: true, valueFormatter: p => formatDate(p.value) }
  • Include them in buildItemsColumnDefs (columnPresets.itemColumns.tsx:414–423).
  • Register in src/app/items/itemTableConfig.ts (VIEW_KEY_TO_FIELD) and add toggles in src/app/items/sections/ColumnVisibilityMenu.tsx.

4. Inline edit — src/app/items/gridColumnEnhancers.tsx

Section titled “4. Inline edit — src/app/items/gridColumnEnhancers.tsx”
  • getEditableCellValue (~115): add
    • if (path === 'totalInventoryCountAmount') return d.totalInventoryCount?.amount != null ? String(d.totalInventoryCount.amount) : '';
    • if (path === 'totalInventoryCountUnit') return d.totalInventoryCount?.unit ?? '';
    • if (path === 'lastCountDate') return d.lastCountDate ?? '';
  • applyEditableCellValue (~156): add (note amount default 0, not 1):
    • amount: const current = originalData.totalInventoryCount || { amount: 0, unit: 'each' }; const n = parseFloat(String(v ?? '')); setNested(d, 'totalInventoryCount', { ...current, amount: isNaN(n) ? 0 : n });
    • unit: const current = originalData.totalInventoryCount || { amount: 0, unit: 'each' }; setNested(d, 'totalInventoryCount', { ...current, unit: String(v ?? '').trim() || 'each' });
    • date: setNested(d, 'lastCountDate', v == null || v === '' ? undefined : String(v));
  • In the enhancer’s per-column spread (~312–322): attach cellEditor: UnitCellEditor for totalInventoryCountUnit, and a date cell editor (DS createDateCellEditor() / DateCellEditor) for lastCountDate. Amount uses the default editor.

5. Detail View — src/components/items/ItemDetailsPanel.tsx

Section titled “5. Detail View — src/components/items/ItemDetailsPanel.tsx”
  • src/constants/types.ts ItemCard: add both fields; src/app/items/hooks/itemsPageUtils.ts convertItemToItemCard (~10–25): populate them.
  • In the field stack (~1242–1271), add a titled “Inventory Count” group at the end with two ReadOnlyField rows (count formatted as amount unit; date via formatDate/formatDateTime), fallback = the existing placeholder.

6. Detail Edit — src/components/items/ItemFormPanel.tsx

Section titled “6. Detail Edit — src/components/items/ItemFormPanel.tsx”
  • Add an “Inventory Count” <div className={sectionClassName}> after the “Additional Info” section (~after line 2542), before the footer.
  • Add a tocSections entry (~1222–1229) with a ref.
  • Prefill in the item→form useEffect (~567–674): count amount/unit and date ISO (default '' / date pre-seeds to now when empty, REQ-INV-023).
  • In the submit builder newItem (~880–1122): emit totalInventoryCount only when amount/unit present (mirror minQuantity at ~905–911); emit lastCountDate when set, else undefined (clear-to-null).
  • Count: number input + unit input (reuse the Min Qty pattern). Date: clearable date-time input (native <input type="datetime-local"> via components/ui/input.tsx).
  • src/mocks/data/mockItems.ts: one item with both set, one with both null.
  • Component tests: ItemDetailsPanel.test.tsx, ItemFormPanel.test.tsx (+ .toc), column/renderer tests, ardaMappers mapper tests.
  • Playwright: e2e/pages/items.page.ts, e2e/specs/items/edit-item.spec.ts, item-details.spec.ts, items-grid.spec.ts — view, inline-edit, detail-edit, clear-to-null.
  • Update the Item current-system functional reference to document both fields (type, nullability, normalization/default behavior). Do not edit CHANGELOG.md (PR-body changelog).

Copyright: (c) Arda Systems 2025-2026, All rights reserved