@aglyn/plugins-commerce 1.0.0-beta.231 → 1.0.0-beta.232
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 +1 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +4 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +56 -26
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-zones.d.ts +23 -0
- package/src/lib/components/console/pos-zones.js +31 -0
- package/src/lib/components/console/pos-zones.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +10 -64
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +11 -20
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +21 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +44 -1
- package/src/lib/model/commerce-orders.js +2 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +4 -1
- package/src/lib/model/commerce.js.map +1 -1
- 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/product-write.d.ts +186 -0
- package/src/lib/model/product-write.js +116 -0
- package/src/lib/model/product-write.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/channel-order-updates.d.ts +50 -0
- package/src/lib/server/channel-order-updates.js +290 -0
- package/src/lib/server/channel-order-updates.js.map +1 -0
- package/src/lib/server/channel-orders.d.ts +85 -0
- package/src/lib/server/channel-orders.js +543 -0
- package/src/lib/server/channel-orders.js.map +1 -0
- package/src/lib/server/product-catalog.js +5 -1
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +5 -1
- package/src/lib/server/product-drafts.js +3 -3
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product-writer.d.ts +19 -0
- package/src/lib/server/product-writer.js +261 -0
- package/src/lib/server/product-writer.js.map +1 -0
- package/src/lib/server/products-write.d.ts +61 -0
- package/src/lib/server/products-write.js +375 -0
- package/src/lib/server/products-write.js.map +1 -0
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +16 -3
- package/src/lib/testing/memory-firestore.js +11 -3
- package/src/lib/testing/memory-firestore.js.map +1 -1
- package/src/lib/transfer/products.server.d.ts +5 -0
- package/src/lib/transfer/products.server.js +2 -2
- package/src/lib/transfer/products.server.js.map +1 -1
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin, getOrgForHost, logHostActivity } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
|
|
20
|
+
import { commerceSlug, liftLegacyProduct, validateProduct } from "../model/commerce.js";
|
|
21
|
+
import { derivedKeys, readSmartCollections } from "../transfer/products.server.js";
|
|
22
|
+
import { withoutUndefined } from "../transfer/server-common.js";
|
|
23
|
+
/** The fields an update compares to tell a change from a repeat. */ const COMPARED_KEYS = [
|
|
24
|
+
'name',
|
|
25
|
+
'description',
|
|
26
|
+
'mediaUrls',
|
|
27
|
+
'tags',
|
|
28
|
+
'options',
|
|
29
|
+
'variants'
|
|
30
|
+
];
|
|
31
|
+
/** How many suffixed addresses a new product tries before it gives up. */ const SLUG_TRIES = 30;
|
|
32
|
+
const SLUGS_PER_QUERY = 30;
|
|
33
|
+
const newVariantId = ()=>`v${Aglyn.createResourceUid().slice(0, 10)}`;
|
|
34
|
+
function priceUsd(minor) {
|
|
35
|
+
return Math.round(Math.max(0, Number(minor) || 0)) / 100;
|
|
36
|
+
}
|
|
37
|
+
/** The variant a sourced configuration becomes, on top of the one it was. */ function variantFrom(source, existing, options) {
|
|
38
|
+
var _ref;
|
|
39
|
+
const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor);
|
|
40
|
+
const next = _extends({}, existing != null ? existing : {}, {
|
|
41
|
+
id: (_ref = existing == null ? void 0 : existing.id) != null ? _ref : newVariantId(),
|
|
42
|
+
options: _extends({}, source.options),
|
|
43
|
+
priceUsd: price,
|
|
44
|
+
inventory: source.available ? null : 0
|
|
45
|
+
});
|
|
46
|
+
delete next.inventoryByLocation;
|
|
47
|
+
if (source.sku) next.sku = source.sku.slice(0, 120);
|
|
48
|
+
else delete next.sku;
|
|
49
|
+
if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {
|
|
50
|
+
next.weightGrams = Math.round(Number(source.weightGrams));
|
|
51
|
+
}
|
|
52
|
+
if (source.imageUrl && (options.content || !(existing == null ? void 0 : existing.imageUrl))) next.imageUrl = source.imageUrl;
|
|
53
|
+
// A price brought below a compare-at that no longer exceeds it would be
|
|
54
|
+
// refused: the strike-through goes, the price stays.
|
|
55
|
+
if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {
|
|
56
|
+
delete next.compareAtPriceUsd;
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
async function freeSlug(products, name) {
|
|
61
|
+
var _candidates_find;
|
|
62
|
+
const base = commerceSlug(name).slice(0, 80) || 'product';
|
|
63
|
+
const candidates = [
|
|
64
|
+
base,
|
|
65
|
+
...Array.from({
|
|
66
|
+
length: SLUG_TRIES - 1
|
|
67
|
+
}, (_, index)=>`${base}-${index + 2}`)
|
|
68
|
+
];
|
|
69
|
+
const taken = new Set();
|
|
70
|
+
for(let at = 0; at < candidates.length; at += SLUGS_PER_QUERY){
|
|
71
|
+
const slice = candidates.slice(at, at + SLUGS_PER_QUERY);
|
|
72
|
+
for (const doc of (await products.where('slug', 'in', slice).get()).docs)taken.add(String(doc.get('slug')));
|
|
73
|
+
}
|
|
74
|
+
return (_candidates_find = candidates.find((candidate)=>!taken.has(candidate))) != null ? _candidates_find : null;
|
|
75
|
+
}
|
|
76
|
+
let WriteRefusal = class WriteRefusal extends Error {
|
|
77
|
+
constructor(outcome){
|
|
78
|
+
super(outcome.outcome), this.outcome = outcome;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export async function upsertSourcedProduct(write, firestore = firebaseAdmin.app().firestore()) {
|
|
82
|
+
var _write_actorUid, _source_description;
|
|
83
|
+
const { hostId, product: source } = write;
|
|
84
|
+
if (!isDocumentId(hostId)) return {
|
|
85
|
+
outcome: 'no_store'
|
|
86
|
+
};
|
|
87
|
+
const owner = await getOrgForHost(hostId);
|
|
88
|
+
if (!owner || !Aglyn.checkEntitlement(owner.org, 'commerce')) return {
|
|
89
|
+
outcome: 'no_store'
|
|
90
|
+
};
|
|
91
|
+
const org = owner.org;
|
|
92
|
+
const products = firestore.collection('hosts').doc(hostId).collection('products');
|
|
93
|
+
const smart = await readSmartCollections(firestore, {
|
|
94
|
+
hostId
|
|
95
|
+
});
|
|
96
|
+
const { Timestamp } = firebaseAdmin.firestore;
|
|
97
|
+
const actor = {
|
|
98
|
+
uid: (_write_actorUid = write.actorUid) != null ? _write_actorUid : 'import',
|
|
99
|
+
email: null
|
|
100
|
+
};
|
|
101
|
+
const name = source.name.trim().slice(0, 200);
|
|
102
|
+
const description = ((_source_description = source.description) != null ? _source_description : '').trim().slice(0, 20000);
|
|
103
|
+
const mapping = (product)=>source.variants.map((variant, index)=>({
|
|
104
|
+
key: variant.key,
|
|
105
|
+
variantId: product.variants[index].id
|
|
106
|
+
}));
|
|
107
|
+
try {
|
|
108
|
+
var _source_tags;
|
|
109
|
+
// An update, when the product is still there.
|
|
110
|
+
if (write.productId && isDocumentId(write.productId)) {
|
|
111
|
+
const ref = products.doc(write.productId);
|
|
112
|
+
const updated = await firestore.runTransaction(async (tx)=>{
|
|
113
|
+
const snapshot = await tx.get(ref);
|
|
114
|
+
const data = snapshot.data();
|
|
115
|
+
if (!snapshot.exists || (data == null ? void 0 : data['deletedAt']) !== null && (data == null ? void 0 : data['deletedAt']) !== undefined) return null;
|
|
116
|
+
const current = liftLegacyProduct(data);
|
|
117
|
+
const flags = {
|
|
118
|
+
prices: write.prices === true,
|
|
119
|
+
content: write.content === true
|
|
120
|
+
};
|
|
121
|
+
const byId = new Map(current.variants.map((variant)=>[
|
|
122
|
+
variant.id,
|
|
123
|
+
variant
|
|
124
|
+
]));
|
|
125
|
+
const next = _extends({}, current, {
|
|
126
|
+
options: source.options.map((option)=>({
|
|
127
|
+
name: option.name,
|
|
128
|
+
values: [
|
|
129
|
+
...option.values
|
|
130
|
+
]
|
|
131
|
+
})),
|
|
132
|
+
variants: source.variants.map((variant)=>variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags))
|
|
133
|
+
}, flags.content ? _extends({
|
|
134
|
+
name,
|
|
135
|
+
description,
|
|
136
|
+
mediaUrls: [
|
|
137
|
+
...source.mediaUrls
|
|
138
|
+
]
|
|
139
|
+
}, source.tags ? {
|
|
140
|
+
tags: [
|
|
141
|
+
...source.tags
|
|
142
|
+
]
|
|
143
|
+
} : {}) : {});
|
|
144
|
+
const invalid = validateProduct(next);
|
|
145
|
+
if (invalid) throw new WriteRefusal({
|
|
146
|
+
outcome: 'invalid',
|
|
147
|
+
message: invalid
|
|
148
|
+
});
|
|
149
|
+
const changed = COMPARED_KEYS.some((key)=>{
|
|
150
|
+
var _next_key, _current_key;
|
|
151
|
+
return JSON.stringify((_next_key = next[key]) != null ? _next_key : null) !== JSON.stringify((_current_key = current[key]) != null ? _current_key : null);
|
|
152
|
+
});
|
|
153
|
+
if (changed) {
|
|
154
|
+
tx.update(ref, withoutUndefined(_extends({}, Object.fromEntries(COMPARED_KEYS.map((key)=>[
|
|
155
|
+
key,
|
|
156
|
+
next[key]
|
|
157
|
+
])), derivedKeys(next, smart), {
|
|
158
|
+
updatedAtMs: Date.now(),
|
|
159
|
+
updatedAt: Timestamp.now()
|
|
160
|
+
})));
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
next,
|
|
164
|
+
changed
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
if (updated) {
|
|
168
|
+
if (updated.changed) {
|
|
169
|
+
await logHostActivity(hostId, actor, 'Updated product from its source', {
|
|
170
|
+
type: 'content',
|
|
171
|
+
id: write.productId,
|
|
172
|
+
name: updated.next.name
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
outcome: updated.changed ? 'updated' : 'unchanged',
|
|
177
|
+
productId: write.productId,
|
|
178
|
+
variants: mapping(updated.next)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (write.productId && write.recreate === false) return {
|
|
183
|
+
outcome: 'missing'
|
|
184
|
+
};
|
|
185
|
+
// A new product.
|
|
186
|
+
const slug = await freeSlug(products, name);
|
|
187
|
+
if (!slug) return {
|
|
188
|
+
outcome: 'invalid',
|
|
189
|
+
message: 'Every address for a product of this name is taken. Rename one first.'
|
|
190
|
+
};
|
|
191
|
+
const productId = Aglyn.createResourceUid();
|
|
192
|
+
const product = _extends({
|
|
193
|
+
name,
|
|
194
|
+
slug,
|
|
195
|
+
description,
|
|
196
|
+
type: 'physical',
|
|
197
|
+
status: write.status === 'active' ? 'active' : 'draft',
|
|
198
|
+
mediaUrls: [
|
|
199
|
+
...source.mediaUrls
|
|
200
|
+
]
|
|
201
|
+
}, ((_source_tags = source.tags) == null ? void 0 : _source_tags.length) ? {
|
|
202
|
+
tags: [
|
|
203
|
+
...source.tags
|
|
204
|
+
]
|
|
205
|
+
} : {}, {
|
|
206
|
+
options: source.options.map((option)=>({
|
|
207
|
+
name: option.name,
|
|
208
|
+
values: [
|
|
209
|
+
...option.values
|
|
210
|
+
]
|
|
211
|
+
})),
|
|
212
|
+
variants: source.variants.map((variant)=>variantFrom(variant, undefined, {
|
|
213
|
+
prices: true,
|
|
214
|
+
content: true
|
|
215
|
+
})),
|
|
216
|
+
oversellPolicy: 'deny'
|
|
217
|
+
});
|
|
218
|
+
const invalid = validateProduct(product);
|
|
219
|
+
if (invalid) return {
|
|
220
|
+
outcome: 'invalid',
|
|
221
|
+
message: invalid
|
|
222
|
+
};
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const ref = products.doc(productId);
|
|
225
|
+
await firestore.runTransaction(async (tx)=>{
|
|
226
|
+
var _write_actorUid;
|
|
227
|
+
const used = (await tx.get(products.count())).data().count;
|
|
228
|
+
const quota = Aglyn.checkQuota(org, 'productsPerHost', Number(used) || 0);
|
|
229
|
+
if (!quota.allowed) throw new WriteRefusal({
|
|
230
|
+
outcome: 'plan_limit',
|
|
231
|
+
limit: Number(quota.limit) || 0
|
|
232
|
+
});
|
|
233
|
+
tx.create(ref, withoutUndefined(_extends({}, product, derivedKeys(product, smart), {
|
|
234
|
+
createdAtMs: now,
|
|
235
|
+
updatedAtMs: now,
|
|
236
|
+
deletedAt: null,
|
|
237
|
+
createdAt: Timestamp.now(),
|
|
238
|
+
updatedAt: Timestamp.now(),
|
|
239
|
+
createdBy: (_write_actorUid = write.actorUid) != null ? _write_actorUid : null
|
|
240
|
+
})));
|
|
241
|
+
});
|
|
242
|
+
await logHostActivity(hostId, actor, 'Imported product', {
|
|
243
|
+
type: 'content',
|
|
244
|
+
id: productId,
|
|
245
|
+
name: product.name
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
outcome: 'created',
|
|
249
|
+
productId,
|
|
250
|
+
variants: mapping(product)
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof WriteRefusal) return error.outcome;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export const commerceProductWriter = {
|
|
258
|
+
upsertSourced: (write)=>upsertSourcedProduct(write)
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
//# sourceMappingURL=product-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-writer.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type {\n PluginProductWriter,\n SourcedProductWrite,\n SourcedProductWriteOutcome,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-writer'\nimport type { TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n commerceSlug,\n liftLegacyProduct,\n validateProduct,\n type HostProduct,\n type ProductVariant,\n} from '../model/commerce'\nimport { derivedKeys, readSmartCollections } from '../transfer/products.server'\nimport { withoutUndefined } from '../transfer/server-common'\n\n/**\n * Products another plugin brings from a source of its own — a print-on-demand\n * service, a supplier — written through the store's own rules (AGL-3641),\n * behind core's `core.product-writer`.\n *\n * The same rules the console's product route and the file import keep: a\n * create is counted against the plan's product allowance in the transaction\n * that makes it, a new product takes an address no other product holds (a\n * deleted one's included), every write carries the search, stock and\n * smart-collection keys every writer derives, and the site's activity says\n * what happened.\n *\n * A configuration the source cannot make now is sold out (`inventory: 0`);\n * one it can is untracked (`inventory: null`), because a made-to-order item\n * has no count to run down.\n */\n\ntype ProductDoc = Partial<HostProduct> & Record<string, unknown>\n\n/** The fields an update compares to tell a change from a repeat. */\nconst COMPARED_KEYS = ['name', 'description', 'mediaUrls', 'tags', 'options', 'variants'] as const\n\n/** How many suffixed addresses a new product tries before it gives up. */\nconst SLUG_TRIES = 30\nconst SLUGS_PER_QUERY = 30\n\nconst newVariantId = () => `v${Aglyn.createResourceUid().slice(0, 10)}`\n\nfunction priceUsd(minor: number): number {\n return Math.round(Math.max(0, Number(minor) || 0)) / 100\n}\n\n/** The variant a sourced configuration becomes, on top of the one it was. */\nfunction variantFrom(\n source: SourcedProductWrite['product']['variants'][number],\n existing: ProductVariant | undefined,\n options: { prices: boolean; content: boolean },\n): ProductVariant {\n const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor)\n const next: ProductVariant = {\n ...(existing ?? {}),\n id: existing?.id ?? newVariantId(),\n options: { ...source.options },\n priceUsd: price,\n inventory: source.available ? null : 0,\n }\n delete next.inventoryByLocation\n if (source.sku) next.sku = source.sku.slice(0, 120)\n else delete next.sku\n if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {\n next.weightGrams = Math.round(Number(source.weightGrams))\n }\n if (source.imageUrl && (options.content || !existing?.imageUrl)) next.imageUrl = source.imageUrl\n // A price brought below a compare-at that no longer exceeds it would be\n // refused: the strike-through goes, the price stays.\n if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {\n delete next.compareAtPriceUsd\n }\n return next\n}\n\nasync function freeSlug(products: FirebaseFirestore.CollectionReference, name: string): Promise<string | null> {\n const base = commerceSlug(name).slice(0, 80) || 'product'\n const candidates = [base, ...Array.from({ length: SLUG_TRIES - 1 }, (_, index) => `${base}-${index + 2}`)]\n const taken = new Set<string>()\n for (let at = 0; at < candidates.length; at += SLUGS_PER_QUERY) {\n const slice = candidates.slice(at, at + SLUGS_PER_QUERY)\n for (const doc of (await products.where('slug', 'in', slice).get()).docs) taken.add(String(doc.get('slug')))\n }\n return candidates.find((candidate) => !taken.has(candidate)) ?? null\n}\n\nclass WriteRefusal extends Error {\n constructor(readonly outcome: Exclude<SourcedProductWriteOutcome, { productId: string }>) {\n super(outcome.outcome)\n }\n}\n\nexport async function upsertSourcedProduct(\n write: SourcedProductWrite,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<SourcedProductWriteOutcome> {\n const { hostId, product: source } = write\n if (!isDocumentId(hostId)) return { outcome: 'no_store' }\n const owner = await getOrgForHost(hostId)\n if (!owner || !Aglyn.checkEntitlement(owner.org as never, 'commerce')) return { outcome: 'no_store' }\n const org = owner.org\n const products = firestore.collection('hosts').doc(hostId).collection('products')\n const smart = await readSmartCollections(firestore, { hostId } as TransferResourceContext)\n const { Timestamp } = firebaseAdmin.firestore\n const actor = { uid: write.actorUid ?? 'import', email: null }\n const name = source.name.trim().slice(0, 200)\n const description = (source.description ?? '').trim().slice(0, 20_000)\n\n const mapping = (product: HostProduct) =>\n source.variants.map((variant, index) => ({ key: variant.key, variantId: product.variants[index].id }))\n\n try {\n // An update, when the product is still there.\n if (write.productId && isDocumentId(write.productId)) {\n const ref = products.doc(write.productId)\n const updated = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const data = snapshot.data() as ProductDoc | undefined\n if (!snapshot.exists || (data?.['deletedAt'] !== null && data?.['deletedAt'] !== undefined)) return null\n const current = liftLegacyProduct(data as ProductDoc)\n const flags = { prices: write.prices === true, content: write.content === true }\n const byId = new Map(current.variants.map((variant) => [variant.id, variant]))\n const next: HostProduct = {\n ...current,\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) =>\n variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags),\n ),\n ...(flags.content\n ? {\n name,\n description,\n mediaUrls: [...source.mediaUrls],\n ...(source.tags ? { tags: [...source.tags] } : {}),\n }\n : {}),\n }\n const invalid = validateProduct(next)\n if (invalid) throw new WriteRefusal({ outcome: 'invalid', message: invalid })\n const changed = COMPARED_KEYS.some(\n (key) =>\n JSON.stringify((next as unknown as Record<string, unknown>)[key] ?? null) !==\n JSON.stringify((current as unknown as Record<string, unknown>)[key] ?? null),\n )\n if (changed) {\n tx.update(\n ref,\n withoutUndefined({\n ...Object.fromEntries(COMPARED_KEYS.map((key) => [key, (next as unknown as Record<string, unknown>)[key]])),\n ...derivedKeys(next, smart),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }) as FirebaseFirestore.UpdateData<FirebaseFirestore.DocumentData>,\n )\n }\n return { next, changed }\n })\n if (updated) {\n if (updated.changed) {\n await logHostActivity(hostId, actor, 'Updated product from its source', {\n type: 'content',\n id: write.productId,\n name: updated.next.name,\n })\n }\n return {\n outcome: updated.changed ? 'updated' : 'unchanged',\n productId: write.productId,\n variants: mapping(updated.next),\n }\n }\n }\n\n if (write.productId && write.recreate === false) return { outcome: 'missing' }\n\n // A new product.\n const slug = await freeSlug(products, name)\n if (!slug) return { outcome: 'invalid', message: 'Every address for a product of this name is taken. Rename one first.' }\n const productId = Aglyn.createResourceUid()\n const product: HostProduct = {\n name,\n slug,\n description,\n type: 'physical',\n status: write.status === 'active' ? 'active' : 'draft',\n mediaUrls: [...source.mediaUrls],\n ...(source.tags?.length ? { tags: [...source.tags] } : {}),\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) => variantFrom(variant, undefined, { prices: true, content: true })),\n oversellPolicy: 'deny',\n }\n const invalid = validateProduct(product)\n if (invalid) return { outcome: 'invalid', message: invalid }\n const now = Date.now()\n const ref = products.doc(productId)\n await firestore.runTransaction(async (tx) => {\n const used = (await tx.get(products.count())).data().count\n const quota = Aglyn.checkQuota(org as never, 'productsPerHost', Number(used) || 0)\n if (!quota.allowed) throw new WriteRefusal({ outcome: 'plan_limit', limit: Number(quota.limit) || 0 })\n tx.create(\n ref,\n withoutUndefined({\n ...product,\n ...derivedKeys(product, smart),\n createdAtMs: now,\n updatedAtMs: now,\n deletedAt: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: write.actorUid ?? null,\n }),\n )\n })\n await logHostActivity(hostId, actor, 'Imported product', { type: 'content', id: productId, name: product.name })\n return { outcome: 'created', productId, variants: mapping(product) }\n } catch (error) {\n if (error instanceof WriteRefusal) return error.outcome\n throw error\n }\n}\n\nexport const commerceProductWriter: PluginProductWriter = {\n upsertSourced: (write) => upsertSourcedProduct(write),\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","logHostActivity","isDocumentId","commerceSlug","liftLegacyProduct","validateProduct","derivedKeys","readSmartCollections","withoutUndefined","COMPARED_KEYS","SLUG_TRIES","SLUGS_PER_QUERY","newVariantId","createResourceUid","slice","priceUsd","minor","Math","round","max","Number","variantFrom","source","existing","options","price","prices","priceMinor","next","id","inventory","available","inventoryByLocation","sku","isFinite","weightGrams","imageUrl","content","compareAtPriceUsd","freeSlug","products","name","candidates","base","Array","from","length","_","index","taken","Set","at","doc","where","get","docs","add","String","find","candidate","has","WriteRefusal","Error","outcome","upsertSourcedProduct","write","firestore","app","hostId","product","owner","checkEntitlement","org","collection","smart","Timestamp","actor","uid","actorUid","email","trim","description","mapping","variants","map","variant","key","variantId","productId","ref","updated","runTransaction","tx","snapshot","data","exists","undefined","current","flags","byId","Map","option","values","mediaUrls","tags","invalid","message","changed","some","JSON","stringify","update","Object","fromEntries","updatedAtMs","Date","now","updatedAt","type","recreate","slug","status","oversellPolicy","used","count","quota","checkQuota","allowed","limit","create","createdAtMs","deletedAt","createdAt","createdBy","error","commerceProductWriter","upsertSourced"],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
|
|
18
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
19
|
+
import { getLockdownVerdict } from '@aglyn/tenant-data-admin';
|
|
20
|
+
import { type InventoryAdjustmentReason } from '../model';
|
|
21
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
22
|
+
/** The reasons the Adjust stock dialog offers. */
|
|
23
|
+
export declare const STOCK_ADJUST_REASONS: readonly InventoryAdjustmentReason[];
|
|
24
|
+
export interface ProductsWriteDeps {
|
|
25
|
+
firestore?: () => Firestore;
|
|
26
|
+
verifyIdToken?: (token: string) => Promise<Record<string, unknown> & {
|
|
27
|
+
uid: string;
|
|
28
|
+
}>;
|
|
29
|
+
orgForHost?: (hostId: string) => Promise<{
|
|
30
|
+
org: Record<string, unknown>;
|
|
31
|
+
} | null>;
|
|
32
|
+
lockdown?: (options: Parameters<typeof getLockdownVerdict>[0]) => Promise<unknown>;
|
|
33
|
+
dropCache?: typeof dropPluginSiteCache;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Saves a product as the console editor does. `create: true` makes a new
|
|
38
|
+
* product under the client-minted `productId` (a retry finds its own product
|
|
39
|
+
* and answers `replayed`); otherwise it replaces the live product, keeping
|
|
40
|
+
* its live stock holds.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createProductSaveHandler(options?: ProductsWriteDeps): PluginApiHandler;
|
|
43
|
+
/**
|
|
44
|
+
* The stored timestamps an edit keeps. The console's replace writes back the
|
|
45
|
+
* Timestamps its seed carried (`createdAt`, a publish time); a client's JSON
|
|
46
|
+
* cannot carry one, so each stored Timestamp field the request does not name
|
|
47
|
+
* is kept as stored.
|
|
48
|
+
*/
|
|
49
|
+
export declare function storedTimestamps(stored: Record<string, unknown>, asked: Record<string, unknown>): Record<string, unknown>;
|
|
50
|
+
/** The ledger row id one attempt owns: the same key from the same member is the same row. */
|
|
51
|
+
export declare function stockAttemptId(uid: string, key: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Adjusts one variant's stock as the Adjust stock dialog does: the variants
|
|
54
|
+
* and stock fields on the product, and one `inventoryAdjustments` row, in one
|
|
55
|
+
* transaction. The Idempotency-Key names that row, so a retried attempt finds
|
|
56
|
+
* it and moves nothing twice.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createProductStockHandler(options?: ProductsWriteDeps): PluginApiHandler;
|
|
59
|
+
export declare const productSaveHandler: PluginApiHandler;
|
|
60
|
+
export declare const productStockHandler: PluginApiHandler;
|
|
61
|
+
export {};
|