@proveanything/smartlinks 1.15.23 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,18 @@ export interface InteractionFilterValue {
5
5
  from?: string;
6
6
  to?: string;
7
7
  }
8
+ export type CustomFieldOperator = 'equals' | 'exists' | 'gt' | 'gte' | 'lt' | 'lte' | 'before' | 'after' | 'within_days' | 'older_than_days';
9
+ export type OwnershipAttributeOperator = 'equals' | 'exists' | 'gt' | 'gte' | 'lt' | 'lte';
10
+ export interface OwnershipAttributePredicate {
11
+ path: string;
12
+ operator: OwnershipAttributeOperator;
13
+ /** Operand: a number for gt/gte/lt/lte, a string for equals; omitted for exists. */
14
+ value?: string | number;
15
+ }
16
+ export interface OwnershipFacetSelector {
17
+ key: string;
18
+ value: string;
19
+ }
8
20
  export type SegmentFilterRule = {
9
21
  field: 'interaction';
10
22
  op: 'had' | 'exists';
@@ -28,6 +40,19 @@ export type SegmentFilterRule = {
28
40
  from?: string;
29
41
  to?: string;
30
42
  };
43
+ } | {
44
+ type: 'custom_field';
45
+ field: string;
46
+ operator: CustomFieldOperator;
47
+ value?: string | number;
48
+ } | {
49
+ type: 'ownership';
50
+ productId?: string;
51
+ facet?: OwnershipFacetSelector;
52
+ attribute?: OwnershipAttributePredicate;
53
+ olderThanDays?: number;
54
+ withinDays?: number;
55
+ includeDeleted?: boolean;
31
56
  } | {
32
57
  type: 'interaction';
33
58
  interactionId: string;
@@ -116,11 +116,18 @@ export interface Gs1DigitalLinkParams {
116
116
  product?: Product;
117
117
  /** Override the global-owner flag; otherwise read from `product.ownGtin`. */
118
118
  ownGtin?: boolean;
119
- /** Consumer Product Variant (AI 22). A string or an object with `id`. */
119
+ /**
120
+ * A real GS1 **Consumer Product Variant** code (AI 22). Use this when the brand has a
121
+ * genuine CPV. Takes precedence over `variant` when both are given.
122
+ */
120
123
  cpv?: string | {
121
124
  id: string;
122
125
  };
123
- /** Alias of `cpv` (AI 22). */
126
+ /**
127
+ * Internal variant id, emitted as AI 22 (the SmartLinks resolver reads path segment 22
128
+ * as the variant). Prefer `cpv` when you have a real GS1 CPV code — a non-CPV variant id
129
+ * in AI 22 is only meaningful to the SmartLinks resolver, not to third-party GS1 resolvers.
130
+ */
124
131
  variant?: string | {
125
132
  id: string;
126
133
  };
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.15.23 | Generated: 2026-08-28T10:57:24.158Z
3
+ Version: 1.16.0 | Generated: 2026-09-01T12:28:06.435Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -35,6 +35,7 @@ For detailed guides on specific features:
35
35
  - **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
36
36
  - **[Proof Share Grants](proof-share-grants.md)** - Delegated, scoped, revocable bearer access to a single proof (read/comment/verify-owner links)
37
37
  - **[Proof Ownership Transfer](proof-ownership-transfer.md)** - Moving a proof to a new owner: directed transfer, open release, accept/cancel, and the state machine
38
+ - **[Lots](lots.md)** - Collection-scoped production groupings spanning many SKUs; facet/product selectors, member resolution, and GS1 AI(10) batch-then-lot resolution
38
39
  - **[Item Context](item-context.md)** - The `itemContext` container prop derived from a serial-proof URL or NFC tap (what item the URL points at)
39
40
  - **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
40
41
  - **[Attestations](attestations.md)** - Append-only fact log with cryptographic chain integrity, time-series analytics, and public/owner/admin visibility
@@ -135,6 +136,7 @@ The Smartlinks SDK is organized into the following namespaces:
135
136
  - **jobs** - Functions for jobs operations
136
137
  - **journeysAnalytics** - Functions for journeysAnalytics operations
137
138
  - **location** - Functions for location operations
139
+ - **lots** - Functions for lots operations
138
140
  - **navigation** - Functions for navigation operations
139
141
  - **order** - Functions for order operations
140
142
  - **products** - Functions for products operations
@@ -6355,6 +6357,120 @@ interface LocationSearchResponse {
6355
6357
 
6356
6358
  **LocationPayload** = `Omit<`
6357
6359
 
6360
+ ### lots
6361
+
6362
+ **LotPayload** (interface)
6363
+ ```typescript
6364
+ interface LotPayload {
6365
+ manufacturedAt?: string | null
6366
+ expiresAt?: string | null
6367
+ location?: string | null
6368
+ custom?: Record<string, any>
6369
+ [key: string]: any
6370
+ }
6371
+ ```
6372
+
6373
+ **Lot** (interface)
6374
+ ```typescript
6375
+ interface Lot {
6376
+ id: string
6377
+ collectionId: string
6378
+ lotNumber: string
6379
+ name?: string | null
6380
+ description?: string | null
6381
+ status: LotStatus
6382
+ selector: LotSelector
6383
+ payload?: LotPayload
6384
+ destination?: Record<string, any> | null
6385
+ productCount: number
6386
+ productIds?: string[]
6387
+ resolvedAt?: string | null
6388
+ createdBy?: string | null
6389
+ updatedBy?: string | null
6390
+ createdAt: string
6391
+ updatedAt: string
6392
+ }
6393
+ ```
6394
+
6395
+ **LotCreateInput** (interface)
6396
+ ```typescript
6397
+ interface LotCreateInput {
6398
+ lotNumber: string
6399
+ name?: string
6400
+ description?: string
6401
+ selector?: LotSelector
6402
+ payload?: LotPayload
6403
+ destination?: Record<string, any> | null
6404
+ status?: LotStatus
6405
+ id?: string
6406
+ }
6407
+ ```
6408
+
6409
+ **ListLotsParams** (interface)
6410
+ ```typescript
6411
+ interface ListLotsParams {
6412
+ status?: LotStatus
6413
+ search?: string
6414
+ productId?: string
6415
+ }
6416
+ ```
6417
+
6418
+ **ListLotsResponse** (interface)
6419
+ ```typescript
6420
+ interface ListLotsResponse {
6421
+ lots: Lot[]
6422
+ }
6423
+ ```
6424
+
6425
+ **LotMemberDiff** (interface)
6426
+ ```typescript
6427
+ interface LotMemberDiff {
6428
+ added: string[]; removed: string[]
6429
+ }
6430
+ ```
6431
+
6432
+ **ResolveLotResponse** (interface)
6433
+ ```typescript
6434
+ interface ResolveLotResponse {
6435
+ lot: Lot; diff: LotMemberDiff
6436
+ }
6437
+ ```
6438
+
6439
+ **LotProductSummary** (interface)
6440
+ ```typescript
6441
+ interface LotProductSummary {
6442
+ id: string; name?: string; gtin?: string | null
6443
+ }
6444
+ ```
6445
+
6446
+ **ListLotProductsResponse** (interface)
6447
+ ```typescript
6448
+ interface ListLotProductsResponse {
6449
+ products: LotProductSummary[]
6450
+ total: number
6451
+ page: number
6452
+ limit: number
6453
+ }
6454
+ ```
6455
+
6456
+ **LotResolutionResult** (interface)
6457
+ ```typescript
6458
+ interface LotResolutionResult {
6459
+ match: 'batch' | 'lot' | 'product' | 'none'
6460
+ productId: string
6461
+ batchId: string | null
6462
+ lotId: string | null
6463
+ ai10: string | null
6464
+ destination?: any
6465
+ }
6466
+ ```
6467
+
6468
+ **LotStatus** = `'open' | 'closed' | 'recalled' | 'archived'`
6469
+
6470
+ **LotSelector** = ``
6471
+
6472
+ **LotUpdateInput** = `Partial<LotCreateInput>`
6473
+
6358
6474
  ### loyalty
6359
6475
 
6360
6476
  **LoyaltyScheme** (interface)
@@ -7329,6 +7445,9 @@ interface Proof {
7329
7445
  productId: string
7330
7446
  tokenId: string
7331
7447
  userId: string
7448
+ batchId?: string | null
7449
+ variantId?: string | null
7450
+ lotId?: string | null
7332
7451
  claimable?: boolean
7333
7452
  virtual?: boolean
7334
7453
  data?: Record<string, JsonValue>
@@ -7531,6 +7650,23 @@ interface InteractionFilterValue {
7531
7650
  }
7532
7651
  ```
7533
7652
 
7653
+ **OwnershipAttributePredicate** (interface)
7654
+ ```typescript
7655
+ interface OwnershipAttributePredicate {
7656
+ path: string
7657
+ operator: OwnershipAttributeOperator
7658
+ value?: string | number
7659
+ }
7660
+ ```
7661
+
7662
+ **OwnershipFacetSelector** (interface)
7663
+ ```typescript
7664
+ interface OwnershipFacetSelector {
7665
+ key: string
7666
+ value: string
7667
+ }
7668
+ ```
7669
+
7534
7670
  **SegmentRecord** (interface)
7535
7671
  ```typescript
7536
7672
  interface SegmentRecord {
@@ -7591,6 +7727,10 @@ interface SegmentRecipientsResponse {
7591
7727
  }
7592
7728
  ```
7593
7729
 
7730
+ **CustomFieldOperator** = ``
7731
+
7732
+ **OwnershipAttributeOperator** = `'equals' | 'exists' | 'gt' | 'gte' | 'lt' | 'lte'`
7733
+
7594
7734
  **SegmentFilterRule** = ``
7595
7735
 
7596
7736
  ### tags
@@ -8283,7 +8423,12 @@ interface Gs1DigitalLinkParams {
8283
8423
  gtin?: string
8284
8424
  product?: Product
8285
8425
  ownGtin?: boolean
8426
+ * A real GS1 **Consumer Product Variant** code (AI 22). Use this when the brand has a
8427
+ * genuine CPV. Takes precedence over `variant` when both are given.
8286
8428
  cpv?: string | { id: string }
8429
+ * Internal variant id, emitted as AI 22 (the SmartLinks resolver reads path segment 22
8430
+ * as the variant). Prefer `cpv` when you have a real GS1 CPV code — a non-CPV variant id
8431
+ * in AI 22 is only meaningful to the SmartLinks resolver, not to third-party GS1 resolvers.
8287
8432
  variant?: string | { id: string }
8288
8433
  lot?: string | { id: string }
8289
8434
  batch?: BatchResponse | string
@@ -9745,6 +9890,44 @@ Public: Fetch a global location by ID GET /public/location/:locationId
9745
9890
  locationId: string) → `Promise<Location>`
9746
9891
  Public: Fetch a location for a collection; returns either a collection-owned or global fallback GET /public/collection/:collectionId/location/:locationId
9747
9892
 
9893
+ ### lots
9894
+
9895
+ **create**(collectionId: string, lot: LotCreateInput) → `Promise<Lot>`
9896
+ Create a lot (resolves its selector into members).
9897
+
9898
+ **list**(collectionId: string, params: ListLotsParams = {}) → `Promise<Lot[]>`
9899
+ List lots (summary rows; `payload`/`productIds` omitted). Filter by status, search, or containing productId.
9900
+
9901
+ **get**(collectionId: string, lotId: string) → `Promise<Lot>`
9902
+ Get the full lot record.
9903
+
9904
+ **getByNumber**(collectionId: string, lotNumber: string) → `Promise<Lot>`
9905
+ Look up a lot by its number (case-insensitive) — used by scan/resolver flows.
9906
+
9907
+ **update**(collectionId: string, lotId: string, lot: LotUpdateInput) → `Promise<Lot>`
9908
+ Update a lot. Re-resolves members if the selector changed (response then carries `diff`).
9909
+
9910
+ **archive**(collectionId: string, lotId: string) → `Promise<`
9911
+ Soft-archive a lot (never deletes members).
9912
+
9913
+ **resolve**(collectionId: string, lotId: string) → `Promise<ResolveLotResponse>`
9914
+ Re-resolve members from the current selector; returns the lot + a member diff.
9915
+
9916
+ **listProducts**(collectionId: string, lotId: string, opts: { page?: number; limit?: number } = {}) → `Promise<ListLotProductsResponse>`
9917
+ Paginated member product summaries.
9918
+
9919
+ **publicList**(collectionId: string, params: ListLotsParams = {}) → `Promise<Lot[]>`
9920
+ Paginated member product summaries.
9921
+
9922
+ **publicGet**(collectionId: string, lotId: string) → `Promise<Lot>`
9923
+ Paginated member product summaries.
9924
+
9925
+ **publicGetByNumber**(collectionId: string, lotNumber: string) → `Promise<Lot>`
9926
+ Paginated member product summaries.
9927
+
9928
+ **publicListProducts**(collectionId: string, lotId: string, opts: { page?: number; limit?: number } = {}) → `Promise<ListLotProductsResponse>`
9929
+ Paginated member product summaries.
9930
+
9748
9931
  ### loyalty
9749
9932
 
9750
9933
  Loyalty programmes built on top of collections. Configure schemes and earning rules; contacts earn points automatically via interaction events. See the [Loyalty guide](loyalty.md) for the full walkthrough.
package/docs/appConfig.md CHANGED
@@ -298,8 +298,9 @@ about:
298
298
  | `hub` | The Hub module. |
299
299
  | `portal` | QR code scan portals. |
300
300
  | `virtualItems` | Virtual items — algorithmic per-item IDs with no persistent row (battery serials, scan-to-collect points, bulk QR sheets). Replaces the old root-level `virtualItemsEnabled` boolean, which has been removed (§8) — resolve this like any other feature flag, not as a separate config key. Independent of `itemRecordMode`. |
301
- | `batches` | Batch support. Used to be the `Collection.batches` boolean; that field has been removed — this flag is now the only source of truth. |
301
+ | `batches` | Batch support. Used to be the `Collection.batches` boolean; that field has been removed — this flag is now the only source of truth. Also gates AI(10) **batch** resolution on a GS1 Digital Link scan (§ see `lots.md`). |
302
302
  | `variants` | Variant support. Used to be the `Collection.variants` boolean; that field has been removed — this flag is now the only source of truth. |
303
+ | `lots` | Lot support — collection-scoped production groupings spanning many SKUs (see `lots.md`). Gates AI(10) **lot** resolution on a GS1 Digital Link scan. Independent of `batches`; when both are on, a scanned AI(10) value resolves batch-first (specific SKU) then lot (broad). |
303
304
 
304
305
  Same resolution rule as any flag (§4.4) — don't read these off
305
306
  `cfg.system.features` directly, always go through `isFeatureEnabled()`.
package/docs/lots.md ADDED
@@ -0,0 +1,94 @@
1
+ # Lots
2
+
3
+ A **Lot** is a collection-scoped production grouping that spans one or more products
4
+ (SKUs) — a single identifier applied across many SKUs and/or many production runs. It's
5
+ the right tool when a manufacturer wants one lot number (e.g. `LOT-2026-09`) across a whole
6
+ range, rather than a per-product **batch** (a single run of a single product).
7
+
8
+ Lots are a first-class entity (not app records): cross-app readable, admin-written, with a
9
+ real lifecycle. They are **never fanned out into batches** — the lot is the single source of
10
+ truth for its shared data.
11
+
12
+ ---
13
+
14
+ ## Concepts
15
+
16
+ - **Selector** — how member products are matched. Two modes:
17
+ - `{ mode: 'facets', rules: [{ key, values }] }` — AND across rules, OR within a rule's values (resolved against the facet index, so it scales to thousands of SKUs).
18
+ - `{ mode: 'products', productIds: [...] }` — an explicit list.
19
+ - **`productIds` / `productCount`** — the materialised snapshot of resolved members (re-resolved on create, on selector change, and on demand via `resolve`).
20
+ - **`payload`** — shared lot data (dates, supplier ref, custom fields). Lives only on the lot.
21
+ - **`status`** — `open` → `closed` → `recalled` → `archived`. Archiving never deletes anything.
22
+ - **`destination`** — optional lot-level redirect; wins over the product's on a lot-scoped scan.
23
+
24
+ ---
25
+
26
+ ## SDK — `SL.lots.*`
27
+
28
+ Writes and admin reads hit `/admin/collection/:cid/lots`; the `public*` reads hit
29
+ `/public/collection/:cid/lots` for cross-app consumers (auth is the ambient bearer token —
30
+ there's no `admin` flag).
31
+
32
+ ```ts
33
+ import { lots } from '@proveanything/smartlinks'
34
+
35
+ // Create — facet-targeted lot
36
+ const lot = await lots.create(collectionId, {
37
+ lotNumber: 'LOT-2026-09',
38
+ name: 'September Oak run',
39
+ selector: { mode: 'facets', rules: [
40
+ { key: 'supplier', values: ['Acme Timber'] },
41
+ { key: 'range', values: ['Oslo', 'Bergen'] },
42
+ ]},
43
+ payload: { manufacturedAt: '2026-09-01', custom: { supplierBatchRef: 'ACM-7741' } },
44
+ })
45
+
46
+ const list = await lots.list(collectionId, { status: 'open' })
47
+ const byId = await lots.get(collectionId, lot.id)
48
+ const byNumber = await lots.getByNumber(collectionId, 'LOT-2026-09') // case-insensitive
49
+ const containing = await lots.list(collectionId, { productId: 'prd_abc' }) // reverse lookup
50
+ const updated = await lots.update(collectionId, lot.id, { status: 'closed' })
51
+ const { diff } = await lots.resolve(collectionId, lot.id) // { added, removed }
52
+ const members = await lots.listProducts(collectionId, lot.id, { page: 1, limit: 50 })
53
+ await lots.archive(collectionId, lot.id)
54
+
55
+ // Cross-app reads
56
+ const publicLots = await lots.publicList(collectionId)
57
+ ```
58
+
59
+ Exported types: `Lot`, `LotStatus`, `LotSelector`, `LotPayload`, `LotCreateInput`,
60
+ `LotUpdateInput`, `ListLotsParams`, `ResolveLotResponse`, `ListLotProductsResponse`,
61
+ `LotResolutionResult`.
62
+
63
+ ---
64
+
65
+ ## GS1 Digital Link resolution — AI(10)
66
+
67
+ GS1 gives batch and lot a **single** slot: AI(10) ("Batch or Lot Number"), in a Digital Link
68
+ as `/01/{gtin}/10/{value}`. A physical code carries exactly one value there, so the server
69
+ decides which namespace it belongs to, driven by two **feature flags** — `batches` and `lots`
70
+ — in the platform feature-flag system (`appConfig.system.features`), resolved the standard way
71
+ (`appConfiguration.isFeatureEnabled(collectionId, 'lots')`):
72
+
73
+ - Explicit `true`/`false` in `system.features` wins; otherwise **enterprise** accounts default
74
+ a flag on and everyone else defaults off. So AI(10) batch/lot resolution is opt-in — no
75
+ existing scan changes until `batches`/`lots` is enabled for the collection.
76
+
77
+ Resolution order — **batch first, then lot** (a hierarchy, not a collision):
78
+
79
+ 1. **Batch** is the narrowest scope (one product). If the scanned product has a batch whose id
80
+ or name matches the AI(10) value, it wins — a batch is a **specific-SKU override**.
81
+ 2. **Lot** is broad (many products). If no batch matches and the value is a lot number *and the
82
+ scanned product is a member*, the lot resolves.
83
+ 3. Otherwise it resolves at the product level.
84
+
85
+ This means a range can share one lot code, and a single SKU can be given richer/overriding
86
+ detail by creating a batch with the **same** identifier — the batch simply takes precedence for
87
+ that SKU. A recalled lot (`status: 'recalled'`) still resolves so the destination page can show
88
+ a recall notice (a `&recall=1` context param is added).
89
+
90
+ The server returns a typed shape ({@link LotResolutionResult}); the front end renders it — clients
91
+ should not implement their own fallback.
92
+
93
+ See also [proof-product-data-scoping.md](proof-product-data-scoping.md) and the GS1 link
94
+ generator in [utils.md](utils.md) (`buildGs1DigitalLink`).
package/docs/utils.md CHANGED
@@ -233,7 +233,8 @@ otherwise).
233
233
  | Input | AI | Notes |
234
234
  |-------|----|-------|
235
235
  | `gtin` / `product.gtin` | 01 | Primary identifier (required) |
236
- | `cpv` / `variant` | 22 | Consumer Product Variant (path) |
236
+ | `cpv` | 22 | Real GS1 Consumer Product Variant code (path). Wins over `variant`. |
237
+ | `variant` | 22 | Internal variant id, emitted as AI 22 for the SmartLinks resolver. Use `cpv` for a genuine GS1 CPV. |
237
238
  | `lot` / `batch` | 10 | Batch/lot (path); a batch object also supplies its expiry |
238
239
  | `serial` | 21 | Specific item — a string, or an object (`serialNumber` ?? `id`, e.g. a proof / serial / virtual id) |
239
240
  | `expiry` | 17 | Date → `YYMMDD` (query) |