@aglyn/plugins-commerce 1.0.0-beta.199 → 1.0.0-beta.201
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/package.json +13 -13
- package/src/lib/components/console/catalog-organization-card.component.js +48 -3
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.d.ts +9 -0
- package/src/lib/components/console/gift-cards-card.component.js +53 -28
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +26 -0
- package/src/lib/components/console/host-orders-card.component.js +264 -194
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +12 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-page.component.js +86 -45
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +30 -6
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-slugs.d.ts +43 -0
- package/src/lib/components/console/product-slugs.js +60 -0
- package/src/lib/components/console/product-slugs.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.d.ts +2 -2
- package/src/lib/components/console/products-hub-card.component.js +229 -282
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +10 -5
- package/src/lib/components/console/products-hub-zone.component.js +32 -9
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/recovery-queue-card.component.d.ts +37 -5
- package/src/lib/components/console/recovery-queue-card.component.js +107 -86
- package/src/lib/components/console/recovery-queue-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +15 -3
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/console/smart-collections.d.ts +32 -0
- package/src/lib/components/console/smart-collections.js +57 -0
- package/src/lib/components/console/smart-collections.js.map +1 -0
- package/src/lib/components/console/stock-movements-card.component.d.ts +13 -4
- package/src/lib/components/console/stock-movements-card.component.js +142 -180
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +5 -0
- package/src/lib/components/product-grid.js +95 -34
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/constants/orders-list-query.d.ts +40 -0
- package/src/lib/constants/orders-list-query.js +205 -0
- package/src/lib/constants/orders-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +42 -0
- package/src/lib/constants/product-list-query.js +217 -0
- package/src/lib/constants/product-list-query.js.map +1 -0
- package/src/lib/constants/stock-movement-query.d.ts +27 -0
- package/src/lib/constants/stock-movement-query.js +110 -0
- package/src/lib/constants/stock-movement-query.js.map +1 -0
- package/src/lib/constants/storefront-catalog-query.d.ts +103 -0
- package/src/lib/constants/storefront-catalog-query.js +312 -0
- package/src/lib/constants/storefront-catalog-query.js.map +1 -0
- package/src/lib/model/checkout-recovery.d.ts +37 -0
- package/src/lib/model/checkout-recovery.js +44 -0
- package/src/lib/model/checkout-recovery.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +7 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +80 -0
- package/src/lib/model/commerce.js +67 -6
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/gift-card-search.d.ts +24 -0
- package/src/lib/model/gift-card-search.js +48 -0
- package/src/lib/model/gift-card-search.js.map +1 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/order-list-fields.d.ts +80 -0
- package/src/lib/model/order-list-fields.js +112 -0
- package/src/lib/model/order-list-fields.js.map +1 -0
- package/src/lib/server/billing-webhook.js +180 -99
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cancel-order.js +6 -6
- package/src/lib/server/cancel-order.js.map +1 -1
- package/src/lib/server/cart-checkout.js +9 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/catalog.d.ts +46 -30
- package/src/lib/server/catalog.js +507 -170
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout.js +3 -1
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/collection-membership.d.ts +35 -0
- package/src/lib/server/collection-membership.js +172 -0
- package/src/lib/server/collection-membership.js.map +1 -0
- package/src/lib/server/draft-order.js +5 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/gift-cards.js +7 -1
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/member-name-search.d.ts +17 -0
- package/src/lib/server/member-name-search.js +23 -1
- package/src/lib/server/member-name-search.js.map +1 -1
- package/src/lib/server/membership-account.js +7 -3
- package/src/lib/server/membership-account.js.map +1 -1
- package/src/lib/server/membership-admin-password.js +8 -2
- package/src/lib/server/membership-admin-password.js.map +1 -1
- package/src/lib/server/membership-recover.js +4 -1
- package/src/lib/server/membership-recover.js.map +1 -1
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/pos-order.js +9 -5
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/process-abandoned.js +20 -7
- package/src/lib/server/process-abandoned.js.map +1 -1
- package/src/lib/server/process-restock.js +6 -2
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/reserve-stock.js +3 -4
- package/src/lib/server/reserve-stock.js.map +1 -1
- package/src/lib/server/reserve.js +3 -1
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/site-page-enricher.js +3 -3
- package/src/lib/server/site-page-enricher.js.map +1 -1
- package/src/lib/server.js +4 -0
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { ORDER_CHANNEL_LABELS, ORDER_STATUS_LABELS } from "../model/commerce-orders.js";
|
|
17
|
+
/*
|
|
18
|
+
* THE ORDERS LIST, ON ITS QUERY (AGL-3321).
|
|
19
|
+
*
|
|
20
|
+
* `hosts/{hostId}/orders`, newest first. Every filter the grid's panel offers
|
|
21
|
+
* and the quick search's word is a predicate on the ONE Firestore query that
|
|
22
|
+
* pages the list (`planListQuery` through `useListQuery`), over the fields
|
|
23
|
+
* every writer stamps with `orderListFields` — so page two of a filtered list
|
|
24
|
+
* is page two of the matches, not page two of the store narrowed afterwards.
|
|
25
|
+
*
|
|
26
|
+
* One order: `createdAtMs` descending, the field every writer stamps. Each
|
|
27
|
+
* filterable field is then ONE `(field, createdAtMs DESC)` composite, and
|
|
28
|
+
* index merging serves every combination of them; the date range is on that
|
|
29
|
+
* same field, so it adds none. Eight composites in all, pinned by
|
|
30
|
+
* `orders-list-query.spec.ts` against `cloud/firebase-firestore.indexes.json`.
|
|
31
|
+
*
|
|
32
|
+
* What one query cannot hold is refused by name above the grid
|
|
33
|
+
* (`ListQueryNotices`, labelled by `listQueryRefusals` from these fields,
|
|
34
|
+
* headers and options), never matched over the rows it returned: the search,
|
|
35
|
+
* Customer `contains`, Order `contains` and Product each take the query's one
|
|
36
|
+
* array clause, so two of them together keep the first and refuse the second.
|
|
37
|
+
*
|
|
38
|
+
* A second range (a total, say) would be a new `sorts` entry and one more
|
|
39
|
+
* composite per filterable field beneath it — eight — plus the range field's
|
|
40
|
+
* own order; nothing else here moves.
|
|
41
|
+
*/ /** The grid columns, each as the query reads it. */ export const ORDER_LIST_FIELDS = [
|
|
42
|
+
{
|
|
43
|
+
// The number as typed, `#1042` or `1042`, by word prefix.
|
|
44
|
+
column: 'orderLabel',
|
|
45
|
+
kind: 'text',
|
|
46
|
+
path: 'number',
|
|
47
|
+
tokensPath: 'orderLabelTokens',
|
|
48
|
+
operators: [
|
|
49
|
+
'contains'
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// `is` a whole address; `contains` an address's piece or its domain.
|
|
54
|
+
column: 'customerEmail',
|
|
55
|
+
kind: 'text',
|
|
56
|
+
path: 'customerEmail',
|
|
57
|
+
lowerPath: 'customerEmailLower',
|
|
58
|
+
tokensPath: 'customerEmailTokens',
|
|
59
|
+
operators: [
|
|
60
|
+
'contains',
|
|
61
|
+
'equals'
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
column: 'channelKey',
|
|
66
|
+
kind: 'exact',
|
|
67
|
+
path: 'channel',
|
|
68
|
+
presence: 'always',
|
|
69
|
+
operators: [
|
|
70
|
+
'equals',
|
|
71
|
+
'isAnyOf'
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
column: 'statusKey',
|
|
76
|
+
kind: 'exact',
|
|
77
|
+
path: 'status',
|
|
78
|
+
presence: 'always',
|
|
79
|
+
operators: [
|
|
80
|
+
'equals',
|
|
81
|
+
'isAnyOf'
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
column: 'createdAtMs',
|
|
86
|
+
kind: 'date',
|
|
87
|
+
path: 'createdAtMs',
|
|
88
|
+
storedAs: 'millis',
|
|
89
|
+
presence: 'always',
|
|
90
|
+
operators: [
|
|
91
|
+
'is',
|
|
92
|
+
'after',
|
|
93
|
+
'onOrAfter',
|
|
94
|
+
'before',
|
|
95
|
+
'onOrBefore'
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// Every product in the order, matched whole — ids keep their case.
|
|
100
|
+
column: 'productIds',
|
|
101
|
+
kind: 'exact',
|
|
102
|
+
path: 'productIds',
|
|
103
|
+
tokensPath: 'productIds',
|
|
104
|
+
verbatimTokens: true,
|
|
105
|
+
operators: [
|
|
106
|
+
'isAnyOf'
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
column: 'disputeKey',
|
|
111
|
+
kind: 'exact',
|
|
112
|
+
path: 'disputeKey',
|
|
113
|
+
operators: [
|
|
114
|
+
'equals',
|
|
115
|
+
'isAnyOf'
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
/** The list's query: its fields, its one order and its search. */ export const ORDER_LIST_QUERY = {
|
|
120
|
+
fields: ORDER_LIST_FIELDS,
|
|
121
|
+
sorts: [
|
|
122
|
+
{
|
|
123
|
+
path: 'createdAtMs',
|
|
124
|
+
direction: 'desc',
|
|
125
|
+
column: 'createdAtMs'
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
search: {
|
|
129
|
+
tokensPath: 'searchTokens'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export const ORDER_LIST_HEADERS = {
|
|
133
|
+
orderLabel: 'Order',
|
|
134
|
+
customerEmail: 'Customer',
|
|
135
|
+
channelKey: 'Channel',
|
|
136
|
+
statusKey: 'Status',
|
|
137
|
+
createdAtMs: 'Date',
|
|
138
|
+
productIds: 'Product',
|
|
139
|
+
disputeKey: 'Disputes'
|
|
140
|
+
};
|
|
141
|
+
/** The fields picked from choices, which the panel shows as a select. */ export const ORDER_LIST_SELECT_FIELDS = [
|
|
142
|
+
'channelKey',
|
|
143
|
+
'statusKey',
|
|
144
|
+
'productIds',
|
|
145
|
+
'disputeKey'
|
|
146
|
+
];
|
|
147
|
+
export const ORDER_STATUS_OPTIONS = Object.keys(ORDER_STATUS_LABELS).map((status)=>({
|
|
148
|
+
value: status,
|
|
149
|
+
label: ORDER_STATUS_LABELS[status]
|
|
150
|
+
}));
|
|
151
|
+
export const ORDER_CHANNEL_OPTIONS = Object.keys(ORDER_CHANNEL_LABELS).map((channel)=>({
|
|
152
|
+
value: channel,
|
|
153
|
+
label: ORDER_CHANNEL_LABELS[channel]
|
|
154
|
+
}));
|
|
155
|
+
/*
|
|
156
|
+
* The dispute badge's tones (`orderDisputeKey`), labelled as the badge
|
|
157
|
+
* labels them. `lost` is the tone, not the raw dispute status: a dispute
|
|
158
|
+
* that was WON also closes with money untouched, and listing it as charged
|
|
159
|
+
* back would tell a merchant they lost a case they won.
|
|
160
|
+
*/ export const ORDER_DISPUTE_OPTIONS = [
|
|
161
|
+
{
|
|
162
|
+
value: 'open',
|
|
163
|
+
label: 'Open dispute'
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
value: 'lost',
|
|
167
|
+
label: 'Charged back'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
value: 'won',
|
|
171
|
+
label: 'Dispute won'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
value: 'settled',
|
|
175
|
+
label: 'Dispute closed'
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
/** The clause the open-dispute banner's "Show them" sets. */ export const OPEN_DISPUTE_CLAUSE = {
|
|
179
|
+
field: 'disputeKey',
|
|
180
|
+
op: 'equals',
|
|
181
|
+
value: 'open'
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* The Customer clause the CRM's link seeds from `?email=` (AGL-2622).
|
|
185
|
+
*
|
|
186
|
+
* A whole address is `is`, served by the lower-cased key, so it finds that
|
|
187
|
+
* buyer and no one whose address merely begins the same way. Anything else —
|
|
188
|
+
* a domain, `@acme.com`, a name — is `contains`, a word prefix of the
|
|
189
|
+
* address, so a domain finds every buyer at a company.
|
|
190
|
+
*/ export function ordersCustomerClause(raw) {
|
|
191
|
+
const value = raw.trim().replace(/^@+/, '');
|
|
192
|
+
if (!value) return null;
|
|
193
|
+
const at = value.indexOf('@');
|
|
194
|
+
return at > 0 && at < value.length - 1 ? {
|
|
195
|
+
field: 'customerEmail',
|
|
196
|
+
op: 'equals',
|
|
197
|
+
value
|
|
198
|
+
} : {
|
|
199
|
+
field: 'customerEmail',
|
|
200
|
+
op: 'contains',
|
|
201
|
+
value
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=orders-list-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/orders-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListFilterClause,\n ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n ORDER_CHANNEL_LABELS,\n ORDER_STATUS_LABELS,\n type OrderChannel,\n type OrderStatus,\n} from '../model/commerce-orders'\n\n/*\n * THE ORDERS LIST, ON ITS QUERY (AGL-3321).\n *\n * `hosts/{hostId}/orders`, newest first. Every filter the grid's panel offers\n * and the quick search's word is a predicate on the ONE Firestore query that\n * pages the list (`planListQuery` through `useListQuery`), over the fields\n * every writer stamps with `orderListFields` — so page two of a filtered list\n * is page two of the matches, not page two of the store narrowed afterwards.\n *\n * One order: `createdAtMs` descending, the field every writer stamps. Each\n * filterable field is then ONE `(field, createdAtMs DESC)` composite, and\n * index merging serves every combination of them; the date range is on that\n * same field, so it adds none. Eight composites in all, pinned by\n * `orders-list-query.spec.ts` against `cloud/firebase-firestore.indexes.json`.\n *\n * What one query cannot hold is refused by name above the grid\n * (`ListQueryNotices`, labelled by `listQueryRefusals` from these fields,\n * headers and options), never matched over the rows it returned: the search,\n * Customer `contains`, Order `contains` and Product each take the query's one\n * array clause, so two of them together keep the first and refuse the second.\n *\n * A second range (a total, say) would be a new `sorts` entry and one more\n * composite per filterable field beneath it — eight — plus the range field's\n * own order; nothing else here moves.\n */\n\n/** The grid columns, each as the query reads it. */\nexport const ORDER_LIST_FIELDS: readonly ListFilterField[] = [\n {\n // The number as typed, `#1042` or `1042`, by word prefix.\n column: 'orderLabel',\n kind: 'text',\n path: 'number',\n tokensPath: 'orderLabelTokens',\n operators: ['contains'],\n },\n {\n // `is` a whole address; `contains` an address's piece or its domain.\n column: 'customerEmail',\n kind: 'text',\n path: 'customerEmail',\n lowerPath: 'customerEmailLower',\n tokensPath: 'customerEmailTokens',\n operators: ['contains', 'equals'],\n },\n {\n column: 'channelKey',\n kind: 'exact',\n path: 'channel',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'statusKey',\n kind: 'exact',\n path: 'status',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'createdAtMs',\n kind: 'date',\n path: 'createdAtMs',\n storedAs: 'millis',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n // Every product in the order, matched whole — ids keep their case.\n column: 'productIds',\n kind: 'exact',\n path: 'productIds',\n tokensPath: 'productIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'disputeKey',\n kind: 'exact',\n path: 'disputeKey',\n operators: ['equals', 'isAnyOf'],\n },\n]\n\n/** The list's query: its fields, its one order and its search. */\nexport const ORDER_LIST_QUERY: ListQueryDeclaration = {\n fields: ORDER_LIST_FIELDS,\n sorts: [{ path: 'createdAtMs', direction: 'desc', column: 'createdAtMs' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const ORDER_LIST_HEADERS: Readonly<Record<string, string>> = {\n orderLabel: 'Order',\n customerEmail: 'Customer',\n channelKey: 'Channel',\n statusKey: 'Status',\n createdAtMs: 'Date',\n productIds: 'Product',\n disputeKey: 'Disputes',\n}\n\n/** The fields picked from choices, which the panel shows as a select. */\nexport const ORDER_LIST_SELECT_FIELDS: readonly string[] = [\n 'channelKey',\n 'statusKey',\n 'productIds',\n 'disputeKey',\n]\n\nexport const ORDER_STATUS_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_STATUS_LABELS) as OrderStatus[]\n).map((status) => ({ value: status, label: ORDER_STATUS_LABELS[status] }))\n\nexport const ORDER_CHANNEL_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_CHANNEL_LABELS) as OrderChannel[]\n).map((channel) => ({ value: channel, label: ORDER_CHANNEL_LABELS[channel] }))\n\n/*\n * The dispute badge's tones (`orderDisputeKey`), labelled as the badge\n * labels them. `lost` is the tone, not the raw dispute status: a dispute\n * that was WON also closes with money untouched, and listing it as charged\n * back would tell a merchant they lost a case they won.\n */\nexport const ORDER_DISPUTE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'open', label: 'Open dispute' },\n { value: 'lost', label: 'Charged back' },\n { value: 'won', label: 'Dispute won' },\n { value: 'settled', label: 'Dispute closed' },\n]\n\n/** The clause the open-dispute banner's \"Show them\" sets. */\nexport const OPEN_DISPUTE_CLAUSE: ListFilterClause = {\n field: 'disputeKey',\n op: 'equals',\n value: 'open',\n}\n\n/**\n * The Customer clause the CRM's link seeds from `?email=` (AGL-2622).\n *\n * A whole address is `is`, served by the lower-cased key, so it finds that\n * buyer and no one whose address merely begins the same way. Anything else —\n * a domain, `@acme.com`, a name — is `contains`, a word prefix of the\n * address, so a domain finds every buyer at a company.\n */\nexport function ordersCustomerClause(raw: string): ListFilterClause | null {\n const value = raw.trim().replace(/^@+/, '')\n if (!value) return null\n const at = value.indexOf('@')\n return at > 0 && at < value.length - 1\n ? { field: 'customerEmail', op: 'equals', value }\n : { field: 'customerEmail', op: 'contains', value }\n}\n"],"names":["ORDER_CHANNEL_LABELS","ORDER_STATUS_LABELS","ORDER_LIST_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","verbatimTokens","ORDER_LIST_QUERY","fields","sorts","direction","search","ORDER_LIST_HEADERS","orderLabel","customerEmail","channelKey","statusKey","createdAtMs","productIds","disputeKey","ORDER_LIST_SELECT_FIELDS","ORDER_STATUS_OPTIONS","Object","keys","map","status","value","label","ORDER_CHANNEL_OPTIONS","channel","ORDER_DISPUTE_OPTIONS","OPEN_DISPUTE_CLAUSE","field","op","ordersCustomerClause","raw","trim","replace","at","indexOf","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAQD,SACEA,oBAAoB,EACpBC,mBAAmB,QAGd,8BAA0B;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,kDAAkD,GAClD,OAAO,MAAMC,oBAAgD;IAC3D;QACE,0DAA0D;QAC1DC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACE,qEAAqE;QACrEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNG,WAAW;QACXF,YAAY;QACZC,WAAW;YAAC;YAAY;SAAS;IACnC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNK,UAAU;QACVD,UAAU;QACVF,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;IACA;QACE,mEAAmE;QACnEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZK,gBAAgB;QAChBJ,WAAW;YAAC;SAAU;IACxB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;CACD,CAAA;AAED,gEAAgE,GAChE,OAAO,MAAMK,mBAAyC;IACpDC,QAAQX;IACRY,OAAO;QAAC;YAAET,MAAM;YAAeU,WAAW;YAAQZ,QAAQ;QAAc;KAAE;IAC1Ea,QAAQ;QAAEV,YAAY;IAAe;AACvC,EAAC;AAED,OAAO,MAAMW,qBAAuD;IAClEC,YAAY;IACZC,eAAe;IACfC,YAAY;IACZC,WAAW;IACXC,aAAa;IACbC,YAAY;IACZC,YAAY;AACd,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMC,2BAA8C;IACzD;IACA;IACA;IACA;CACD,CAAA;AAED,OAAO,MAAMC,uBAAoD,AAC/DC,OAAOC,IAAI,CAAC3B,qBACZ4B,GAAG,CAAC,CAACC,SAAY,CAAA;QAAEC,OAAOD;QAAQE,OAAO/B,mBAAmB,CAAC6B,OAAO;IAAC,CAAA,GAAG;AAE1E,OAAO,MAAMG,wBAAqD,AAChEN,OAAOC,IAAI,CAAC5B,sBACZ6B,GAAG,CAAC,CAACK,UAAa,CAAA;QAAEH,OAAOG;QAASF,OAAOhC,oBAAoB,CAACkC,QAAQ;IAAC,CAAA,GAAG;AAE9E;;;;;CAKC,GACD,OAAO,MAAMC,wBAAqD;IAChE;QAAEJ,OAAO;QAAQC,OAAO;IAAe;IACvC;QAAED,OAAO;QAAQC,OAAO;IAAe;IACvC;QAAED,OAAO;QAAOC,OAAO;IAAc;IACrC;QAAED,OAAO;QAAWC,OAAO;IAAiB;CAC7C,CAAA;AAED,2DAA2D,GAC3D,OAAO,MAAMI,sBAAwC;IACnDC,OAAO;IACPC,IAAI;IACJP,OAAO;AACT,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,SAASQ,qBAAqBC,GAAW;IAC9C,MAAMT,QAAQS,IAAIC,IAAI,GAAGC,OAAO,CAAC,OAAO;IACxC,IAAI,CAACX,OAAO,OAAO;IACnB,MAAMY,KAAKZ,MAAMa,OAAO,CAAC;IACzB,OAAOD,KAAK,KAAKA,KAAKZ,MAAMc,MAAM,GAAG,IACjC;QAAER,OAAO;QAAiBC,IAAI;QAAUP;IAAM,IAC9C;QAAEM,OAAO;QAAiBC,IAAI;QAAYP;IAAM;AACtD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
18
|
+
import type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
19
|
+
import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
20
|
+
/** The list's scope: live products only. */
|
|
21
|
+
export declare const PRODUCT_LIST_BASE: readonly ListQueryFilter[];
|
|
22
|
+
/** The scope, as `listQueryIndexes` enumerates it. */
|
|
23
|
+
export declare const PRODUCT_LIST_INDEX_BASE: readonly {
|
|
24
|
+
path: string;
|
|
25
|
+
array?: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
export declare const PRODUCT_LIST_FIELDS: readonly ListFilterField[];
|
|
28
|
+
export declare const PRODUCT_LIST_QUERY: ListQueryDeclaration;
|
|
29
|
+
export declare const PRODUCT_LIST_HEADERS: Readonly<Record<string, string>>;
|
|
30
|
+
export declare const PRODUCT_LIST_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
31
|
+
/** The fields whose values are picked, which the panel shows as a select. */
|
|
32
|
+
export declare const PRODUCT_LIST_SELECT_FIELDS: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* The clauses as the query must ask them.
|
|
35
|
+
*
|
|
36
|
+
* `productSearchFields` stores every SKU and barcode trimmed and lower-cased,
|
|
37
|
+
* and an array field is matched by whole member, byte for byte — so a SKU
|
|
38
|
+
* typed `ABC-123` would ask for a member nobody stored. The typed value is
|
|
39
|
+
* folded the way the writer folded it; `is any of` folds each of its values.
|
|
40
|
+
* Every other clause passes through as the panel wrote it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function productListRequestClauses<Clause extends ListFilterRequest>(clauses: readonly Clause[]): Clause[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
/*
|
|
18
|
+
* The products hub's catalog table (`hosts/{hostId}/products`): what its
|
|
19
|
+
* Filters panel and quick search may ask, and the ONE Firestore query that
|
|
20
|
+
* answers all of it (AGL-3321).
|
|
21
|
+
*
|
|
22
|
+
* Every clause and the search word go on the query and the pages come from
|
|
23
|
+
* it, so a product on page forty is found exactly as one on page one is. The
|
|
24
|
+
* card matches nothing over the rows it holds.
|
|
25
|
+
*
|
|
26
|
+
* ## The order, and why every product carries its key
|
|
27
|
+
*
|
|
28
|
+
* `nameLower` ascending, the one order the list offers. Ordering by a field
|
|
29
|
+
* drops every document that lacks it, so every writer stamps it through
|
|
30
|
+
* `productSearchFields` — the editor, Duplicate, the CSV importer and the AI
|
|
31
|
+
* drafts, all of which create through `/api/hosts/resources` — and
|
|
32
|
+
* `tools/scripts/backfill-products-list-fields.mjs` stamps it onto the
|
|
33
|
+
* products written before any of them did.
|
|
34
|
+
*
|
|
35
|
+
* ## Soft-deleted products are excluded BY THE QUERY
|
|
36
|
+
*
|
|
37
|
+
* `deletedAt` is `null` on a live product and a timestamp on a deleted one.
|
|
38
|
+
* Firestore cannot ask for a document that LACKS a field, so the list's scope
|
|
39
|
+
* is `deletedAt == null`, and the create route stamps the explicit `null` on
|
|
40
|
+
* every product it makes (the backfill stamps the rest). Dropping deleted
|
|
41
|
+
* rows after the read would leave a page short by however many it dropped.
|
|
42
|
+
*
|
|
43
|
+
* ## What is offered, and what is not
|
|
44
|
+
*
|
|
45
|
+
* - Name: `contains` (word prefix, over `nameTokens`), `equals` and
|
|
46
|
+
* `startsWith` (over `nameLower`, the field the list is ordered by, so a
|
|
47
|
+
* prefix range keeps the order it already has).
|
|
48
|
+
* - Status and Type: picked, `is` / `is any of`.
|
|
49
|
+
* - Slug: `is`. Slugs are lower-case by construction, so the stored value
|
|
50
|
+
* is its own key.
|
|
51
|
+
* - SKU and Barcode: WHOLE values, `contains` / `is any of`, over the
|
|
52
|
+
* flattened `skus` / `barcodes` arrays — a code is copied or scanned, not
|
|
53
|
+
* half-remembered. Stored lower-cased; see `productListRequestClauses`.
|
|
54
|
+
*
|
|
55
|
+
* Not offered, and why: price and the created/updated dates are ranges, and a
|
|
56
|
+
* range must lead the order, so each would add a whole second order's worth
|
|
57
|
+
* of composites for every field above (and `priceUsd` is the first
|
|
58
|
+
* variant's price, which describes a multi-variant product wrongly). A
|
|
59
|
+
* name `endsWith` needs the reversed key as its order for the same reason.
|
|
60
|
+
* Tags are stored as typed, so a whole-member match would be case-sensitive
|
|
61
|
+
* where the reader expects it not to be.
|
|
62
|
+
*
|
|
63
|
+
* ## The indexes
|
|
64
|
+
*
|
|
65
|
+
* One `(field, nameLower ASC)` composite per filterable field and for the
|
|
66
|
+
* scope, merged by Firestore for any combination:
|
|
67
|
+
* `listQueryIndexes(PRODUCT_LIST_QUERY, PRODUCT_LIST_INDEX_BASE)`, held to
|
|
68
|
+
* `cloud/firebase-firestore.indexes.json` by `product-list-query.spec.ts`.
|
|
69
|
+
*/ /** The list's scope: live products only. */ export const PRODUCT_LIST_BASE = [
|
|
70
|
+
{
|
|
71
|
+
path: 'deletedAt',
|
|
72
|
+
op: '==',
|
|
73
|
+
value: null
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
/** The scope, as `listQueryIndexes` enumerates it. */ export const PRODUCT_LIST_INDEX_BASE = [
|
|
77
|
+
{
|
|
78
|
+
path: 'deletedAt'
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
export const PRODUCT_LIST_FIELDS = [
|
|
82
|
+
{
|
|
83
|
+
column: 'name',
|
|
84
|
+
kind: 'text',
|
|
85
|
+
path: 'name',
|
|
86
|
+
lowerPath: 'nameLower',
|
|
87
|
+
tokensPath: 'nameTokens',
|
|
88
|
+
operators: [
|
|
89
|
+
'contains',
|
|
90
|
+
'equals',
|
|
91
|
+
'startsWith'
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
column: 'status',
|
|
96
|
+
kind: 'exact',
|
|
97
|
+
path: 'status',
|
|
98
|
+
operators: [
|
|
99
|
+
'equals',
|
|
100
|
+
'isAnyOf'
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
column: 'type',
|
|
105
|
+
kind: 'exact',
|
|
106
|
+
path: 'type',
|
|
107
|
+
operators: [
|
|
108
|
+
'equals',
|
|
109
|
+
'isAnyOf'
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
column: 'slug',
|
|
114
|
+
kind: 'text',
|
|
115
|
+
path: 'slug',
|
|
116
|
+
lowerPath: 'slug',
|
|
117
|
+
operators: [
|
|
118
|
+
'equals'
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
column: 'skus',
|
|
123
|
+
kind: 'exact',
|
|
124
|
+
path: 'skus',
|
|
125
|
+
tokensPath: 'skus',
|
|
126
|
+
operators: [
|
|
127
|
+
'contains',
|
|
128
|
+
'isAnyOf'
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
column: 'barcodes',
|
|
133
|
+
kind: 'exact',
|
|
134
|
+
path: 'barcodes',
|
|
135
|
+
tokensPath: 'barcodes',
|
|
136
|
+
operators: [
|
|
137
|
+
'contains',
|
|
138
|
+
'isAnyOf'
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
export const PRODUCT_LIST_QUERY = {
|
|
143
|
+
fields: PRODUCT_LIST_FIELDS,
|
|
144
|
+
sorts: [
|
|
145
|
+
{
|
|
146
|
+
path: 'nameLower',
|
|
147
|
+
direction: 'asc',
|
|
148
|
+
column: 'name'
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
search: {
|
|
152
|
+
tokensPath: 'nameTokens'
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
export const PRODUCT_LIST_HEADERS = {
|
|
156
|
+
name: 'Product',
|
|
157
|
+
status: 'Status',
|
|
158
|
+
type: 'Type',
|
|
159
|
+
slug: 'Slug',
|
|
160
|
+
skus: 'SKU',
|
|
161
|
+
barcodes: 'Barcode'
|
|
162
|
+
};
|
|
163
|
+
export const PRODUCT_LIST_OPTIONS = {
|
|
164
|
+
status: [
|
|
165
|
+
{
|
|
166
|
+
value: 'active',
|
|
167
|
+
label: 'Active'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
value: 'draft',
|
|
171
|
+
label: 'Draft'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
value: 'archived',
|
|
175
|
+
label: 'Archived'
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
type: [
|
|
179
|
+
{
|
|
180
|
+
value: 'physical',
|
|
181
|
+
label: 'Physical'
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
value: 'digital',
|
|
185
|
+
label: 'Digital'
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
value: 'service',
|
|
189
|
+
label: 'Service'
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
/** The fields whose values are picked, which the panel shows as a select. */ export const PRODUCT_LIST_SELECT_FIELDS = Object.keys(PRODUCT_LIST_OPTIONS);
|
|
194
|
+
/** The columns folded to lower case before they reach the query. */ const FOLDED = new Set([
|
|
195
|
+
'skus',
|
|
196
|
+
'barcodes'
|
|
197
|
+
]);
|
|
198
|
+
/**
|
|
199
|
+
* The clauses as the query must ask them.
|
|
200
|
+
*
|
|
201
|
+
* `productSearchFields` stores every SKU and barcode trimmed and lower-cased,
|
|
202
|
+
* and an array field is matched by whole member, byte for byte — so a SKU
|
|
203
|
+
* typed `ABC-123` would ask for a member nobody stored. The typed value is
|
|
204
|
+
* folded the way the writer folded it; `is any of` folds each of its values.
|
|
205
|
+
* Every other clause passes through as the panel wrote it.
|
|
206
|
+
*/ export function productListRequestClauses(clauses) {
|
|
207
|
+
const fold = (value)=>value.trim().toLowerCase();
|
|
208
|
+
return clauses.map((clause)=>{
|
|
209
|
+
if (!FOLDED.has(clause.field)) return clause;
|
|
210
|
+
const value = clause.op === 'isAnyOf' ? clause.value.split(',').map(fold).filter(Boolean).join(',') : fold(clause.value);
|
|
211
|
+
return _extends({}, clause, {
|
|
212
|
+
value
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//# sourceMappingURL=product-list-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/product-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * The products hub's catalog table (`hosts/{hostId}/products`): what its\n * Filters panel and quick search may ask, and the ONE Firestore query that\n * answers all of it (AGL-3321).\n *\n * Every clause and the search word go on the query and the pages come from\n * it, so a product on page forty is found exactly as one on page one is. The\n * card matches nothing over the rows it holds.\n *\n * ## The order, and why every product carries its key\n *\n * `nameLower` ascending, the one order the list offers. Ordering by a field\n * drops every document that lacks it, so every writer stamps it through\n * `productSearchFields` — the editor, Duplicate, the CSV importer and the AI\n * drafts, all of which create through `/api/hosts/resources` — and\n * `tools/scripts/backfill-products-list-fields.mjs` stamps it onto the\n * products written before any of them did.\n *\n * ## Soft-deleted products are excluded BY THE QUERY\n *\n * `deletedAt` is `null` on a live product and a timestamp on a deleted one.\n * Firestore cannot ask for a document that LACKS a field, so the list's scope\n * is `deletedAt == null`, and the create route stamps the explicit `null` on\n * every product it makes (the backfill stamps the rest). Dropping deleted\n * rows after the read would leave a page short by however many it dropped.\n *\n * ## What is offered, and what is not\n *\n * - Name: `contains` (word prefix, over `nameTokens`), `equals` and\n * `startsWith` (over `nameLower`, the field the list is ordered by, so a\n * prefix range keeps the order it already has).\n * - Status and Type: picked, `is` / `is any of`.\n * - Slug: `is`. Slugs are lower-case by construction, so the stored value\n * is its own key.\n * - SKU and Barcode: WHOLE values, `contains` / `is any of`, over the\n * flattened `skus` / `barcodes` arrays — a code is copied or scanned, not\n * half-remembered. Stored lower-cased; see `productListRequestClauses`.\n *\n * Not offered, and why: price and the created/updated dates are ranges, and a\n * range must lead the order, so each would add a whole second order's worth\n * of composites for every field above (and `priceUsd` is the first\n * variant's price, which describes a multi-variant product wrongly). A\n * name `endsWith` needs the reversed key as its order for the same reason.\n * Tags are stored as typed, so a whole-member match would be case-sensitive\n * where the reader expects it not to be.\n *\n * ## The indexes\n *\n * One `(field, nameLower ASC)` composite per filterable field and for the\n * scope, merged by Firestore for any combination:\n * `listQueryIndexes(PRODUCT_LIST_QUERY, PRODUCT_LIST_INDEX_BASE)`, held to\n * `cloud/firebase-firestore.indexes.json` by `product-list-query.spec.ts`.\n */\n\n/** The list's scope: live products only. */\nexport const PRODUCT_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'deletedAt', op: '==', value: null },\n]\n\n/** The scope, as `listQueryIndexes` enumerates it. */\nexport const PRODUCT_LIST_INDEX_BASE: readonly { path: string; array?: boolean }[] = [\n { path: 'deletedAt' },\n]\n\nexport const PRODUCT_LIST_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals', 'startsWith'],\n },\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'type', kind: 'exact', path: 'type', operators: ['equals', 'isAnyOf'] },\n { column: 'slug', kind: 'text', path: 'slug', lowerPath: 'slug', operators: ['equals'] },\n {\n column: 'skus',\n kind: 'exact',\n path: 'skus',\n tokensPath: 'skus',\n operators: ['contains', 'isAnyOf'],\n },\n {\n column: 'barcodes',\n kind: 'exact',\n path: 'barcodes',\n tokensPath: 'barcodes',\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const PRODUCT_LIST_QUERY: ListQueryDeclaration = {\n fields: PRODUCT_LIST_FIELDS,\n sorts: [{ path: 'nameLower', direction: 'asc', column: 'name' }],\n search: { tokensPath: 'nameTokens' },\n}\n\nexport const PRODUCT_LIST_HEADERS: Readonly<Record<string, string>> = {\n name: 'Product',\n status: 'Status',\n type: 'Type',\n slug: 'Slug',\n skus: 'SKU',\n barcodes: 'Barcode',\n}\n\nexport const PRODUCT_LIST_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'draft', label: 'Draft' },\n { value: 'archived', label: 'Archived' },\n ],\n type: [\n { value: 'physical', label: 'Physical' },\n { value: 'digital', label: 'Digital' },\n { value: 'service', label: 'Service' },\n ],\n}\n\n/** The fields whose values are picked, which the panel shows as a select. */\nexport const PRODUCT_LIST_SELECT_FIELDS: readonly string[] = Object.keys(PRODUCT_LIST_OPTIONS)\n\n/** The columns folded to lower case before they reach the query. */\nconst FOLDED = new Set(['skus', 'barcodes'])\n\n/**\n * The clauses as the query must ask them.\n *\n * `productSearchFields` stores every SKU and barcode trimmed and lower-cased,\n * and an array field is matched by whole member, byte for byte — so a SKU\n * typed `ABC-123` would ask for a member nobody stored. The typed value is\n * folded the way the writer folded it; `is any of` folds each of its values.\n * Every other clause passes through as the panel wrote it.\n */\nexport function productListRequestClauses<Clause extends ListFilterRequest>(\n clauses: readonly Clause[],\n): Clause[] {\n const fold = (value: string) => value.trim().toLowerCase()\n return clauses.map((clause) => {\n if (!FOLDED.has(clause.field)) return clause\n const value =\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map(fold).filter(Boolean).join(',')\n : fold(clause.value)\n return { ...clause, value }\n })\n}\n"],"names":["PRODUCT_LIST_BASE","path","op","value","PRODUCT_LIST_INDEX_BASE","PRODUCT_LIST_FIELDS","column","kind","lowerPath","tokensPath","operators","PRODUCT_LIST_QUERY","fields","sorts","direction","search","PRODUCT_LIST_HEADERS","name","status","type","slug","skus","barcodes","PRODUCT_LIST_OPTIONS","label","PRODUCT_LIST_SELECT_FIELDS","Object","keys","FOLDED","Set","productListRequestClauses","clauses","fold","trim","toLowerCase","map","clause","has","field","split","filter","Boolean","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDC,GAED,0CAA0C,GAC1C,OAAO,MAAMA,oBAAgD;IAC3D;QAAEC,MAAM;QAAaC,IAAI;QAAMC,OAAO;IAAK;CAC5C,CAAA;AAED,oDAAoD,GACpD,OAAO,MAAMC,0BAAwE;IACnF;QAAEH,MAAM;IAAY;CACrB,CAAA;AAED,OAAO,MAAMI,sBAAkD;IAC7D;QACEC,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNO,WAAW;QACXC,YAAY;QACZC,WAAW;YAAC;YAAY;YAAU;SAAa;IACjD;IACA;QAAEJ,QAAQ;QAAUC,MAAM;QAASN,MAAM;QAAUS,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEJ,QAAQ;QAAQC,MAAM;QAASN,MAAM;QAAQS,WAAW;YAAC;YAAU;SAAU;IAAC;IAChF;QAAEJ,QAAQ;QAAQC,MAAM;QAAQN,MAAM;QAAQO,WAAW;QAAQE,WAAW;YAAC;SAAS;IAAC;IACvF;QACEJ,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNQ,YAAY;QACZC,WAAW;YAAC;YAAY;SAAU;IACpC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNQ,YAAY;QACZC,WAAW;YAAC;YAAY;SAAU;IACpC;CACD,CAAA;AAED,OAAO,MAAMC,qBAA2C;IACtDC,QAAQP;IACRQ,OAAO;QAAC;YAAEZ,MAAM;YAAaa,WAAW;YAAOR,QAAQ;QAAO;KAAE;IAChES,QAAQ;QAAEN,YAAY;IAAa;AACrC,EAAC;AAED,OAAO,MAAMO,uBAAyD;IACpEC,MAAM;IACNC,QAAQ;IACRC,MAAM;IACNC,MAAM;IACNC,MAAM;IACNC,UAAU;AACZ,EAAC;AAED,OAAO,MAAMC,uBAA8E;IACzFL,QAAQ;QACN;YAAEf,OAAO;YAAUqB,OAAO;QAAS;QACnC;YAAErB,OAAO;YAASqB,OAAO;QAAQ;QACjC;YAAErB,OAAO;YAAYqB,OAAO;QAAW;KACxC;IACDL,MAAM;QACJ;YAAEhB,OAAO;YAAYqB,OAAO;QAAW;QACvC;YAAErB,OAAO;YAAWqB,OAAO;QAAU;QACrC;YAAErB,OAAO;YAAWqB,OAAO;QAAU;KACtC;AACH,EAAC;AAED,2EAA2E,GAC3E,OAAO,MAAMC,6BAAgDC,OAAOC,IAAI,CAACJ,sBAAqB;AAE9F,kEAAkE,GAClE,MAAMK,SAAS,IAAIC,IAAI;IAAC;IAAQ;CAAW;AAE3C;;;;;;;;CAQC,GACD,OAAO,SAASC,0BACdC,OAA0B;IAE1B,MAAMC,OAAO,CAAC7B,QAAkBA,MAAM8B,IAAI,GAAGC,WAAW;IACxD,OAAOH,QAAQI,GAAG,CAAC,CAACC;QAClB,IAAI,CAACR,OAAOS,GAAG,CAACD,OAAOE,KAAK,GAAG,OAAOF;QACtC,MAAMjC,QACJiC,OAAOlC,EAAE,KAAK,YACVkC,OAAOjC,KAAK,CAACoC,KAAK,CAAC,KAAKJ,GAAG,CAACH,MAAMQ,MAAM,CAACC,SAASC,IAAI,CAAC,OACvDV,KAAKI,OAAOjC,KAAK;QACvB,OAAO,aAAKiC;YAAQjC;;IACtB;AACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
18
|
+
import type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
19
|
+
import type { InventoryAdjustmentReason } from '../model/commerce';
|
|
20
|
+
export declare const STOCK_MOVEMENT_FILTER_FIELDS: readonly ListFilterField[];
|
|
21
|
+
/** The ledger's one order: newest first, the order every writer stamps. */
|
|
22
|
+
export declare const STOCK_MOVEMENT_QUERY: ListQueryDeclaration;
|
|
23
|
+
export declare const STOCK_MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>>;
|
|
24
|
+
/** The fields picked from choices rather than typed. */
|
|
25
|
+
export declare const STOCK_MOVEMENT_SELECT_FIELDS: readonly string[];
|
|
26
|
+
/** The stored key is a closed set; these are what a merchant reads. */
|
|
27
|
+
export declare const STOCK_MOVEMENT_REASON_LABEL: Readonly<Record<InventoryAdjustmentReason, string>>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*
|
|
17
|
+
* What the stock movements ledger filters by, and how its query serves it
|
|
18
|
+
* (AGL-3321).
|
|
19
|
+
*
|
|
20
|
+
* Every field is one every writer of `hosts/{hostId}/inventoryAdjustments`
|
|
21
|
+
* already stores — the "Adjust stock" dialog, the sale path
|
|
22
|
+
* (`decrementVariantStock`) and the cancellation restock — so nothing new is
|
|
23
|
+
* written and nothing needs a backfill. Each is an EQUALITY beneath the
|
|
24
|
+
* ledger's one order, `atMs` newest first, which is one `(field, atMs DESC)`
|
|
25
|
+
* composite per field under index merging; the date filter is a range on
|
|
26
|
+
* `atMs` itself, the order the ledger is already in, so it costs nothing.
|
|
27
|
+
*
|
|
28
|
+
* Order and location are typed or picked ids, matched whole. A sparse field
|
|
29
|
+
* answers `equals` exactly — a row with no order is simply not that order —
|
|
30
|
+
* and offers no "is set", which would be an inequality on a second field.
|
|
31
|
+
*
|
|
32
|
+
* No quick search. The ledger's rows name a product by id; the name a reader
|
|
33
|
+
* would type lives on the product, so a search could only ever match the
|
|
34
|
+
* names of the rows already loaded. Product is a picked filter instead, and
|
|
35
|
+
* the ids the old search read (order, location) are filters of their own.
|
|
36
|
+
*/ export const STOCK_MOVEMENT_FILTER_FIELDS = [
|
|
37
|
+
{
|
|
38
|
+
column: 'productId',
|
|
39
|
+
kind: 'exact',
|
|
40
|
+
path: 'productId',
|
|
41
|
+
operators: [
|
|
42
|
+
'equals',
|
|
43
|
+
'isAnyOf'
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
column: 'reason',
|
|
48
|
+
kind: 'exact',
|
|
49
|
+
path: 'reason',
|
|
50
|
+
operators: [
|
|
51
|
+
'equals',
|
|
52
|
+
'isAnyOf'
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
column: 'atMs',
|
|
57
|
+
kind: 'date',
|
|
58
|
+
path: 'atMs',
|
|
59
|
+
storedAs: 'millis',
|
|
60
|
+
presence: 'always'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
column: 'orderId',
|
|
64
|
+
kind: 'exact',
|
|
65
|
+
path: 'orderId',
|
|
66
|
+
operators: [
|
|
67
|
+
'equals'
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
column: 'locationId',
|
|
72
|
+
kind: 'exact',
|
|
73
|
+
path: 'locationId',
|
|
74
|
+
operators: [
|
|
75
|
+
'equals'
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
/** The ledger's one order: newest first, the order every writer stamps. */ export const STOCK_MOVEMENT_QUERY = {
|
|
80
|
+
fields: STOCK_MOVEMENT_FILTER_FIELDS,
|
|
81
|
+
sorts: [
|
|
82
|
+
{
|
|
83
|
+
path: 'atMs',
|
|
84
|
+
direction: 'desc',
|
|
85
|
+
column: 'atMs'
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
export const STOCK_MOVEMENT_FILTER_HEADERS = {
|
|
90
|
+
productId: 'Product',
|
|
91
|
+
reason: 'Reason',
|
|
92
|
+
atMs: 'When',
|
|
93
|
+
orderId: 'Order',
|
|
94
|
+
locationId: 'Location'
|
|
95
|
+
};
|
|
96
|
+
/** The fields picked from choices rather than typed. */ export const STOCK_MOVEMENT_SELECT_FIELDS = [
|
|
97
|
+
'productId',
|
|
98
|
+
'reason',
|
|
99
|
+
'locationId'
|
|
100
|
+
];
|
|
101
|
+
/** The stored key is a closed set; these are what a merchant reads. */ export const STOCK_MOVEMENT_REASON_LABEL = {
|
|
102
|
+
sale: 'Sale',
|
|
103
|
+
refund: 'Refund return',
|
|
104
|
+
restock: 'Restock',
|
|
105
|
+
correction: 'Correction',
|
|
106
|
+
damage: 'Damaged',
|
|
107
|
+
cancellation: 'Order canceled'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=stock-movement-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/stock-movement-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport type { InventoryAdjustmentReason } from '../model/commerce'\n\n/*\n * What the stock movements ledger filters by, and how its query serves it\n * (AGL-3321).\n *\n * Every field is one every writer of `hosts/{hostId}/inventoryAdjustments`\n * already stores — the \"Adjust stock\" dialog, the sale path\n * (`decrementVariantStock`) and the cancellation restock — so nothing new is\n * written and nothing needs a backfill. Each is an EQUALITY beneath the\n * ledger's one order, `atMs` newest first, which is one `(field, atMs DESC)`\n * composite per field under index merging; the date filter is a range on\n * `atMs` itself, the order the ledger is already in, so it costs nothing.\n *\n * Order and location are typed or picked ids, matched whole. A sparse field\n * answers `equals` exactly — a row with no order is simply not that order —\n * and offers no \"is set\", which would be an inequality on a second field.\n *\n * No quick search. The ledger's rows name a product by id; the name a reader\n * would type lives on the product, so a search could only ever match the\n * names of the rows already loaded. Product is a picked filter instead, and\n * the ids the old search read (order, location) are filters of their own.\n */\nexport const STOCK_MOVEMENT_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'productId', kind: 'exact', path: 'productId', operators: ['equals', 'isAnyOf'] },\n { column: 'reason', kind: 'exact', path: 'reason', operators: ['equals', 'isAnyOf'] },\n { column: 'atMs', kind: 'date', path: 'atMs', storedAs: 'millis', presence: 'always' },\n { column: 'orderId', kind: 'exact', path: 'orderId', operators: ['equals'] },\n { column: 'locationId', kind: 'exact', path: 'locationId', operators: ['equals'] },\n]\n\n/** The ledger's one order: newest first, the order every writer stamps. */\nexport const STOCK_MOVEMENT_QUERY: ListQueryDeclaration = {\n fields: STOCK_MOVEMENT_FILTER_FIELDS,\n sorts: [{ path: 'atMs', direction: 'desc', column: 'atMs' }],\n}\n\nexport const STOCK_MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n productId: 'Product',\n reason: 'Reason',\n atMs: 'When',\n orderId: 'Order',\n locationId: 'Location',\n}\n\n/** The fields picked from choices rather than typed. */\nexport const STOCK_MOVEMENT_SELECT_FIELDS: readonly string[] = ['productId', 'reason', 'locationId']\n\n/** The stored key is a closed set; these are what a merchant reads. */\nexport const STOCK_MOVEMENT_REASON_LABEL: Readonly<Record<InventoryAdjustmentReason, string>> = {\n sale: 'Sale',\n refund: 'Refund return',\n restock: 'Restock',\n correction: 'Correction',\n damage: 'Damaged',\n cancellation: 'Order canceled',\n}\n"],"names":["STOCK_MOVEMENT_FILTER_FIELDS","column","kind","path","operators","storedAs","presence","STOCK_MOVEMENT_QUERY","fields","sorts","direction","STOCK_MOVEMENT_FILTER_HEADERS","productId","reason","atMs","orderId","locationId","STOCK_MOVEMENT_SELECT_FIELDS","STOCK_MOVEMENT_REASON_LABEL","sale","refund","restock","correction","damage","cancellation"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMA,+BAA2D;IACtE;QAAEC,QAAQ;QAAaC,MAAM;QAASC,MAAM;QAAaC,WAAW;YAAC;YAAU;SAAU;IAAC;IAC1F;QAAEH,QAAQ;QAAUC,MAAM;QAASC,MAAM;QAAUC,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEH,QAAQ;QAAQC,MAAM;QAAQC,MAAM;QAAQE,UAAU;QAAUC,UAAU;IAAS;IACrF;QAAEL,QAAQ;QAAWC,MAAM;QAASC,MAAM;QAAWC,WAAW;YAAC;SAAS;IAAC;IAC3E;QAAEH,QAAQ;QAAcC,MAAM;QAASC,MAAM;QAAcC,WAAW;YAAC;SAAS;IAAC;CAClF,CAAA;AAED,yEAAyE,GACzE,OAAO,MAAMG,uBAA6C;IACxDC,QAAQR;IACRS,OAAO;QAAC;YAAEN,MAAM;YAAQO,WAAW;YAAQT,QAAQ;QAAO;KAAE;AAC9D,EAAC;AAED,OAAO,MAAMU,gCAAkE;IAC7EC,WAAW;IACXC,QAAQ;IACRC,MAAM;IACNC,SAAS;IACTC,YAAY;AACd,EAAC;AAED,sDAAsD,GACtD,OAAO,MAAMC,+BAAkD;IAAC;IAAa;IAAU;CAAa,CAAA;AAEpG,qEAAqE,GACrE,OAAO,MAAMC,8BAAmF;IAC9FC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,YAAY;IACZC,QAAQ;IACRC,cAAc;AAChB,EAAC"}
|