Elements Version 3.8+
Overview #
The Item Ledger is an append-only audit trail that records every inventory and item-catalog lifecycle event in the system. It answers the question: “when did this user acquire, modify, or lose this item, and who made the change?”
Every time an inventory item or item-catalog record is created, modified, or deleted, the responsible service writes an immutable ItemLedgerEntry to the item_ledger MongoDB collection. Records are never updated or deleted – the collection grows monotonically and serves as a permanent history.
Reads are superuser-only. Regular users and unauthenticated callers receive a ForbiddenException.
What is recorded #
| Trigger | Event type |
|---|---|
| Fungible inventory item created | CREATED |
| Fungible inventory quantity adjusted by delta | QUANTITY_ADJUSTED |
| Fungible inventory quantity set to absolute value | QUANTITY_SET |
| Fungible inventory item deleted | DELETED |
| Distinct inventory item created | CREATED |
| Distinct inventory item metadata updated | METADATA_UPDATED |
| Distinct inventory item deleted | DELETED |
| Item catalog entry created | ITEM_CREATED |
| Item catalog entry updated | ITEM_UPDATED |
| Item catalog entry deleted | ITEM_DELETED |
Model #
Class: dev.getelements.elements.sdk.model.inventory.ItemLedgerEntry
| Field | Type | Description |
|---|---|---|
id | String | Database id. Assigned on creation; null on write. |
inventoryItemId | String | Id of the affected inventory item. Null for item-catalog events (ITEM_CREATED, ITEM_UPDATED, ITEM_DELETED). |
itemCategory | ItemCategory | FUNGIBLE or DISTINCT. Null for item-catalog events. |
itemId | String | Id of the item definition (the Item catalog record). |
userId | String | Id of the user who owns the inventory item. For item-catalog events this is set to the actorId. |
actorId | String | Id of the user who performed the action. Null when triggered by a system process or plugin without an active user session. |
eventType | ItemLedgerEventType | Classifies the event. See event types below. |
timestamp | long | Epoch milliseconds when the event was recorded. |
quantityBefore | Integer | Quantity before the change. Populated only for QUANTITY_ADJUSTED. |
quantityAfter | Integer | Quantity after the change. Populated for CREATED, QUANTITY_ADJUSTED, and QUANTITY_SET. |
metadataBefore | Map<String, Object> | Metadata snapshot before the change. Populated only for METADATA_UPDATED. |
metadataAfter | Map<String, Object> | Metadata snapshot after the change. Populated only for METADATA_UPDATED. |
Event types #
Class: dev.getelements.elements.sdk.model.inventory.ItemLedgerEventType
| Value | Applies to | Description |
|---|---|---|
CREATED | Fungible and distinct items | Item was added to a user’s inventory. |
QUANTITY_ADJUSTED | Fungible items | Quantity changed by a signed delta. |
QUANTITY_SET | Fungible items | Quantity replaced with an absolute value. |
DELETED | Fungible and distinct items | Item was removed from inventory. |
METADATA_UPDATED | Distinct items | Per-item metadata was updated. |
ITEM_CREATED | Item catalog | An Item definition was created. |
ITEM_UPDATED | Item catalog | An Item definition was updated. |
ITEM_DELETED | Item catalog | An Item definition was deleted. |
REST API #
Base path: /api/rest/inventory/ledger
Get ledger entries #
GET /api/rest/inventory/ledger
Returns paginated ItemLedgerEntry objects sorted most recent first. Exactly one of inventoryItemId or userId is required.
Query parameters
| Parameter | Required | Description |
|---|---|---|
inventoryItemId | One of these two is required | Filter entries for a specific inventory item. |
userId | One of these two is required | Filter entries for a specific user (across all their inventory items). |
eventType | No | Filter by event type. Omit to return all event types. |
offset | No (default 0) | Zero-based page offset. |
count | No (default 20) | Number of results per page. |
Responses
| Status | Description |
|---|---|
200 OK | Pagination<ItemLedgerEntry> – paginated results. |
400 Bad Request | Neither inventoryItemId nor userId was supplied, or a numeric parameter is negative. |
403 Forbidden | Caller is not a superuser. |
Example – full history for an inventory item
GET /api/rest/inventory/ledger?inventoryItemId=6630f1a2b4e3c70012345678
Authorization: Bearer <superuser-token>
{
"total": 3,
"objects": [
{
"id": "6630f1a2b4e3c70099999993",
"inventoryItemId": "6630f1a2b4e3c70012345678",
"itemCategory": "FUNGIBLE",
"itemId": "6630f1a2b4e3c70011111111",
"userId": "6630f1a2b4e3c70022222222",
"actorId": "6630f1a2b4e3c70033333333",
"eventType": "QUANTITY_ADJUSTED",
"timestamp": 1714000800000,
"quantityBefore": 10,
"quantityAfter": 15
},
{
"id": "6630f1a2b4e3c70099999992",
"inventoryItemId": "6630f1a2b4e3c70012345678",
"itemCategory": "FUNGIBLE",
"itemId": "6630f1a2b4e3c70011111111",
"userId": "6630f1a2b4e3c70022222222",
"actorId": "6630f1a2b4e3c70033333333",
"eventType": "QUANTITY_SET",
"timestamp": 1714000700000,
"quantityAfter": 10
},
{
"id": "6630f1a2b4e3c70099999991",
"inventoryItemId": "6630f1a2b4e3c70012345678",
"itemCategory": "FUNGIBLE",
"itemId": "6630f1a2b4e3c70011111111",
"userId": "6630f1a2b4e3c70022222222",
"actorId": "6630f1a2b4e3c70033333333",
"eventType": "CREATED",
"timestamp": 1714000600000,
"quantityBefore": 0,
"quantityAfter": 5
}
]
}
Example – user history filtered to creation events
GET /api/rest/inventory/ledger?userId=6630f1a2b4e3c70022222222&eventType=CREATED
Authorization: Bearer <superuser-token>
Example – entries within a timestamp range
GET /api/rest/inventory/ledger?inventoryItemId=6630f1a2b4e3c70012345678&from=1714000000000&to=1714001000000
Authorization: Bearer <superuser-token>
DAO interface #
Interface: dev.getelements.elements.sdk.dao.ItemLedgerDao Annotation: @ElementServiceExport
/** Appends an immutable audit record. */
ItemLedgerEntry createLedgerEntry(ItemLedgerEntry entry);
/** Entries for a specific inventory item, most recent first. eventType/from/to may be null. */
Pagination<ItemLedgerEntry> getLedgerEntries(
String inventoryItemId, int offset, int count,
ItemLedgerEventType eventType, Long from, Long to);
/** All entries for a specific user across all inventory items, most recent first. eventType/from/to may be null. */
Pagination<ItemLedgerEntry> getLedgerEntriesForUser(
String userId, int offset, int count,
ItemLedgerEventType eventType, Long from, Long to);
There are intentionally no update or delete methods – the interface enforces immutability.
Service interface #
Interface: dev.getelements.elements.sdk.service.inventory.ItemLedgerService Annotations: @ElementPublic, @ElementServiceExport, @ElementServiceExport(name = UNSCOPED)
The service is read-only and exposes the same two query methods as the DAO.
| Access level | Behaviour |
|---|---|
SUPERUSER | Full read access. |
| Any other level | Throws ForbiddenException. |
How writes work #
Ledger entries are written automatically by the inventory and item-catalog service implementations immediately after each successful mutation. No explicit calls are needed by callers of those services.
SuperUserSimpleInventoryItemService ─|
SuperUserAdvancedInventoryItemService ─|
SuperUserDistinctInventoryItemService ─|
SuperuserItemService (catalog) ─|----> ItemLedgerDao.createLedgerEntry(...)
The actorId is set to the id of the currently authenticated user. If the call originates from a plugin or system process without an active user session, actorId is null.
Item-catalog events (ITEM_CREATED, ITEM_UPDATED, ITEM_DELETED) set inventoryItemId, itemCategory, and userId to null; actorId holds the id of the superuser who made the change.
Reading the ledger from a plugin #
@Inject
private ItemLedgerService itemLedgerService;
// Full history for one inventory item
Pagination<ItemLedgerEntry> history = itemLedgerService
.getLedgerEntries(inventoryItemId, 0, 20, null, null, null);
// Creation events for a user
Pagination<ItemLedgerEntry> created = itemLedgerService
.getLedgerEntriesForUser(userId, 0, 20, ItemLedgerEventType.CREATED, null, null);
// Entries within a timestamp range
long from = Instant.parse("2024-04-25T00:00:00Z").toEpochMilli();
long to = Instant.parse("2024-04-26T00:00:00Z").toEpochMilli();
Pagination<ItemLedgerEntry> ranged = itemLedgerService
.getLedgerEntries(inventoryItemId, 0, 20, null, from, to);
The service must be injected from a superuser-scoped or UNSCOPED context.
Best practices #
- The ledger is append-only and grows indefinitely. Add a TTL index to
item_ledger.timestampif unbounded growth is a concern for your deployment. - Use the
eventTypefilter to narrow queries – scanning all event types for a high-traffic user can be expensive at scale. - For
QUANTITY_ADJUSTEDevents,quantityBefore + delta = quantityAfter. ForQUANTITY_SETevents, onlyquantityAfteris recorded to avoid an extra DAO read. METADATA_UPDATEDentries include fullmetadataBeforeandmetadataAftersnapshots. For items with large metadata objects this doubles the storage cost per update.- Item-catalog events cannot be retrieved via the
inventoryItemIdoruserIdREST filters. Query MongoDB directly or useItemLedgerDaoin a plugin for catalog-only audit needs.
FAQ #
Q: Why is the ledger superuser-only? A: Inventory histories can reveal sensitive activity information about other users. Only privileged callers (administrators, backend services) should be able to read them.
Q: Does deleting an inventory item also delete its ledger entries? A: No. Ledger entries are immutable and are never removed. After deletion a DELETED entry is appended; the prior history remains intact.
Q: Can I query entries for a specific event type across all users? A: Not directly via the REST API. For cross-user analytics, query MongoDB directly or build a custom aggregation in a plugin using ItemLedgerDao.
Q: Are entries written inside a transaction with the inventory mutation? A: Not currently. The ledger write happens immediately after the DAO call returns. If the ledger write fails the inventory mutation has already been committed – this is an acceptable trade-off for an audit trail where eventual consistency is sufficient.

