@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.
Files changed (28) hide show
  1. package/README.md +14 -0
  2. package/dist/plugin-server/api/platform-integration-admin-resolver.d.ts +20 -1
  3. package/dist/plugin-server/api/platform-integration-admin-resolver.js +44 -19
  4. package/dist/plugin-server/entities/merchant-sync-item.entity.d.ts +14 -0
  5. package/dist/plugin-server/entities/merchant-sync-item.entity.js +56 -0
  6. package/dist/plugin-server/entities/merchant-sync-run.entity.d.ts +19 -0
  7. package/dist/plugin-server/entities/merchant-sync-run.entity.js +72 -0
  8. package/dist/plugin-server/extensions/api-extensions.js +53 -2
  9. package/dist/plugin-server/index.js +10 -1
  10. package/dist/plugin-server/services/google-merchant-api.d.ts +48 -6
  11. package/dist/plugin-server/services/google-merchant-api.js +247 -32
  12. package/dist/plugin-server/services/google-platform-integration.service.d.ts +8 -7
  13. package/dist/plugin-server/services/google-platform-integration.service.js +79 -6
  14. package/dist/plugin-server/services/merchant-sync-history.service.d.ts +25 -0
  15. package/dist/plugin-server/services/merchant-sync-history.service.js +97 -0
  16. package/dist/plugin-server/services/platform-integration.service.d.ts +6 -2
  17. package/dist/plugin-server/services/platform-integration.service.js +53 -29
  18. package/dist/plugin-server/services/subscriber.service.d.ts +32 -4
  19. package/dist/plugin-server/services/subscriber.service.js +141 -23
  20. package/dist/plugin-server/ui/graphql/mutations.ts +7 -0
  21. package/dist/plugin-server/ui/graphql/queries.ts +99 -0
  22. package/dist/plugin-server/ui/pages/GooglePage.tsx +158 -16
  23. package/dist/plugin-ui/graphql/mutations.d.ts +5 -0
  24. package/dist/plugin-ui/graphql/mutations.js +6 -0
  25. package/dist/plugin-ui/graphql/queries.d.ts +53 -2
  26. package/dist/plugin-ui/graphql/queries.js +49 -0
  27. package/dist/plugin-ui/pages/GooglePage.js +51 -13
  28. 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: ((_e = getValue("autoUpdate")) !== null && _e !== void 0 ? _e : "").toLowerCase() === "true",
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 = [exports.GOOGLE_CONTENT_LANGUAGE, exports.GOOGLE_FEED_LABEL, offerId];
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: exports.GOOGLE_CONTENT_LANGUAGE, feedLabel: exports.GOOGLE_FEED_LABEL, productAttributes: Object.assign(Object.assign({}, product.productAttributes), { brand }) }, (product.customAttributes && {
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] = { index, item, status: "success" };
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] = { error, index, item, status: "error" };
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
- try {
243
- if (operation.method === "insert") {
244
- await client.insertProductInput(operation.request);
245
- }
246
- else if (operation.method === "update") {
247
- await client.updateProductInput(operation.request);
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
- else {
250
- await client.deleteProductInput(operation.request);
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 OpResult = {
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<OpResult>;
29
+ }): Promise<GoogleOperationResult>;
29
30
  updateProduct(opts: {
30
31
  ctx: RequestContext;
31
32
  data: BaseProductData<BaseData>;
32
33
  entity: Product;
33
- }): Promise<OpResult>;
34
+ }): Promise<GoogleOperationResult>;
34
35
  deleteProduct(opts: {
35
36
  ctx: RequestContext;
36
37
  data: BaseProductData<BaseData>;
37
38
  entity: Product;
38
- }): Promise<OpResult>;
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<OpResult>;
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, method);
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, method) {
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 (method === "update" && previousId && previousId !== nextId) {
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
- return (payload !== null && payload !== void 0 ? payload : []).filter((product) => Boolean(product &&
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 [clients.products, clients.productInputs]) {
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
+ }