@proveanything/smartlinks 1.15.24 → 1.16.1
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.
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/lots.d.ts +50 -0
- package/dist/api/lots.js +114 -0
- package/dist/docs/API_SUMMARY.md +227 -1
- package/dist/docs/appConfig.md +2 -1
- package/dist/docs/lots.md +111 -0
- package/dist/index.d.ts +1 -0
- package/dist/openapi.yaml +658 -0
- package/dist/types/lots.d.ts +99 -0
- package/dist/types/lots.js +7 -0
- package/dist/types/proof.d.ts +6 -0
- package/docs/API_SUMMARY.md +227 -1
- package/docs/appConfig.md +2 -1
- package/docs/lots.md +111 -0
- package/openapi.yaml +658 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export type LotStatus = 'open' | 'closed' | 'recalled' | 'archived';
|
|
2
|
+
/** How a lot's member products are resolved. */
|
|
3
|
+
export type LotSelector = {
|
|
4
|
+
mode: 'facets';
|
|
5
|
+
rules: Array<{
|
|
6
|
+
key: string;
|
|
7
|
+
values: string[];
|
|
8
|
+
}>;
|
|
9
|
+
} | {
|
|
10
|
+
mode: 'products';
|
|
11
|
+
productIds: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Shared lot data (single source of truth — not copied to batches). */
|
|
14
|
+
export interface LotPayload {
|
|
15
|
+
manufacturedAt?: string | null;
|
|
16
|
+
expiresAt?: string | null;
|
|
17
|
+
location?: string | null;
|
|
18
|
+
custom?: Record<string, any>;
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
}
|
|
21
|
+
export interface Lot {
|
|
22
|
+
id: string;
|
|
23
|
+
collectionId: string;
|
|
24
|
+
/** Human / GS1 AI(10) identifier, ≤20 chars, unique per collection (case-insensitive). */
|
|
25
|
+
lotNumber: string;
|
|
26
|
+
name?: string | null;
|
|
27
|
+
description?: string | null;
|
|
28
|
+
status: LotStatus;
|
|
29
|
+
selector: LotSelector;
|
|
30
|
+
/** Omitted on list summaries. */
|
|
31
|
+
payload?: LotPayload;
|
|
32
|
+
/** Optional lot-level redirect/destination; wins over the product's destination on a lot hit. */
|
|
33
|
+
destination?: Record<string, any> | null;
|
|
34
|
+
productCount: number;
|
|
35
|
+
/** Materialised member product ids. Omitted on list summaries. */
|
|
36
|
+
productIds?: string[];
|
|
37
|
+
resolvedAt?: string | null;
|
|
38
|
+
createdBy?: string | null;
|
|
39
|
+
updatedBy?: string | null;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
/** Soft-delete timestamp (recoverable). `null` for live lots. Distinct from `status:'archived'`. */
|
|
43
|
+
deletedAt?: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface LotCreateInput {
|
|
46
|
+
lotNumber: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
selector?: LotSelector;
|
|
50
|
+
payload?: LotPayload;
|
|
51
|
+
destination?: Record<string, any> | null;
|
|
52
|
+
status?: LotStatus;
|
|
53
|
+
/** Optional client-supplied id; the server generates one otherwise. */
|
|
54
|
+
id?: string;
|
|
55
|
+
}
|
|
56
|
+
export type LotUpdateInput = Partial<LotCreateInput>;
|
|
57
|
+
export interface ListLotsParams {
|
|
58
|
+
status?: LotStatus;
|
|
59
|
+
search?: string;
|
|
60
|
+
/** Reverse lookup — lots containing this product id. */
|
|
61
|
+
productId?: string;
|
|
62
|
+
/** Admin only: include soft-deleted lots (default false). */
|
|
63
|
+
includeDeleted?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface ListLotsResponse {
|
|
66
|
+
lots: Lot[];
|
|
67
|
+
}
|
|
68
|
+
/** Member changes returned by resolve/update-with-selector-change. */
|
|
69
|
+
export interface LotMemberDiff {
|
|
70
|
+
added: string[];
|
|
71
|
+
removed: string[];
|
|
72
|
+
}
|
|
73
|
+
export interface ResolveLotResponse {
|
|
74
|
+
lot: Lot;
|
|
75
|
+
diff: LotMemberDiff;
|
|
76
|
+
}
|
|
77
|
+
export interface LotProductSummary {
|
|
78
|
+
id: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
gtin?: string | null;
|
|
81
|
+
}
|
|
82
|
+
export interface ListLotProductsResponse {
|
|
83
|
+
products: LotProductSummary[];
|
|
84
|
+
total: number;
|
|
85
|
+
page: number;
|
|
86
|
+
limit: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Typed result of GS1 AI(10) resolution. The server decides batch-vs-lot based on
|
|
90
|
+
* the collection's flags (batch first — unique to one product — then lot).
|
|
91
|
+
*/
|
|
92
|
+
export interface LotResolutionResult {
|
|
93
|
+
match: 'batch' | 'lot' | 'product' | 'none';
|
|
94
|
+
productId: string;
|
|
95
|
+
batchId: string | null;
|
|
96
|
+
lotId: string | null;
|
|
97
|
+
ai10: string | null;
|
|
98
|
+
destination?: any;
|
|
99
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// src/types/lots.ts
|
|
2
|
+
//
|
|
3
|
+
// A Lot is a collection-scoped production grouping that spans one or more products
|
|
4
|
+
// (SKUs). Unlike a batch (a single production run of a single product), a lot is a
|
|
5
|
+
// first-class entity with a selector (how members are matched), a materialised
|
|
6
|
+
// product list, and a shared payload. Lots are never fanned out into batches.
|
|
7
|
+
export {};
|
package/dist/types/proof.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface Proof {
|
|
|
28
28
|
tokenId: string;
|
|
29
29
|
/** Unique identifier for the user */
|
|
30
30
|
userId: string;
|
|
31
|
+
/** Batch (single production run of this product) this item belongs to, if any. */
|
|
32
|
+
batchId?: string | null;
|
|
33
|
+
/** Variant (product definition) this item belongs to, if any. */
|
|
34
|
+
variantId?: string | null;
|
|
35
|
+
/** Lot (cross-product production grouping) this item belongs to, if any. Separate from `batchId`. */
|
|
36
|
+
lotId?: string | null;
|
|
31
37
|
/** Is this proof available to be claimed */
|
|
32
38
|
claimable?: boolean;
|
|
33
39
|
/** Is this proof virtual */
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.1 | Generated: 2026-09-01T15:57:41.258Z
|
|
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
|
|
@@ -3056,6 +3058,63 @@ interface AccountInfoResponse {
|
|
|
3056
3058
|
|
|
3057
3059
|
### authKit
|
|
3058
3060
|
|
|
3061
|
+
**AuthTelemetryEvent** (interface)
|
|
3062
|
+
```typescript
|
|
3063
|
+
interface AuthTelemetryEvent {
|
|
3064
|
+
eventId: string
|
|
3065
|
+
correlationId: string
|
|
3066
|
+
clientId: string
|
|
3067
|
+
collectionId?: string
|
|
3068
|
+
type: AuthEventType
|
|
3069
|
+
flow: AuthFlow
|
|
3070
|
+
ts: string
|
|
3071
|
+
durationMs?: number
|
|
3072
|
+
outcome?: 'success' | 'error' | 'stalled' | 'abandoned'
|
|
3073
|
+
error?: {
|
|
3074
|
+
code?: string
|
|
3075
|
+
statusCode?: number
|
|
3076
|
+
message?: string
|
|
3077
|
+
name?: string
|
|
3078
|
+
stack?: string
|
|
3079
|
+
endpoint?: string
|
|
3080
|
+
}
|
|
3081
|
+
context: {
|
|
3082
|
+
sdkVersion: string
|
|
3083
|
+
authKitVersion: string
|
|
3084
|
+
mode: 'standalone' | 'embedded' | 'proxy' | 'native'
|
|
3085
|
+
route?: string
|
|
3086
|
+
deepLinkMode?: string
|
|
3087
|
+
userAgent: string
|
|
3088
|
+
platform?: string
|
|
3089
|
+
language?: string
|
|
3090
|
+
online: boolean
|
|
3091
|
+
viewport?: { w: number; h: number }
|
|
3092
|
+
darkMode?: boolean
|
|
3093
|
+
}
|
|
3094
|
+
subject?: { uid?: string; emailHash?: string }
|
|
3095
|
+
}
|
|
3096
|
+
```
|
|
3097
|
+
|
|
3098
|
+
**TelemetryIngestResponse** (interface)
|
|
3099
|
+
```typescript
|
|
3100
|
+
interface TelemetryIngestResponse {
|
|
3101
|
+
accepted: number
|
|
3102
|
+
rejected: number
|
|
3103
|
+
rejectedIds: string[]
|
|
3104
|
+
}
|
|
3105
|
+
```
|
|
3106
|
+
|
|
3107
|
+
**AuthKitTelemetryConfig** (interface)
|
|
3108
|
+
```typescript
|
|
3109
|
+
interface AuthKitTelemetryConfig {
|
|
3110
|
+
enabled?: boolean
|
|
3111
|
+
successSampleRate?: number
|
|
3112
|
+
captureJsErrors?: boolean
|
|
3113
|
+
stallThresholdMs?: number
|
|
3114
|
+
retentionDays?: number
|
|
3115
|
+
}
|
|
3116
|
+
```
|
|
3117
|
+
|
|
3059
3118
|
**AuthKitUser** (interface)
|
|
3060
3119
|
```typescript
|
|
3061
3120
|
interface AuthKitUser {
|
|
@@ -3519,6 +3578,10 @@ interface AuthKitLockoutPolicy {
|
|
|
3519
3578
|
}
|
|
3520
3579
|
```
|
|
3521
3580
|
|
|
3581
|
+
**AuthEventType** = ``
|
|
3582
|
+
|
|
3583
|
+
**AuthFlow** = ``
|
|
3584
|
+
|
|
3522
3585
|
**RefreshErrorCode** = ``
|
|
3523
3586
|
|
|
3524
3587
|
**AuthKitErrorCode** = ``
|
|
@@ -6355,6 +6418,122 @@ interface LocationSearchResponse {
|
|
|
6355
6418
|
|
|
6356
6419
|
**LocationPayload** = `Omit<`
|
|
6357
6420
|
|
|
6421
|
+
### lots
|
|
6422
|
+
|
|
6423
|
+
**LotPayload** (interface)
|
|
6424
|
+
```typescript
|
|
6425
|
+
interface LotPayload {
|
|
6426
|
+
manufacturedAt?: string | null
|
|
6427
|
+
expiresAt?: string | null
|
|
6428
|
+
location?: string | null
|
|
6429
|
+
custom?: Record<string, any>
|
|
6430
|
+
[key: string]: any
|
|
6431
|
+
}
|
|
6432
|
+
```
|
|
6433
|
+
|
|
6434
|
+
**Lot** (interface)
|
|
6435
|
+
```typescript
|
|
6436
|
+
interface Lot {
|
|
6437
|
+
id: string
|
|
6438
|
+
collectionId: string
|
|
6439
|
+
lotNumber: string
|
|
6440
|
+
name?: string | null
|
|
6441
|
+
description?: string | null
|
|
6442
|
+
status: LotStatus
|
|
6443
|
+
selector: LotSelector
|
|
6444
|
+
payload?: LotPayload
|
|
6445
|
+
destination?: Record<string, any> | null
|
|
6446
|
+
productCount: number
|
|
6447
|
+
productIds?: string[]
|
|
6448
|
+
resolvedAt?: string | null
|
|
6449
|
+
createdBy?: string | null
|
|
6450
|
+
updatedBy?: string | null
|
|
6451
|
+
createdAt: string
|
|
6452
|
+
updatedAt: string
|
|
6453
|
+
deletedAt?: string | null
|
|
6454
|
+
}
|
|
6455
|
+
```
|
|
6456
|
+
|
|
6457
|
+
**LotCreateInput** (interface)
|
|
6458
|
+
```typescript
|
|
6459
|
+
interface LotCreateInput {
|
|
6460
|
+
lotNumber: string
|
|
6461
|
+
name?: string
|
|
6462
|
+
description?: string
|
|
6463
|
+
selector?: LotSelector
|
|
6464
|
+
payload?: LotPayload
|
|
6465
|
+
destination?: Record<string, any> | null
|
|
6466
|
+
status?: LotStatus
|
|
6467
|
+
id?: string
|
|
6468
|
+
}
|
|
6469
|
+
```
|
|
6470
|
+
|
|
6471
|
+
**ListLotsParams** (interface)
|
|
6472
|
+
```typescript
|
|
6473
|
+
interface ListLotsParams {
|
|
6474
|
+
status?: LotStatus
|
|
6475
|
+
search?: string
|
|
6476
|
+
productId?: string
|
|
6477
|
+
includeDeleted?: boolean
|
|
6478
|
+
}
|
|
6479
|
+
```
|
|
6480
|
+
|
|
6481
|
+
**ListLotsResponse** (interface)
|
|
6482
|
+
```typescript
|
|
6483
|
+
interface ListLotsResponse {
|
|
6484
|
+
lots: Lot[]
|
|
6485
|
+
}
|
|
6486
|
+
```
|
|
6487
|
+
|
|
6488
|
+
**LotMemberDiff** (interface)
|
|
6489
|
+
```typescript
|
|
6490
|
+
interface LotMemberDiff {
|
|
6491
|
+
added: string[]; removed: string[]
|
|
6492
|
+
}
|
|
6493
|
+
```
|
|
6494
|
+
|
|
6495
|
+
**ResolveLotResponse** (interface)
|
|
6496
|
+
```typescript
|
|
6497
|
+
interface ResolveLotResponse {
|
|
6498
|
+
lot: Lot; diff: LotMemberDiff
|
|
6499
|
+
}
|
|
6500
|
+
```
|
|
6501
|
+
|
|
6502
|
+
**LotProductSummary** (interface)
|
|
6503
|
+
```typescript
|
|
6504
|
+
interface LotProductSummary {
|
|
6505
|
+
id: string; name?: string; gtin?: string | null
|
|
6506
|
+
}
|
|
6507
|
+
```
|
|
6508
|
+
|
|
6509
|
+
**ListLotProductsResponse** (interface)
|
|
6510
|
+
```typescript
|
|
6511
|
+
interface ListLotProductsResponse {
|
|
6512
|
+
products: LotProductSummary[]
|
|
6513
|
+
total: number
|
|
6514
|
+
page: number
|
|
6515
|
+
limit: number
|
|
6516
|
+
}
|
|
6517
|
+
```
|
|
6518
|
+
|
|
6519
|
+
**LotResolutionResult** (interface)
|
|
6520
|
+
```typescript
|
|
6521
|
+
interface LotResolutionResult {
|
|
6522
|
+
match: 'batch' | 'lot' | 'product' | 'none'
|
|
6523
|
+
productId: string
|
|
6524
|
+
batchId: string | null
|
|
6525
|
+
lotId: string | null
|
|
6526
|
+
ai10: string | null
|
|
6527
|
+
destination?: any
|
|
6528
|
+
}
|
|
6529
|
+
```
|
|
6530
|
+
|
|
6531
|
+
**LotStatus** = `'open' | 'closed' | 'recalled' | 'archived'`
|
|
6532
|
+
|
|
6533
|
+
**LotSelector** = ``
|
|
6534
|
+
|
|
6535
|
+
**LotUpdateInput** = `Partial<LotCreateInput>`
|
|
6536
|
+
|
|
6358
6537
|
### loyalty
|
|
6359
6538
|
|
|
6360
6539
|
**LoyaltyScheme** (interface)
|
|
@@ -7329,6 +7508,9 @@ interface Proof {
|
|
|
7329
7508
|
productId: string
|
|
7330
7509
|
tokenId: string
|
|
7331
7510
|
userId: string
|
|
7511
|
+
batchId?: string | null
|
|
7512
|
+
variantId?: string | null
|
|
7513
|
+
lotId?: string | null
|
|
7332
7514
|
claimable?: boolean
|
|
7333
7515
|
virtual?: boolean
|
|
7334
7516
|
data?: Record<string, JsonValue>
|
|
@@ -9771,6 +9953,50 @@ Public: Fetch a global location by ID GET /public/location/:locationId
|
|
|
9771
9953
|
locationId: string) → `Promise<Location>`
|
|
9772
9954
|
Public: Fetch a location for a collection; returns either a collection-owned or global fallback GET /public/collection/:collectionId/location/:locationId
|
|
9773
9955
|
|
|
9956
|
+
### lots
|
|
9957
|
+
|
|
9958
|
+
**create**(collectionId: string, lot: LotCreateInput) → `Promise<Lot>`
|
|
9959
|
+
Create a lot (resolves its selector into members).
|
|
9960
|
+
|
|
9961
|
+
**list**(collectionId: string, params: ListLotsParams = {}) → `Promise<Lot[]>`
|
|
9962
|
+
List lots (summary rows; `payload`/`productIds` omitted). Filter by status, search, or containing productId.
|
|
9963
|
+
|
|
9964
|
+
**get**(collectionId: string, lotId: string, opts: { includeDeleted?: boolean } = {}) → `Promise<Lot>`
|
|
9965
|
+
Get the full lot record. Pass `{ includeDeleted: true }` to fetch a soft-deleted one.
|
|
9966
|
+
|
|
9967
|
+
**getByNumber**(collectionId: string, lotNumber: string, opts: { includeDeleted?: boolean } = {}) → `Promise<Lot>`
|
|
9968
|
+
Look up a lot by its number (case-insensitive) — used by scan/resolver flows.
|
|
9969
|
+
|
|
9970
|
+
**update**(collectionId: string, lotId: string, lot: LotUpdateInput) → `Promise<Lot>`
|
|
9971
|
+
Update a lot. Re-resolves members if the selector changed (response then carries `diff`).
|
|
9972
|
+
|
|
9973
|
+
**remove**(collectionId: string, lotId: string) → `Promise<`
|
|
9974
|
+
Soft-delete a lot — recoverable, and frees its `lotNumber` for reuse. Distinct from {@link archive}. Hidden from reads unless `{ includeDeleted: true }`; undo with {@link restore}.
|
|
9975
|
+
|
|
9976
|
+
**archive**(collectionId: string, lotId: string) → `Promise<`
|
|
9977
|
+
Archive a lot — a live lifecycle state (stays visible, keeps its number). Not a delete.
|
|
9978
|
+
|
|
9979
|
+
**restore**(collectionId: string, lotId: string) → `Promise<Lot>`
|
|
9980
|
+
Restore a soft-deleted lot. Rejects (409) if a live lot now uses the same number.
|
|
9981
|
+
|
|
9982
|
+
**resolve**(collectionId: string, lotId: string) → `Promise<ResolveLotResponse>`
|
|
9983
|
+
Re-resolve members from the current selector; returns the lot + a member diff.
|
|
9984
|
+
|
|
9985
|
+
**listProducts**(collectionId: string, lotId: string, opts: { page?: number; limit?: number } = {}) → `Promise<ListLotProductsResponse>`
|
|
9986
|
+
Paginated member product summaries.
|
|
9987
|
+
|
|
9988
|
+
**publicList**(collectionId: string, params: ListLotsParams = {}) → `Promise<Lot[]>`
|
|
9989
|
+
Paginated member product summaries.
|
|
9990
|
+
|
|
9991
|
+
**publicGet**(collectionId: string, lotId: string) → `Promise<Lot>`
|
|
9992
|
+
Paginated member product summaries.
|
|
9993
|
+
|
|
9994
|
+
**publicGetByNumber**(collectionId: string, lotNumber: string) → `Promise<Lot>`
|
|
9995
|
+
Paginated member product summaries.
|
|
9996
|
+
|
|
9997
|
+
**publicListProducts**(collectionId: string, lotId: string, opts: { page?: number; limit?: number } = {}) → `Promise<ListLotProductsResponse>`
|
|
9998
|
+
Paginated member product summaries.
|
|
9999
|
+
|
|
9774
10000
|
### loyalty
|
|
9775
10001
|
|
|
9776
10002
|
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,111 @@
|
|
|
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`. A live lifecycle state, distinct from deletion.
|
|
22
|
+
- **`destination`** — optional lot-level redirect; wins over the product's on a lot-scoped scan.
|
|
23
|
+
|
|
24
|
+
### Archive vs delete
|
|
25
|
+
|
|
26
|
+
Two separate ideas — mirroring the platform's `deletedAt` convention:
|
|
27
|
+
|
|
28
|
+
| | `archive` (`status: 'archived'`) | `remove` (soft-delete, `deletedAt`) |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Record | stays **live** & visible | **hidden** from reads unless `includeDeleted: true` |
|
|
31
|
+
| `lotNumber` | **stays reserved** | **freed** for reuse by a new lot |
|
|
32
|
+
| AI(10) scan | not resolved (excluded) | not resolved |
|
|
33
|
+
| Reversible | change status back | `restore` (409 if the number was taken by a live lot) |
|
|
34
|
+
|
|
35
|
+
Nothing is ever hard-deleted (joins/history stay intact). Use **archive** for "this run is done, keep it around"; use **remove** for "this was a mistake, and I want the number back."
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## SDK — `SL.lots.*`
|
|
40
|
+
|
|
41
|
+
Writes and admin reads hit `/admin/collection/:cid/lots`; the `public*` reads hit
|
|
42
|
+
`/public/collection/:cid/lots` for cross-app consumers (auth is the ambient bearer token —
|
|
43
|
+
there's no `admin` flag).
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { lots } from '@proveanything/smartlinks'
|
|
47
|
+
|
|
48
|
+
// Create — facet-targeted lot
|
|
49
|
+
const lot = await lots.create(collectionId, {
|
|
50
|
+
lotNumber: 'LOT-2026-09',
|
|
51
|
+
name: 'September Oak run',
|
|
52
|
+
selector: { mode: 'facets', rules: [
|
|
53
|
+
{ key: 'supplier', values: ['Acme Timber'] },
|
|
54
|
+
{ key: 'range', values: ['Oslo', 'Bergen'] },
|
|
55
|
+
]},
|
|
56
|
+
payload: { manufacturedAt: '2026-09-01', custom: { supplierBatchRef: 'ACM-7741' } },
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const list = await lots.list(collectionId, { status: 'open' })
|
|
60
|
+
const byId = await lots.get(collectionId, lot.id)
|
|
61
|
+
const byNumber = await lots.getByNumber(collectionId, 'LOT-2026-09') // case-insensitive
|
|
62
|
+
const containing = await lots.list(collectionId, { productId: 'prd_abc' }) // reverse lookup
|
|
63
|
+
const updated = await lots.update(collectionId, lot.id, { status: 'closed' })
|
|
64
|
+
const { diff } = await lots.resolve(collectionId, lot.id) // { added, removed }
|
|
65
|
+
const members = await lots.listProducts(collectionId, lot.id, { page: 1, limit: 50 })
|
|
66
|
+
|
|
67
|
+
await lots.archive(collectionId, lot.id) // live, keeps its number
|
|
68
|
+
await lots.remove(collectionId, lot.id) // soft-delete, frees the number
|
|
69
|
+
const withDeleted = await lots.list(collectionId, { includeDeleted: true })
|
|
70
|
+
const restored = await lots.restore(collectionId, lot.id) // undo a soft-delete
|
|
71
|
+
|
|
72
|
+
// Cross-app reads
|
|
73
|
+
const publicLots = await lots.publicList(collectionId)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Exported types: `Lot`, `LotStatus`, `LotSelector`, `LotPayload`, `LotCreateInput`,
|
|
77
|
+
`LotUpdateInput`, `ListLotsParams`, `ResolveLotResponse`, `ListLotProductsResponse`,
|
|
78
|
+
`LotResolutionResult`.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## GS1 Digital Link resolution — AI(10)
|
|
83
|
+
|
|
84
|
+
GS1 gives batch and lot a **single** slot: AI(10) ("Batch or Lot Number"), in a Digital Link
|
|
85
|
+
as `/01/{gtin}/10/{value}`. A physical code carries exactly one value there, so the server
|
|
86
|
+
decides which namespace it belongs to, driven by two **feature flags** — `batches` and `lots`
|
|
87
|
+
— in the platform feature-flag system (`appConfig.system.features`), resolved the standard way
|
|
88
|
+
(`appConfiguration.isFeatureEnabled(collectionId, 'lots')`):
|
|
89
|
+
|
|
90
|
+
- Explicit `true`/`false` in `system.features` wins; otherwise **enterprise** accounts default
|
|
91
|
+
a flag on and everyone else defaults off. So AI(10) batch/lot resolution is opt-in — no
|
|
92
|
+
existing scan changes until `batches`/`lots` is enabled for the collection.
|
|
93
|
+
|
|
94
|
+
Resolution order — **batch first, then lot** (a hierarchy, not a collision):
|
|
95
|
+
|
|
96
|
+
1. **Batch** is the narrowest scope (one product). If the scanned product has a batch whose id
|
|
97
|
+
or name matches the AI(10) value, it wins — a batch is a **specific-SKU override**.
|
|
98
|
+
2. **Lot** is broad (many products). If no batch matches and the value is a lot number *and the
|
|
99
|
+
scanned product is a member*, the lot resolves.
|
|
100
|
+
3. Otherwise it resolves at the product level.
|
|
101
|
+
|
|
102
|
+
This means a range can share one lot code, and a single SKU can be given richer/overriding
|
|
103
|
+
detail by creating a batch with the **same** identifier — the batch simply takes precedence for
|
|
104
|
+
that SKU. A recalled lot (`status: 'recalled'`) still resolves so the destination page can show
|
|
105
|
+
a recall notice (a `&recall=1` context param is added).
|
|
106
|
+
|
|
107
|
+
The server returns a typed shape ({@link LotResolutionResult}); the front end renders it — clients
|
|
108
|
+
should not implement their own fallback.
|
|
109
|
+
|
|
110
|
+
See also [proof-product-data-scoping.md](proof-product-data-scoping.md) and the GS1 link
|
|
111
|
+
generator in [utils.md](utils.md) (`buildGs1DigitalLink`).
|