@brand-map/exchange-core 0.0.10-alpha.10

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.
@@ -0,0 +1,275 @@
1
+ // src/exports.ts
2
+ async function applyExchangeDocument(brandMap, document) {
3
+ const result = {
4
+ attributesUpserted: 0,
5
+ imagesAdded: 0,
6
+ inventorySynced: 0,
7
+ priceListPricesSet: 0,
8
+ priceListsUpserted: 0,
9
+ pricesSet: 0,
10
+ productsCreated: 0,
11
+ productsUpdated: 0,
12
+ rowsProcessed: document.products.length,
13
+ source: document.source,
14
+ variantOptionValuesLinked: 0,
15
+ variantsCreated: 0,
16
+ variantsUpdated: 0
17
+ };
18
+ for (const productDraft of document.products) {
19
+ const existingProduct = await findExistingProduct(brandMap, productDraft);
20
+ const product = existingProduct ? await brandMap.product.update({
21
+ description: productDraft.description,
22
+ id: existingProduct.id,
23
+ metadata: mergeMetadata(productDraft.metadata, document.source),
24
+ slug: productDraft.slug,
25
+ subtitle: productDraft.subtitle,
26
+ thumbnail: productDraft.thumbnail,
27
+ title: productDraft.title
28
+ }) : await brandMap.product.create({
29
+ categoryId: productDraft.categoryId,
30
+ description: productDraft.description,
31
+ metadata: mergeMetadata(productDraft.metadata, document.source),
32
+ slug: productDraft.slug,
33
+ status: productDraft.status ?? undefined,
34
+ subtitle: productDraft.subtitle,
35
+ thumbnail: productDraft.thumbnail,
36
+ title: productDraft.title
37
+ });
38
+ if (!product) {
39
+ throw new Error(`Failed to persist product "${productDraft.slug}".`);
40
+ }
41
+ if (existingProduct)
42
+ result.productsUpdated++;
43
+ else
44
+ result.productsCreated++;
45
+ for (const image of productDraft.images ?? []) {
46
+ const hasImage = existingProduct?.images?.some((candidate) => candidate.url === image.url);
47
+ if (!hasImage) {
48
+ await brandMap.product.addImage(product.id, image.url, image.rank ?? undefined);
49
+ result.imagesAdded++;
50
+ }
51
+ }
52
+ for (const attribute of productDraft.attributes ?? []) {
53
+ await upsertAttributeWithValue(brandMap, product.id, null, attribute);
54
+ result.attributesUpserted++;
55
+ }
56
+ for (const variantDraft of productDraft.variants) {
57
+ const existingVariant = await brandMap.product.retrieveVariantByExternalSku(variantDraft.externalSku);
58
+ const variant = existingVariant ? await brandMap.product.updateVariant(existingVariant.id, {
59
+ externalSku: variantDraft.externalSku,
60
+ manageInventory: variantDraft.manageInventory ?? undefined,
61
+ title: variantDraft.title ?? undefined
62
+ }) : await brandMap.product.createVariant(product.id, {
63
+ externalSku: variantDraft.externalSku,
64
+ manageInventory: variantDraft.manageInventory ?? undefined,
65
+ title: variantDraft.title ?? variantDraft.externalSku
66
+ });
67
+ const variantId = readVariantId(variant) ?? existingVariant?.id ?? (await brandMap.product.retrieveVariantByExternalSku(variantDraft.externalSku))?.id;
68
+ if (!variantId) {
69
+ throw new Error(`Failed to persist variant "${variantDraft.externalSku}".`);
70
+ }
71
+ if (existingVariant)
72
+ result.variantsUpdated++;
73
+ else
74
+ result.variantsCreated++;
75
+ for (const [optionKey, optionValue] of Object.entries(variantDraft.options ?? {})) {
76
+ const option = await brandMap.product.upsertOption(product.id, {
77
+ rank: optionValue.rank ?? undefined,
78
+ title: optionValue.title ?? toTitle(optionKey)
79
+ });
80
+ const value = await brandMap.product.upsertOptionValue(option.id, {
81
+ colorHex: optionValue.colorHex ?? undefined,
82
+ rank: optionValue.rank ?? undefined,
83
+ value: optionValue.value
84
+ });
85
+ await brandMap.product.linkVariantOptionValue(variantId, value.id);
86
+ result.variantOptionValuesLinked++;
87
+ }
88
+ for (const attribute of variantDraft.attributes ?? []) {
89
+ await upsertAttributeWithValue(brandMap, product.id, variantId, attribute);
90
+ result.attributesUpserted++;
91
+ }
92
+ const basePrice = variantDraft.prices?.base;
93
+ if (basePrice?.amount !== undefined) {
94
+ await brandMap.price.setBasePrice({
95
+ amount: basePrice.amount,
96
+ compareAtAmount: basePrice.compareAtAmount ?? undefined,
97
+ costAmount: basePrice.costAmount ?? undefined,
98
+ currencyCode: basePrice.currencyCode ?? "RUB",
99
+ taxIncluded: true,
100
+ variantId
101
+ });
102
+ result.pricesSet++;
103
+ }
104
+ for (const price of variantDraft.prices?.lists ?? []) {
105
+ const priceList = await upsertPriceList(brandMap, document, price);
106
+ await upsertPriceListPrice(brandMap, priceList.id, variantId, price);
107
+ result.priceListsUpserted++;
108
+ result.priceListPricesSet++;
109
+ }
110
+ for (const inventory of variantDraft.inventory ?? []) {
111
+ const storeId = inventory.storeId ?? document.storeId;
112
+ if (!storeId) {
113
+ throw new Error(`Inventory for "${variantDraft.externalSku}" requires storeId.`);
114
+ }
115
+ const stockLocation = await brandMap.stockLocation.upsertStockLocation({
116
+ code: inventory.stockLocationCode,
117
+ isEnabled: true,
118
+ metadata: { sourceSystem: document.source },
119
+ name: inventory.stockLocationName ?? inventory.stockLocationCode,
120
+ storeId,
121
+ type: "warehouse"
122
+ });
123
+ const item = await brandMap.inventory.enrollVariantInStore({
124
+ isManaged: true,
125
+ metadata: { sourceSystem: document.source },
126
+ sku: inventory.sku ?? variantDraft.externalSku,
127
+ storeId,
128
+ variantId
129
+ });
130
+ await brandMap.inventory.syncStockLevel({
131
+ inventoryItemId: item.id,
132
+ sourceSystem: document.source,
133
+ stockedQuantity: inventory.stockedQuantity,
134
+ stockLocationId: stockLocation.id
135
+ });
136
+ result.inventorySynced++;
137
+ }
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ async function upsertAttributeWithValue(brandMap, productId, variantId, attributeDraft) {
143
+ const attribute = await brandMap.product.upsertAttribute(productId, {
144
+ isFilterable: attributeDraft.isFilterable ?? undefined,
145
+ isSearchable: attributeDraft.isSearchable ?? undefined,
146
+ key: attributeDraft.key,
147
+ label: attributeDraft.label ?? toTitle(attributeDraft.key),
148
+ rank: attributeDraft.rank ?? undefined,
149
+ type: attributeDraft.type ?? inferAttributeType(attributeDraft.value)
150
+ });
151
+ const valueInput = buildAttributeValueInput(attribute.id, productId, variantId, attributeDraft.value);
152
+ await brandMap.product.upsertAttributeValue(valueInput);
153
+ }
154
+ function buildAttributeValueInput(attributeId, productId, variantId, value) {
155
+ return {
156
+ attributeId,
157
+ productId: variantId ? null : productId,
158
+ valueBoolean: typeof value === "boolean" ? value : null,
159
+ valueNumber: typeof value === "number" ? value : null,
160
+ valueText: typeof value === "string" ? value : null,
161
+ variantId
162
+ };
163
+ }
164
+ async function upsertPriceList(brandMap, document, price) {
165
+ const storeId = document.storeId;
166
+ if (!storeId) {
167
+ throw new Error(`Price list "${price.key}" requires storeId.`);
168
+ }
169
+ const title = price.title ?? toTitle(price.key);
170
+ const existing = await brandMap.price.listPriceList({
171
+ limit: 1,
172
+ where: { $and: [{ storeId: { $eq: storeId } }, { title: { $eq: title } }] }
173
+ });
174
+ const current = existing.data[0];
175
+ if (current) {
176
+ const updated = await brandMap.price.updatePriceList({
177
+ id: current.id,
178
+ metadata: { key: price.key, sourceSystem: document.source },
179
+ status: "active",
180
+ title,
181
+ type: "override"
182
+ });
183
+ return updated ?? current;
184
+ }
185
+ return await brandMap.price.createPriceList({
186
+ automaticallyApplies: true,
187
+ metadata: { key: price.key, sourceSystem: document.source },
188
+ priority: 0,
189
+ status: "active",
190
+ storeId,
191
+ title,
192
+ type: "override"
193
+ });
194
+ }
195
+ async function upsertPriceListPrice(brandMap, priceListId, variantId, price) {
196
+ const currencyCode = price.currencyCode ?? "RUB";
197
+ const existing = await brandMap.price.listPriceListPrice({
198
+ limit: 1,
199
+ where: {
200
+ $and: [
201
+ { priceListId: { $eq: priceListId } },
202
+ { variantId: { $eq: variantId } },
203
+ { currencyCode: { $eq: currencyCode } }
204
+ ]
205
+ }
206
+ });
207
+ const current = existing.data[0];
208
+ if (current) {
209
+ await brandMap.price.updatePriceListPrice({
210
+ amount: price.amount,
211
+ compareAtAmount: price.compareAtAmount ?? undefined,
212
+ currencyCode,
213
+ id: current.id,
214
+ metadata: { key: price.key },
215
+ priceListId,
216
+ variantId
217
+ });
218
+ return;
219
+ }
220
+ await brandMap.price.createPriceListPrice({
221
+ amount: price.amount,
222
+ compareAtAmount: price.compareAtAmount ?? undefined,
223
+ currencyCode,
224
+ metadata: { key: price.key },
225
+ priceListId,
226
+ variantId
227
+ });
228
+ }
229
+ async function findProductBySlug(brandMap, slug) {
230
+ const result = await brandMap.product.list({
231
+ joins: { images: { select: ["id", "url", "rank"] } },
232
+ limit: 1,
233
+ where: { slug: { $eq: slug } }
234
+ });
235
+ return result.data[0] ?? null;
236
+ }
237
+ async function findExistingProduct(brandMap, productDraft) {
238
+ const productBySlug = await findProductBySlug(brandMap, productDraft.slug);
239
+ if (productBySlug)
240
+ return productBySlug;
241
+ for (const variantDraft of productDraft.variants) {
242
+ const existingVariant = await brandMap.product.retrieveVariantByExternalSku(variantDraft.externalSku);
243
+ if (!existingVariant)
244
+ continue;
245
+ return await brandMap.product.retrieve(existingVariant.productId, {
246
+ joins: { images: { select: ["id", "url", "rank"] } }
247
+ });
248
+ }
249
+ return null;
250
+ }
251
+ function mergeMetadata(metadata, source) {
252
+ return {
253
+ ...metadata,
254
+ sourceSystem: source
255
+ };
256
+ }
257
+ function readVariantId(value) {
258
+ return value && "productId" in value ? value.id : null;
259
+ }
260
+ function inferAttributeType(value) {
261
+ if (typeof value === "boolean")
262
+ return "boolean";
263
+ if (typeof value === "number")
264
+ return "number";
265
+ return isColorHex(value) ? "color" : "string";
266
+ }
267
+ function isColorHex(value) {
268
+ return /^#[0-9a-f]{6}$/i.test(value);
269
+ }
270
+ function toTitle(value) {
271
+ return value.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (letter) => letter.toUpperCase());
272
+ }
273
+ export {
274
+ applyExchangeDocument
275
+ };
@@ -0,0 +1,116 @@
1
+ import type { ExtensionBrandMapOperations, ExtensionJson } from "@brand-map/extension-sdk/types";
2
+
3
+ type ExchangeAttributeType = "string" | "number" | "boolean" | "enum" | "color";
4
+
5
+ type ExchangeAttributeValue = {
6
+ isFilterable?: boolean | null;
7
+ isSearchable?: boolean | null;
8
+ key: string;
9
+ label?: string | null;
10
+ rank?: number | null;
11
+ type?: ExchangeAttributeType | null;
12
+ value: boolean | number | string;
13
+ };
14
+
15
+ type ExchangeOptionValue = {
16
+ colorHex?: string | null;
17
+ rank?: number | null;
18
+ title?: string | null;
19
+ value: string;
20
+ };
21
+
22
+ type ExchangePrice = {
23
+ amount: number;
24
+ compareAtAmount?: number | null;
25
+ costAmount?: number | null;
26
+ currencyCode?: string | null;
27
+ };
28
+
29
+ type ExchangePriceListPrice = ExchangePrice & {
30
+ key: string;
31
+ title?: string | null;
32
+ };
33
+
34
+ type ExchangeInventoryLevel = {
35
+ sku?: string | null;
36
+ stockLocationCode: string;
37
+ stockLocationName?: string | null;
38
+ stockedQuantity: number;
39
+ storeId?: string | null;
40
+ };
41
+
42
+ type ExchangeImage = {
43
+ rank?: number | null;
44
+ url: string;
45
+ };
46
+
47
+ type ExchangeVariant = {
48
+ attributes?: ExchangeAttributeValue[];
49
+ externalSku: string;
50
+ inventory?: ExchangeInventoryLevel[];
51
+ isActive?: boolean | null;
52
+ manageInventory?: boolean | null;
53
+ metadata?: Record<string, ExtensionJson> | null;
54
+ options?: Record<string, ExchangeOptionValue>;
55
+ prices?: {
56
+ base?: ExchangePrice | null;
57
+ lists?: ExchangePriceListPrice[];
58
+ };
59
+ title?: string | null;
60
+ };
61
+
62
+ type ExchangeProduct = {
63
+ attributes?: ExchangeAttributeValue[];
64
+ categoryId?: string | null;
65
+ description?: string | null;
66
+ images?: ExchangeImage[];
67
+ metadata?: Record<string, ExtensionJson> | null;
68
+ slug: string;
69
+ status?: string | null;
70
+ subtitle?: string | null;
71
+ thumbnail?: string | null;
72
+ title: string;
73
+ variants: ExchangeVariant[];
74
+ };
75
+
76
+ type ExchangeDocument = {
77
+ products: ExchangeProduct[];
78
+ source: string;
79
+ storeId?: string | null;
80
+ };
81
+
82
+ type ExchangeApplyResult = {
83
+ attributesUpserted: number;
84
+ imagesAdded: number;
85
+ inventorySynced: number;
86
+ priceListPricesSet: number;
87
+ priceListsUpserted: number;
88
+ pricesSet: number;
89
+ productsCreated: number;
90
+ productsUpdated: number;
91
+ rowsProcessed: number;
92
+ source: string;
93
+ variantOptionValuesLinked: number;
94
+ variantsCreated: number;
95
+ variantsUpdated: number;
96
+ };
97
+
98
+ declare function applyExchangeDocument(
99
+ brandMap: ExtensionBrandMapOperations,
100
+ document: ExchangeDocument,
101
+ ): Promise<ExchangeApplyResult>;
102
+
103
+ export { applyExchangeDocument };
104
+ export type {
105
+ ExchangeApplyResult,
106
+ ExchangeAttributeType,
107
+ ExchangeAttributeValue,
108
+ ExchangeDocument,
109
+ ExchangeImage,
110
+ ExchangeInventoryLevel,
111
+ ExchangeOptionValue,
112
+ ExchangePrice,
113
+ ExchangePriceListPrice,
114
+ ExchangeProduct,
115
+ ExchangeVariant,
116
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@brand-map/exchange-core",
3
+ "version": "0.0.10-alpha.10",
4
+ "type": "module",
5
+ "main": "./dist/exports.js",
6
+ "module": "./dist/exports.js",
7
+ "types": "./dist/types/exports.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/exports.js",
11
+ "types": "./dist/types/exports.d.ts"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org"
20
+ },
21
+ "dependencies": {
22
+ "@brand-map/extension-sdk": "0.0.10-alpha.7"
23
+ }
24
+ }