@deenruv/merchant-plugin 1.0.17-dev.17 → 1.0.17-dev.20
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/README.md +14 -0
- package/dist/plugin-server/api/platform-integration-admin-resolver.d.ts +20 -1
- package/dist/plugin-server/api/platform-integration-admin-resolver.js +44 -19
- package/dist/plugin-server/entities/merchant-sync-item.entity.d.ts +14 -0
- package/dist/plugin-server/entities/merchant-sync-item.entity.js +56 -0
- package/dist/plugin-server/entities/merchant-sync-run.entity.d.ts +19 -0
- package/dist/plugin-server/entities/merchant-sync-run.entity.js +72 -0
- package/dist/plugin-server/extensions/api-extensions.js +53 -2
- package/dist/plugin-server/index.js +10 -1
- package/dist/plugin-server/services/google-merchant-api.d.ts +48 -6
- package/dist/plugin-server/services/google-merchant-api.js +247 -32
- package/dist/plugin-server/services/google-platform-integration.service.d.ts +8 -7
- package/dist/plugin-server/services/google-platform-integration.service.js +79 -6
- package/dist/plugin-server/services/merchant-sync-history.service.d.ts +25 -0
- package/dist/plugin-server/services/merchant-sync-history.service.js +97 -0
- package/dist/plugin-server/services/platform-integration.service.d.ts +6 -2
- package/dist/plugin-server/services/platform-integration.service.js +53 -29
- package/dist/plugin-server/services/subscriber.service.d.ts +32 -4
- package/dist/plugin-server/services/subscriber.service.js +141 -23
- package/dist/plugin-server/ui/graphql/mutations.ts +7 -0
- package/dist/plugin-server/ui/graphql/queries.ts +99 -0
- package/dist/plugin-server/ui/pages/GooglePage.tsx +158 -16
- package/dist/plugin-ui/graphql/mutations.d.ts +5 -0
- package/dist/plugin-ui/graphql/mutations.js +6 -0
- package/dist/plugin-ui/graphql/queries.d.ts +53 -2
- package/dist/plugin-ui/graphql/queries.js +49 -0
- package/dist/plugin-ui/pages/GooglePage.js +51 -13
- package/package.json +8 -7
|
@@ -6,19 +6,90 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
|
6
6
|
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
7
7
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
8
8
|
};
|
|
9
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
10
|
+
var t = {};
|
|
11
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
12
|
+
t[p] = s[p];
|
|
13
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
14
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
15
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
16
|
+
t[p[i]] = s[p[i]];
|
|
17
|
+
}
|
|
18
|
+
return t;
|
|
19
|
+
};
|
|
9
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.selectRemoteOrphanProducts = exports.collectGoogleProductsForDataSource = exports.summarizeMerchantOperations = exports.executeGoogleWriteOperations = exports.runBoundedMerchantOperations = exports.createGoogleDeleteOperation = exports.createGoogleUpdateOperation = exports.createGoogleInsertOperation = exports.buildGoogleUpdateMask = exports.toGoogleProductInput = exports.buildGoogleProductInputName = exports.buildGoogleProductName = exports.buildGoogleProductIdentifier = exports.createGoogleMerchantClients = exports.parseGoogleMerchantSettings = exports.normalizeDataSource = exports.normalizeMerchantId = exports.parseGoogleCredentials = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = exports.GOOGLE_FEED_LABEL = exports.GOOGLE_CONTENT_LANGUAGE = void 0;
|
|
21
|
+
exports.selectRemoteOrphanProducts = exports.diagnoseGoogleMerchantConnection = exports.collectGoogleProductsForDataSource = exports.summarizeMerchantOperations = exports.executeGoogleWriteOperations = exports.runBoundedMerchantOperations = exports.createGoogleDeleteOperation = exports.createGoogleUpdateOperation = exports.createGoogleInsertOperation = exports.buildGoogleUpdateMask = exports.validateGoogleProductUrls = exports.toGoogleProductInput = exports.buildGoogleProductInputName = exports.buildGoogleProductName = exports.buildGoogleProductIdentifier = exports.createGoogleMerchantClients = exports.parseGoogleMerchantSettings = exports.normalizeDataSource = exports.normalizeMerchantId = exports.parseGoogleCredentials = exports.normalizeGoogleMerchantError = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = exports.GOOGLE_FEED_LABEL = exports.GOOGLE_CONTENT_LANGUAGE = void 0;
|
|
11
22
|
const products_1 = require("@google-shopping/products");
|
|
23
|
+
const datasources_1 = require("@google-shopping/datasources");
|
|
12
24
|
const GOOGLE_OAUTH_SCOPE = "https://www.googleapis.com/auth/content";
|
|
13
25
|
const PLAIN_IDENTIFIER_COMPONENT = /^[A-Za-z0-9_-]+$/;
|
|
14
26
|
const MERCHANT_ID_PATTERN = /^[1-9]\d*$/;
|
|
15
27
|
const DATA_SOURCE_PATTERN = /^accounts\/([1-9]\d*)\/dataSources\/([1-9]\d*)$/;
|
|
28
|
+
const CONTENT_LANGUAGE_PATTERN = /^[a-z]{2}$/;
|
|
29
|
+
const FEED_LABEL_PATTERN = /^[A-Z0-9_-]{1,20}$/;
|
|
16
30
|
exports.GOOGLE_CONTENT_LANGUAGE = "pl";
|
|
17
31
|
exports.GOOGLE_FEED_LABEL = "PL";
|
|
18
32
|
exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
|
|
33
|
+
class MerchantOperationAttemptError {
|
|
34
|
+
constructor(cause, attempts) {
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
this.attempts = attempts;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
function isRecord(value) {
|
|
20
40
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
41
|
}
|
|
42
|
+
function normalizeGoogleMerchantError(error) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const candidate = isRecord(error) ? error : {};
|
|
45
|
+
const numericCode = typeof candidate.code === "number"
|
|
46
|
+
? candidate.code
|
|
47
|
+
: Number.parseInt(String((_a = candidate.code) !== null && _a !== void 0 ? _a : ""), 10);
|
|
48
|
+
const codeMap = {
|
|
49
|
+
7: {
|
|
50
|
+
code: "PERMISSION_DENIED",
|
|
51
|
+
retryable: false,
|
|
52
|
+
status: "PERMISSION_DENIED",
|
|
53
|
+
},
|
|
54
|
+
5: {
|
|
55
|
+
code: "NOT_FOUND",
|
|
56
|
+
retryable: false,
|
|
57
|
+
status: "DATA_SOURCE_NOT_FOUND",
|
|
58
|
+
},
|
|
59
|
+
8: {
|
|
60
|
+
code: "RESOURCE_EXHAUSTED",
|
|
61
|
+
retryable: true,
|
|
62
|
+
status: "UNAVAILABLE",
|
|
63
|
+
},
|
|
64
|
+
13: { code: "INTERNAL", retryable: true, status: "UNAVAILABLE" },
|
|
65
|
+
14: { code: "UNAVAILABLE", retryable: true, status: "UNAVAILABLE" },
|
|
66
|
+
16: {
|
|
67
|
+
code: "UNAUTHENTICATED",
|
|
68
|
+
retryable: false,
|
|
69
|
+
status: "AUTHENTICATION_FAILED",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const classification = (_b = codeMap[numericCode]) !== null && _b !== void 0 ? _b : {
|
|
73
|
+
code: typeof candidate.code === "string"
|
|
74
|
+
? candidate.code
|
|
75
|
+
: Number.isFinite(numericCode)
|
|
76
|
+
? String(numericCode)
|
|
77
|
+
: "UNKNOWN",
|
|
78
|
+
retryable: false,
|
|
79
|
+
status: "ERROR",
|
|
80
|
+
};
|
|
81
|
+
const rawMessage = typeof candidate.details === "string"
|
|
82
|
+
? candidate.details
|
|
83
|
+
: error instanceof Error
|
|
84
|
+
? error.message
|
|
85
|
+
: "Unknown Google Merchant error";
|
|
86
|
+
const message = rawMessage
|
|
87
|
+
.replace(/-----BEGIN [^-]+ PRIVATE KEY-----[\s\S]*?-----END [^-]+ PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]")
|
|
88
|
+
.replace(/(access_token|refresh_token|client_secret)=([^&\s]+)/gi, "$1=[REDACTED]")
|
|
89
|
+
.slice(0, 2000);
|
|
90
|
+
return Object.assign(Object.assign({}, classification), { message });
|
|
91
|
+
}
|
|
92
|
+
exports.normalizeGoogleMerchantError = normalizeGoogleMerchantError;
|
|
22
93
|
function optionalString(value, key) {
|
|
23
94
|
const candidate = value[key];
|
|
24
95
|
return typeof candidate === "string" && candidate.length > 0
|
|
@@ -84,7 +155,7 @@ function normalizeDataSource(rawDataSource, merchantId) {
|
|
|
84
155
|
}
|
|
85
156
|
exports.normalizeDataSource = normalizeDataSource;
|
|
86
157
|
function parseGoogleMerchantSettings(entries) {
|
|
87
|
-
var _a, _b, _c, _d, _e;
|
|
158
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
88
159
|
const getValue = (key) => { var _a; return (_a = entries.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.value; };
|
|
89
160
|
const accountId = normalizeMerchantId((_a = getValue("merchantId")) !== null && _a !== void 0 ? _a : "");
|
|
90
161
|
const dataSource = normalizeDataSource((_b = getValue("dataSource")) !== null && _b !== void 0 ? _b : "", accountId);
|
|
@@ -93,12 +164,26 @@ function parseGoogleMerchantSettings(entries) {
|
|
|
93
164
|
throw new Error("Google brand is required");
|
|
94
165
|
}
|
|
95
166
|
const credentials = parseGoogleCredentials((_d = getValue("credentials")) !== null && _d !== void 0 ? _d : "");
|
|
167
|
+
const contentLanguage = ((_e = getValue("contentLanguage")) !== null && _e !== void 0 ? _e : exports.GOOGLE_CONTENT_LANGUAGE)
|
|
168
|
+
.trim()
|
|
169
|
+
.toLowerCase();
|
|
170
|
+
if (!CONTENT_LANGUAGE_PATTERN.test(contentLanguage)) {
|
|
171
|
+
throw new Error("Google contentLanguage must be a two-letter language code");
|
|
172
|
+
}
|
|
173
|
+
const feedLabel = ((_f = getValue("feedLabel")) !== null && _f !== void 0 ? _f : exports.GOOGLE_FEED_LABEL)
|
|
174
|
+
.trim()
|
|
175
|
+
.toUpperCase();
|
|
176
|
+
if (!FEED_LABEL_PATTERN.test(feedLabel)) {
|
|
177
|
+
throw new Error("Google feedLabel must contain 1-20 uppercase letters, numbers, hyphens, or underscores");
|
|
178
|
+
}
|
|
96
179
|
return {
|
|
97
180
|
accountId,
|
|
98
|
-
autoUpdate: ((
|
|
181
|
+
autoUpdate: ((_g = getValue("autoUpdate")) !== null && _g !== void 0 ? _g : "").toLowerCase() === "true",
|
|
99
182
|
brand,
|
|
100
183
|
credentials,
|
|
101
184
|
dataSource,
|
|
185
|
+
contentLanguage,
|
|
186
|
+
feedLabel,
|
|
102
187
|
};
|
|
103
188
|
}
|
|
104
189
|
exports.parseGoogleMerchantSettings = parseGoogleMerchantSettings;
|
|
@@ -108,38 +193,67 @@ function createGoogleMerchantClients(credentials) {
|
|
|
108
193
|
scopes: [GOOGLE_OAUTH_SCOPE],
|
|
109
194
|
};
|
|
110
195
|
return {
|
|
196
|
+
dataSources: new datasources_1.v1.DataSourcesServiceClient(options),
|
|
111
197
|
productInputs: new products_1.v1.ProductInputsServiceClient(options),
|
|
112
198
|
products: new products_1.v1.ProductsServiceClient(options),
|
|
113
199
|
};
|
|
114
200
|
}
|
|
115
201
|
exports.createGoogleMerchantClients = createGoogleMerchantClients;
|
|
116
|
-
function buildGoogleProductIdentifier(offerId) {
|
|
202
|
+
function buildGoogleProductIdentifier(offerId, contentLanguage = exports.GOOGLE_CONTENT_LANGUAGE, feedLabel = exports.GOOGLE_FEED_LABEL) {
|
|
117
203
|
if (!offerId) {
|
|
118
204
|
throw new Error("Google offer ID is required");
|
|
119
205
|
}
|
|
120
|
-
const components = [
|
|
206
|
+
const components = [contentLanguage, feedLabel, offerId];
|
|
121
207
|
const identifier = components.join("~");
|
|
122
208
|
return components.every((component) => PLAIN_IDENTIFIER_COMPONENT.test(component))
|
|
123
209
|
? identifier
|
|
124
210
|
: Buffer.from(identifier, "utf8").toString("base64url");
|
|
125
211
|
}
|
|
126
212
|
exports.buildGoogleProductIdentifier = buildGoogleProductIdentifier;
|
|
127
|
-
function buildGoogleProductName(accountId, offerId) {
|
|
128
|
-
return `accounts/${accountId}/products/${buildGoogleProductIdentifier(offerId)}`;
|
|
213
|
+
function buildGoogleProductName(accountId, offerId, contentLanguage = exports.GOOGLE_CONTENT_LANGUAGE, feedLabel = exports.GOOGLE_FEED_LABEL) {
|
|
214
|
+
return `accounts/${accountId}/products/${buildGoogleProductIdentifier(offerId, contentLanguage, feedLabel)}`;
|
|
129
215
|
}
|
|
130
216
|
exports.buildGoogleProductName = buildGoogleProductName;
|
|
131
|
-
function buildGoogleProductInputName(accountId, offerId) {
|
|
132
|
-
return `accounts/${accountId}/productInputs/${buildGoogleProductIdentifier(offerId)}`;
|
|
217
|
+
function buildGoogleProductInputName(accountId, offerId, contentLanguage = exports.GOOGLE_CONTENT_LANGUAGE, feedLabel = exports.GOOGLE_FEED_LABEL) {
|
|
218
|
+
return `accounts/${accountId}/productInputs/${buildGoogleProductIdentifier(offerId, contentLanguage, feedLabel)}`;
|
|
133
219
|
}
|
|
134
220
|
exports.buildGoogleProductInputName = buildGoogleProductInputName;
|
|
135
|
-
function toGoogleProductInput(product, brand) {
|
|
136
|
-
return Object.assign(Object.assign({ offerId: String(product.communicateID), contentLanguage
|
|
221
|
+
function toGoogleProductInput(product, brand, contentLanguage = exports.GOOGLE_CONTENT_LANGUAGE, feedLabel = exports.GOOGLE_FEED_LABEL) {
|
|
222
|
+
return Object.assign(Object.assign({ offerId: String(product.communicateID), contentLanguage,
|
|
223
|
+
feedLabel, productAttributes: Object.assign(Object.assign({}, product.productAttributes), { brand }) }, (product.customAttributes && {
|
|
137
224
|
customAttributes: product.customAttributes,
|
|
138
225
|
})), (product.versionNumber !== undefined && {
|
|
139
226
|
versionNumber: product.versionNumber,
|
|
140
227
|
}));
|
|
141
228
|
}
|
|
142
229
|
exports.toGoogleProductInput = toGoogleProductInput;
|
|
230
|
+
function validateGoogleProductUrls(product) {
|
|
231
|
+
var _a;
|
|
232
|
+
const attributes = product.productAttributes;
|
|
233
|
+
const urls = [
|
|
234
|
+
["link", attributes.link],
|
|
235
|
+
["imageLink", attributes.imageLink],
|
|
236
|
+
...((_a = attributes.additionalImageLinks) !== null && _a !== void 0 ? _a : []).map((url, index) => [
|
|
237
|
+
`additionalImageLinks[${index}]`,
|
|
238
|
+
url,
|
|
239
|
+
]),
|
|
240
|
+
];
|
|
241
|
+
for (const [field, value] of urls) {
|
|
242
|
+
if (!value)
|
|
243
|
+
continue;
|
|
244
|
+
let parsed;
|
|
245
|
+
try {
|
|
246
|
+
parsed = new URL(value);
|
|
247
|
+
}
|
|
248
|
+
catch (_b) {
|
|
249
|
+
throw new Error(`Google product ${product.communicateID} ${field} must be an absolute http(s) URL`);
|
|
250
|
+
}
|
|
251
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
252
|
+
throw new Error(`Google product ${product.communicateID} ${field} must be an absolute http(s) URL`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.validateGoogleProductUrls = validateGoogleProductUrls;
|
|
143
257
|
function camelToSnake(value) {
|
|
144
258
|
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
145
259
|
}
|
|
@@ -167,15 +281,15 @@ function createGoogleInsertOperation(product, settings) {
|
|
|
167
281
|
request: {
|
|
168
282
|
parent: `accounts/${settings.accountId}`,
|
|
169
283
|
dataSource: settings.dataSource,
|
|
170
|
-
productInput: toGoogleProductInput(product, settings.brand),
|
|
284
|
+
productInput: toGoogleProductInput(product, settings.brand, settings.contentLanguage, settings.feedLabel),
|
|
171
285
|
},
|
|
172
286
|
};
|
|
173
287
|
}
|
|
174
288
|
exports.createGoogleInsertOperation = createGoogleInsertOperation;
|
|
175
289
|
function createGoogleUpdateOperation(product, settings) {
|
|
176
290
|
const communicateID = String(product.communicateID);
|
|
177
|
-
const input = toGoogleProductInput(product, settings.brand);
|
|
178
|
-
const productInput = Object.assign({ name: buildGoogleProductInputName(settings.accountId, communicateID), productAttributes: input.productAttributes }, (input.customAttributes && {
|
|
291
|
+
const input = toGoogleProductInput(product, settings.brand, settings.contentLanguage, settings.feedLabel);
|
|
292
|
+
const productInput = Object.assign({ name: buildGoogleProductInputName(settings.accountId, communicateID, settings.contentLanguage, settings.feedLabel), productAttributes: input.productAttributes }, (input.customAttributes && {
|
|
179
293
|
customAttributes: input.customAttributes,
|
|
180
294
|
}));
|
|
181
295
|
return {
|
|
@@ -194,7 +308,7 @@ function createGoogleDeleteOperation(communicateID, settings) {
|
|
|
194
308
|
communicateID,
|
|
195
309
|
method: "delete",
|
|
196
310
|
request: {
|
|
197
|
-
name: buildGoogleProductInputName(settings.accountId, communicateID),
|
|
311
|
+
name: buildGoogleProductInputName(settings.accountId, communicateID, settings.contentLanguage, settings.feedLabel),
|
|
198
312
|
dataSource: settings.dataSource,
|
|
199
313
|
},
|
|
200
314
|
};
|
|
@@ -217,11 +331,22 @@ async function runBoundedMerchantOperations(items, operation, concurrency = expo
|
|
|
217
331
|
return;
|
|
218
332
|
const item = items[index];
|
|
219
333
|
try {
|
|
220
|
-
await operation(item, index);
|
|
221
|
-
results[index] = {
|
|
334
|
+
const attempts = await operation(item, index);
|
|
335
|
+
results[index] = {
|
|
336
|
+
attempts: attempts !== null && attempts !== void 0 ? attempts : 1,
|
|
337
|
+
index,
|
|
338
|
+
item,
|
|
339
|
+
status: "success",
|
|
340
|
+
};
|
|
222
341
|
}
|
|
223
342
|
catch (error) {
|
|
224
|
-
results[index] = {
|
|
343
|
+
results[index] = {
|
|
344
|
+
attempts: error instanceof MerchantOperationAttemptError ? error.attempts : 1,
|
|
345
|
+
error: error instanceof MerchantOperationAttemptError ? error.cause : error,
|
|
346
|
+
index,
|
|
347
|
+
item,
|
|
348
|
+
status: "error",
|
|
349
|
+
};
|
|
225
350
|
}
|
|
226
351
|
}
|
|
227
352
|
});
|
|
@@ -237,24 +362,38 @@ function isNotFoundError(error) {
|
|
|
237
362
|
const response = error.response;
|
|
238
363
|
return isRecord(response) && response.status === 404;
|
|
239
364
|
}
|
|
240
|
-
async function executeGoogleWriteOperations(client, operations, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY) {
|
|
365
|
+
async function executeGoogleWriteOperations(client, operations, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY, retryOptions = {}) {
|
|
366
|
+
var _a, _b;
|
|
367
|
+
const maxAttempts = (_a = retryOptions.maxAttempts) !== null && _a !== void 0 ? _a : 3;
|
|
368
|
+
const delay = (_b = retryOptions.delay) !== null && _b !== void 0 ? _b : (async (attempt) => {
|
|
369
|
+
const exponentialDelay = 250 * 2 ** (attempt - 1);
|
|
370
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, exponentialDelay + jitter));
|
|
372
|
+
});
|
|
241
373
|
return runBoundedMerchantOperations(operations, async (operation) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
374
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
375
|
+
try {
|
|
376
|
+
if (operation.method === "insert") {
|
|
377
|
+
await client.insertProductInput(operation.request);
|
|
378
|
+
}
|
|
379
|
+
else if (operation.method === "update") {
|
|
380
|
+
await client.updateProductInput(operation.request);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
await client.deleteProductInput(operation.request);
|
|
384
|
+
}
|
|
385
|
+
return attempt;
|
|
248
386
|
}
|
|
249
|
-
|
|
250
|
-
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (operation.method === "delete" && isNotFoundError(error))
|
|
389
|
+
return;
|
|
390
|
+
const normalized = normalizeGoogleMerchantError(error);
|
|
391
|
+
if (!normalized.retryable || attempt === maxAttempts) {
|
|
392
|
+
throw new MerchantOperationAttemptError(error, attempt);
|
|
393
|
+
}
|
|
394
|
+
await delay(attempt);
|
|
251
395
|
}
|
|
252
396
|
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
if (operation.method === "delete" && isNotFoundError(error))
|
|
255
|
-
return;
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
397
|
}, concurrency);
|
|
259
398
|
}
|
|
260
399
|
exports.executeGoogleWriteOperations = executeGoogleWriteOperations;
|
|
@@ -297,6 +436,82 @@ async function collectGoogleProductsForDataSource(client, accountId, dataSource)
|
|
|
297
436
|
return [...products.values()];
|
|
298
437
|
}
|
|
299
438
|
exports.collectGoogleProductsForDataSource = collectGoogleProductsForDataSource;
|
|
439
|
+
async function diagnoseGoogleMerchantConnection(client, settings, dataSourcesClient) {
|
|
440
|
+
var _a, e_2, _b, _c;
|
|
441
|
+
var _d, _e, _f, _g, _h, _j, _k;
|
|
442
|
+
const startedAt = Date.now();
|
|
443
|
+
const issues = [];
|
|
444
|
+
let productsCount = 0;
|
|
445
|
+
let disapprovedProductsCount = 0;
|
|
446
|
+
try {
|
|
447
|
+
if (dataSourcesClient) {
|
|
448
|
+
await dataSourcesClient.getDataSource({ name: settings.dataSource });
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
for (var _l = true, _m = __asyncValues(client.listProductsAsync({
|
|
452
|
+
parent: `accounts/${settings.accountId}`,
|
|
453
|
+
pageSize: 1000,
|
|
454
|
+
})), _o; _o = await _m.next(), _a = _o.done, !_a; _l = true) {
|
|
455
|
+
_c = _o.value;
|
|
456
|
+
_l = false;
|
|
457
|
+
const product = _c;
|
|
458
|
+
if (product.dataSource !== settings.dataSource)
|
|
459
|
+
continue;
|
|
460
|
+
productsCount += 1;
|
|
461
|
+
let isDisapproved = false;
|
|
462
|
+
for (const issue of (_e = (_d = product.productStatus) === null || _d === void 0 ? void 0 : _d.itemLevelIssues) !== null && _e !== void 0 ? _e : []) {
|
|
463
|
+
const severity = typeof issue.severity === "string"
|
|
464
|
+
? issue.severity
|
|
465
|
+
: issue.severity === 3
|
|
466
|
+
? "DISAPPROVED"
|
|
467
|
+
: String((_f = issue.severity) !== null && _f !== void 0 ? _f : "SEVERITY_UNSPECIFIED");
|
|
468
|
+
if (severity === "DISAPPROVED")
|
|
469
|
+
isDisapproved = true;
|
|
470
|
+
issues.push({
|
|
471
|
+
code: (_g = issue.code) !== null && _g !== void 0 ? _g : "unknown",
|
|
472
|
+
description: (_j = (_h = issue.description) !== null && _h !== void 0 ? _h : issue.detail) !== null && _j !== void 0 ? _j : "Unknown product issue",
|
|
473
|
+
offerId: (_k = product.offerId) !== null && _k !== void 0 ? _k : "unknown",
|
|
474
|
+
severity,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (isDisapproved)
|
|
478
|
+
disapprovedProductsCount += 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
482
|
+
finally {
|
|
483
|
+
try {
|
|
484
|
+
if (!_l && !_a && (_b = _m.return)) await _b.call(_m);
|
|
485
|
+
}
|
|
486
|
+
finally { if (e_2) throw e_2.error; }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const _p = normalizeGoogleMerchantError(error), { status } = _p, lastError = __rest(_p, ["status"]);
|
|
491
|
+
return {
|
|
492
|
+
checkedAt: new Date().toISOString(),
|
|
493
|
+
connectionStatus: status,
|
|
494
|
+
dataSourceVerified: false,
|
|
495
|
+
disapprovedProductsCount,
|
|
496
|
+
issues,
|
|
497
|
+
issuesCount: issues.length,
|
|
498
|
+
lastError,
|
|
499
|
+
latencyMs: Date.now() - startedAt,
|
|
500
|
+
productsCount,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
checkedAt: new Date().toISOString(),
|
|
505
|
+
connectionStatus: "CONNECTED",
|
|
506
|
+
dataSourceVerified: Boolean(dataSourcesClient),
|
|
507
|
+
disapprovedProductsCount,
|
|
508
|
+
issues,
|
|
509
|
+
issuesCount: issues.length,
|
|
510
|
+
latencyMs: Date.now() - startedAt,
|
|
511
|
+
productsCount,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
exports.diagnoseGoogleMerchantConnection = diagnoseGoogleMerchantConnection;
|
|
300
515
|
function selectRemoteOrphanProducts(remoteProducts, localProducts) {
|
|
301
516
|
const localIds = new Set(localProducts.map((product) => String(product.communicateID)));
|
|
302
517
|
const selected = new Map();
|
|
@@ -3,8 +3,8 @@ import { MerchantPlatformSettingsEntity } from "../entities/platform-integration
|
|
|
3
3
|
import { BaseData, BaseProductData, GoogleProcessedProduct, RemoteProduct } from "../types.js";
|
|
4
4
|
import { GoogleMerchantSettings, GoogleWriteOperation, MerchantOperationResult } from "./google-merchant-api.js";
|
|
5
5
|
import { MerchantStrategyService } from "./merchant-strategy.service.js";
|
|
6
|
-
type GoogleWriteResult = MerchantOperationResult<GoogleWriteOperation>;
|
|
7
|
-
type
|
|
6
|
+
export type GoogleWriteResult = MerchantOperationResult<GoogleWriteOperation>;
|
|
7
|
+
export type GoogleOperationResult = {
|
|
8
8
|
error?: unknown;
|
|
9
9
|
results: GoogleWriteResult[];
|
|
10
10
|
status: "success" | "error";
|
|
@@ -18,6 +18,7 @@ export declare class GooglePlatformIntegrationService {
|
|
|
18
18
|
constructor(connection: TransactionalConnection, strategy: MerchantStrategyService);
|
|
19
19
|
removeOrphanItems(ctx: RequestContext, items: RemoteProduct[]): Promise<void>;
|
|
20
20
|
getAllProducts(ctx: RequestContext): Promise<RemoteProduct[]>;
|
|
21
|
+
getConnectionDiagnostic(ctx: RequestContext): Promise<import("./google-merchant-api.js").GoogleMerchantConnectionDiagnostic>;
|
|
21
22
|
getGoogleProduct(ctx: RequestContext, { communicateID }: {
|
|
22
23
|
communicateID: string;
|
|
23
24
|
}): Promise<GoogleProcessedProduct | null>;
|
|
@@ -25,21 +26,22 @@ export declare class GooglePlatformIntegrationService {
|
|
|
25
26
|
ctx: RequestContext;
|
|
26
27
|
data: BaseProductData<T>;
|
|
27
28
|
entity: Product;
|
|
28
|
-
}): Promise<
|
|
29
|
+
}): Promise<GoogleOperationResult>;
|
|
29
30
|
updateProduct(opts: {
|
|
30
31
|
ctx: RequestContext;
|
|
31
32
|
data: BaseProductData<BaseData>;
|
|
32
33
|
entity: Product;
|
|
33
|
-
}): Promise<
|
|
34
|
+
}): Promise<GoogleOperationResult>;
|
|
34
35
|
deleteProduct(opts: {
|
|
35
36
|
ctx: RequestContext;
|
|
36
37
|
data: BaseProductData<BaseData>;
|
|
37
38
|
entity: Product;
|
|
38
|
-
}): Promise<
|
|
39
|
+
}): Promise<GoogleOperationResult>;
|
|
40
|
+
deleteProductsByCommunicationIds(ctx: RequestContext, communicateIds: string[]): Promise<GoogleOperationResult>;
|
|
39
41
|
batchProductsAction(opts: {
|
|
40
42
|
ctx: RequestContext;
|
|
41
43
|
products: BaseProductData<BaseData>[];
|
|
42
|
-
}): Promise<
|
|
44
|
+
}): Promise<GoogleOperationResult>;
|
|
43
45
|
private sendProducts;
|
|
44
46
|
private sendPreparedProducts;
|
|
45
47
|
private createOperations;
|
|
@@ -51,4 +53,3 @@ export declare class GooglePlatformIntegrationService {
|
|
|
51
53
|
private closeClients;
|
|
52
54
|
private isNotFoundError;
|
|
53
55
|
}
|
|
54
|
-
export {};
|
|
@@ -63,11 +63,48 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
63
63
|
await this.closeClients(authorization.clients);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
async getConnectionDiagnostic(ctx) {
|
|
67
|
+
const rawSettings = await this.connection
|
|
68
|
+
.getRepository(ctx, platform_integration_settings_entity_js_1.MerchantPlatformSettingsEntity)
|
|
69
|
+
.findOne({ relations: ["entries"], where: { platform: "google" } });
|
|
70
|
+
const checkedAt = new Date().toISOString();
|
|
71
|
+
const emptyDiagnostic = {
|
|
72
|
+
checkedAt,
|
|
73
|
+
dataSourceVerified: false,
|
|
74
|
+
disapprovedProductsCount: 0,
|
|
75
|
+
issues: [],
|
|
76
|
+
issuesCount: 0,
|
|
77
|
+
latencyMs: 0,
|
|
78
|
+
productsCount: 0,
|
|
79
|
+
};
|
|
80
|
+
if (!rawSettings) {
|
|
81
|
+
return Object.assign(Object.assign({}, emptyDiagnostic), { connectionStatus: "NOT_CONFIGURED" });
|
|
82
|
+
}
|
|
83
|
+
let settings;
|
|
84
|
+
try {
|
|
85
|
+
settings = this.validateGoogleSettings(rawSettings);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const normalized = (0, google_merchant_api_js_1.normalizeGoogleMerchantError)(error);
|
|
89
|
+
return Object.assign(Object.assign({}, emptyDiagnostic), { connectionStatus: "INVALID_CONFIGURATION", lastError: {
|
|
90
|
+
code: "INVALID_CONFIGURATION",
|
|
91
|
+
message: normalized.message,
|
|
92
|
+
retryable: false,
|
|
93
|
+
} });
|
|
94
|
+
}
|
|
95
|
+
const clients = (0, google_merchant_api_js_1.createGoogleMerchantClients)(settings.credentials);
|
|
96
|
+
try {
|
|
97
|
+
return await (0, google_merchant_api_js_1.diagnoseGoogleMerchantConnection)(clients.products, settings, clients.dataSources);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await this.closeClients(clients);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
66
103
|
async getGoogleProduct(ctx, { communicateID }) {
|
|
67
104
|
const authorization = await this.getAuthorization(ctx);
|
|
68
105
|
try {
|
|
69
106
|
const [product] = await authorization.clients.products.getProduct({
|
|
70
|
-
name: (0, google_merchant_api_js_1.buildGoogleProductName)(authorization.settings.accountId, communicateID),
|
|
107
|
+
name: (0, google_merchant_api_js_1.buildGoogleProductName)(authorization.settings.accountId, communicateID, authorization.settings.contentLanguage, authorization.settings.feedLabel),
|
|
71
108
|
});
|
|
72
109
|
return product !== null && product !== void 0 ? product : null;
|
|
73
110
|
}
|
|
@@ -89,6 +126,35 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
89
126
|
async deleteProduct(opts) {
|
|
90
127
|
return this.sendProducts(Object.assign(Object.assign({}, opts), { method: "delete" }));
|
|
91
128
|
}
|
|
129
|
+
async deleteProductsByCommunicationIds(ctx, communicateIds) {
|
|
130
|
+
const uniqueIds = [...new Set(communicateIds.filter(Boolean))];
|
|
131
|
+
if (uniqueIds.length === 0)
|
|
132
|
+
return { results: [], status: "success" };
|
|
133
|
+
let authorization;
|
|
134
|
+
let results = [];
|
|
135
|
+
try {
|
|
136
|
+
authorization = await this.getAuthorization(ctx);
|
|
137
|
+
const operations = uniqueIds.map((communicateID) => (0, google_merchant_api_js_1.createGoogleDeleteOperation)(communicateID, authorization.settings));
|
|
138
|
+
results = await (0, google_merchant_api_js_1.executeGoogleWriteOperations)(authorization.clients.productInputs, operations);
|
|
139
|
+
const summary = (0, google_merchant_api_js_1.summarizeMerchantOperations)(results);
|
|
140
|
+
if (summary.status === "error") {
|
|
141
|
+
return {
|
|
142
|
+
error: new GoogleMerchantOperationError("delete", results),
|
|
143
|
+
results,
|
|
144
|
+
status: "error",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { results, status: "success" };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
this.error("Google delete failed", error);
|
|
151
|
+
return { error, results, status: "error" };
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (authorization)
|
|
155
|
+
await this.closeClients(authorization.clients);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
92
158
|
async batchProductsAction(opts) {
|
|
93
159
|
const { ctx, products } = opts;
|
|
94
160
|
try {
|
|
@@ -128,7 +194,7 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
128
194
|
.filter((result) => result.status === "success")
|
|
129
195
|
.map((result) => result.item.communicateID));
|
|
130
196
|
const successfulProducts = payload.filter((product) => successfulIds.has(String(product.communicateID)));
|
|
131
|
-
const previousIds = await this.persistCommunicationIds(ctx, successfulProducts
|
|
197
|
+
const previousIds = await this.persistCommunicationIds(ctx, successfulProducts);
|
|
132
198
|
if (previousIds.length > 0) {
|
|
133
199
|
const cleanupOperations = previousIds.map((communicateID) => (0, google_merchant_api_js_1.createGoogleDeleteOperation)(communicateID, authorization.settings));
|
|
134
200
|
const cleanupResults = await (0, google_merchant_api_js_1.executeGoogleWriteOperations)(authorization.clients.productInputs, cleanupOperations);
|
|
@@ -162,7 +228,7 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
162
228
|
}
|
|
163
229
|
return payload.map((product) => (0, google_merchant_api_js_1.createGoogleDeleteOperation)(String(product.communicateID), settings));
|
|
164
230
|
}
|
|
165
|
-
async persistCommunicationIds(ctx, products
|
|
231
|
+
async persistCommunicationIds(ctx, products) {
|
|
166
232
|
var _a;
|
|
167
233
|
const items = products.filter((product) => product.variantID !== undefined);
|
|
168
234
|
const ids = [...new Set(items.map((item) => String(item.variantID)))];
|
|
@@ -178,7 +244,7 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
178
244
|
continue;
|
|
179
245
|
const nextId = String(item.communicateID);
|
|
180
246
|
const previousId = (_a = variant.customFields) === null || _a === void 0 ? void 0 : _a.communicateID;
|
|
181
|
-
if (
|
|
247
|
+
if (previousId && previousId !== nextId) {
|
|
182
248
|
previousIds.add(previousId);
|
|
183
249
|
}
|
|
184
250
|
variant.customFields = Object.assign(Object.assign({}, variant.customFields), { communicateID: nextId });
|
|
@@ -190,9 +256,12 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
190
256
|
}
|
|
191
257
|
async buildPayload(ctx, data) {
|
|
192
258
|
const payload = await this.strategy.prepareGoogleProductPayload(ctx, data);
|
|
193
|
-
|
|
259
|
+
const validPayload = (payload !== null && payload !== void 0 ? payload : []).filter((product) => Boolean(product &&
|
|
194
260
|
String(product.communicateID).length > 0 &&
|
|
195
261
|
product.productAttributes));
|
|
262
|
+
for (const product of validPayload)
|
|
263
|
+
(0, google_merchant_api_js_1.validateGoogleProductUrls)(product);
|
|
264
|
+
return validPayload;
|
|
196
265
|
}
|
|
197
266
|
validateGoogleSettings(rawSettings) {
|
|
198
267
|
return (0, google_merchant_api_js_1.parseGoogleMerchantSettings)(rawSettings.entries);
|
|
@@ -216,7 +285,11 @@ let GooglePlatformIntegrationService = class GooglePlatformIntegrationService {
|
|
|
216
285
|
};
|
|
217
286
|
}
|
|
218
287
|
async closeClients(clients) {
|
|
219
|
-
for (const client of [
|
|
288
|
+
for (const client of [
|
|
289
|
+
clients.products,
|
|
290
|
+
clients.productInputs,
|
|
291
|
+
clients.dataSources,
|
|
292
|
+
]) {
|
|
220
293
|
try {
|
|
221
294
|
await client.close();
|
|
222
295
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RequestContext, TransactionalConnection } from "@deenruv/core";
|
|
2
|
+
import { MerchantSyncRun, MerchantSyncRunStatus } from "../entities/merchant-sync-run.entity.js";
|
|
3
|
+
import { GoogleWriteOperation, MerchantOperationResult } from "./google-merchant-api.js";
|
|
4
|
+
export declare function summarizeGoogleSyncRun(input: {
|
|
5
|
+
fallbackError?: unknown;
|
|
6
|
+
requestedTotal: number;
|
|
7
|
+
results: Array<MerchantOperationResult<GoogleWriteOperation>>;
|
|
8
|
+
}): {
|
|
9
|
+
failed: number;
|
|
10
|
+
status: MerchantSyncRunStatus;
|
|
11
|
+
succeeded: number;
|
|
12
|
+
total: number;
|
|
13
|
+
};
|
|
14
|
+
export declare class MerchantSyncHistoryService {
|
|
15
|
+
private readonly connection;
|
|
16
|
+
constructor(connection: TransactionalConnection);
|
|
17
|
+
start(ctx: RequestContext, input: {
|
|
18
|
+
jobId?: string;
|
|
19
|
+
platform: string;
|
|
20
|
+
total?: number;
|
|
21
|
+
trigger: string;
|
|
22
|
+
}): Promise<MerchantSyncRun>;
|
|
23
|
+
finishGoogleRun(ctx: RequestContext, run: MerchantSyncRun, results: Array<MerchantOperationResult<GoogleWriteOperation>>, fallbackError?: unknown): Promise<MerchantSyncRun>;
|
|
24
|
+
findLatest(ctx: RequestContext, platform: string, take?: number): Promise<MerchantSyncRun[]>;
|
|
25
|
+
}
|