@deenruv/merchant-plugin 1.0.16 → 1.0.17-dev.15

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 CHANGED
@@ -29,10 +29,42 @@ plugins: [
29
29
  ]
30
30
  ```
31
31
 
32
+ ### Google Merchant API settings
33
+
34
+ The Google integration uses the stable Merchant API Products v1 clients. The
35
+ Google settings page requires:
36
+
37
+ - `merchantId` — the numeric Merchant Center account ID.
38
+ - `dataSource` — the full existing data source resource name in the form
39
+ `accounts/{merchantId}/dataSources/{id}`. Its account segment must match
40
+ `merchantId`.
41
+ - `credentials` — Google OAuth credential JSON for a service account or an
42
+ authorized user. Store and distribute this value as a secret.
43
+ - `brand` — the brand applied to exported product attributes.
44
+
45
+ Product inputs are written only to the configured data source. Merchant API
46
+ processing is asynchronous, so a processed product might not be readable for
47
+ several minutes after a successful write.
48
+
49
+ #### Deployment prerequisites (documentation only)
50
+
51
+ An operator must complete the following outside this plugin and outside the
52
+ application deployment. The plugin does not enable APIs, create or register
53
+ data sources, change Merchant Center configuration, or provision credentials:
54
+
55
+ 1. Enable Merchant API for the Google Cloud project that owns the credentials.
56
+ 2. Grant the credential identity the required access to the Merchant Center
57
+ account.
58
+ 3. Create or select an API product data source in Merchant Center and copy its
59
+ full `accounts/{merchantId}/dataSources/{id}` resource name into the plugin
60
+ settings.
61
+ 4. Ensure the credentials can request the
62
+ `https://www.googleapis.com/auth/content` OAuth scope.
63
+
32
64
  ## Features
33
65
 
34
66
  - Strategy-based product export architecture supporting Google and Facebook platforms
35
- - Google Merchant Center integration via Google Content API
67
+ - Google Merchant Center integration via Merchant API Products v1
36
68
  - Facebook Commerce integration via Facebook Business SDK
37
69
  - Per-platform settings storage and management
38
70
  - Bulk product sync to merchant platforms
@@ -30,13 +30,23 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
30
30
  return this.platformIntegrationService.getBaseSettings(ctx, args.platform);
31
31
  }
32
32
  async getMerchantPlatformInfo(ctx, args) {
33
- var _a, _b;
33
+ var _a;
34
34
  const settings = await this.platformIntegrationService.getBaseSettings(ctx, args.platform);
35
35
  if (args.platform === "google") {
36
+ let isValidConnection = false;
37
+ if (settings) {
38
+ try {
39
+ this.googlePlatformIntegrationService.validateGoogleSettings(settings);
40
+ isValidConnection = true;
41
+ }
42
+ catch (_b) {
43
+ isValidConnection = false;
44
+ }
45
+ }
36
46
  return [
37
47
  {
38
48
  productsCount: 0,
39
- isValidConnection: ((_a = settings === null || settings === void 0 ? void 0 : settings.entries.find((entry) => entry.key === "credentials")) === null || _a === void 0 ? void 0 : _a.value) !== "",
49
+ isValidConnection,
40
50
  },
41
51
  ];
42
52
  }
@@ -44,7 +54,7 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
44
54
  return [
45
55
  {
46
56
  productsCount: 0,
47
- isValidConnection: ((_b = settings === null || settings === void 0 ? void 0 : settings.entries.find((entry) => entry.key === "accessToken")) === null || _b === void 0 ? void 0 : _b.value) !== "",
57
+ isValidConnection: ((_a = settings === null || settings === void 0 ? void 0 : settings.entries.find((entry) => entry.key === "accessToken")) === null || _a === void 0 ? void 0 : _a.value) !== "",
48
58
  },
49
59
  ];
50
60
  }
@@ -55,6 +65,9 @@ let PlatformIntegrationAdminResolver = class PlatformIntegrationAdminResolver {
55
65
  platform: args.input.platform,
56
66
  entries: args.input.entries.map((entry) => new platform_integration_setting_entity_js_1.MerchantPlatformSetting(entry)),
57
67
  });
68
+ if (settingsEntity.platform === "google") {
69
+ this.googlePlatformIntegrationService.validateGoogleSettings(settingsEntity);
70
+ }
58
71
  const settings = await this.platformIntegrationService.savePlatformIntegrationSettings(ctx, settingsEntity);
59
72
  return settings;
60
73
  }
@@ -1,6 +1,6 @@
1
1
  import { OnApplicationBootstrap } from "@nestjs/common";
2
2
  import { ModuleRef } from "@nestjs/core";
3
- import { FacebookProduct, GoogleProduct, MerchantExportStrategy, MerchantPluginOptions } from "./types.js";
3
+ import { FacebookProduct, GoogleProcessedProduct, GoogleProduct, GoogleProductInput, MerchantExportStrategy, MerchantPluginOptions } from "./types.js";
4
4
  declare class MerchantPlugin implements OnApplicationBootstrap {
5
5
  private moduleRef;
6
6
  static options: MerchantPluginOptions;
@@ -11,4 +11,4 @@ declare class MerchantPlugin implements OnApplicationBootstrap {
11
11
  private initMerchantStrategy;
12
12
  private destroyMerchantStrategy;
13
13
  }
14
- export { FacebookProduct, GoogleProduct, MerchantExportStrategy, MerchantPlugin, };
14
+ export { FacebookProduct, GoogleProcessedProduct, GoogleProduct, GoogleProductInput, MerchantExportStrategy, MerchantPlugin, };
@@ -1,11 +1,12 @@
1
1
  import { Product, RequestContext, TransactionalConnection } from "@deenruv/core";
2
2
  import { MerchantPlatformSettingsEntity } from "../entities/platform-integration-settings.entity.js";
3
- import { BaseData, BaseProductData } from "../types.js";
3
+ import { BaseData, BaseProductData, RemoteProduct } from "../types.js";
4
4
  import { MerchantStrategyService } from "./merchant-strategy.service.js";
5
- type OpResult = {
5
+ export type FacebookOperationResult = {
6
6
  status: "success" | "error";
7
7
  message?: string;
8
8
  };
9
+ export declare function executeFacebookBatches(batches: readonly BaseProductData<BaseData>[], executeBatch: (batch: BaseProductData<BaseData>) => Promise<FacebookOperationResult>): Promise<FacebookOperationResult>;
9
10
  export declare class FacebookPlatformIntegrationService {
10
11
  private readonly connection;
11
12
  private readonly strategy;
@@ -13,33 +14,30 @@ export declare class FacebookPlatformIntegrationService {
13
14
  private log;
14
15
  private error;
15
16
  constructor(connection: TransactionalConnection, strategy: MerchantStrategyService);
16
- removeOrphanItems(ctx: RequestContext, items: BaseData[]): Promise<void>;
17
- getAllProducts(ctx: RequestContext): Promise<{
18
- communicateID: any;
19
- name: any;
20
- }[]>;
17
+ removeOrphanItems(ctx: RequestContext, items: RemoteProduct[]): Promise<void>;
18
+ getAllProducts(ctx: RequestContext): Promise<RemoteProduct[]>;
21
19
  private withCatalog;
22
20
  private sendBatch;
23
21
  createProduct({ ctx, data, }: {
24
22
  ctx: RequestContext;
25
23
  data: BaseProductData<BaseData>;
26
24
  entity?: Product;
27
- }): Promise<OpResult>;
25
+ }): Promise<FacebookOperationResult>;
28
26
  updateProduct({ ctx, data, }: {
29
27
  ctx: RequestContext;
30
28
  data: BaseProductData<BaseData>;
31
29
  entity?: Product;
32
- }): Promise<OpResult>;
30
+ }): Promise<FacebookOperationResult>;
33
31
  deleteProduct({ ctx, data, }: {
34
32
  ctx: RequestContext;
35
33
  data: BaseProductData<BaseData>;
36
34
  entity?: Product;
37
- }): Promise<OpResult>;
35
+ }): Promise<FacebookOperationResult>;
38
36
  batchProductsAction({ ctx, products, }: {
39
37
  ctx: RequestContext;
40
38
  products: BaseProductData<BaseData>[];
41
39
  entity?: Product;
42
- }): Promise<OpResult>;
40
+ }): Promise<FacebookOperationResult>;
43
41
  setFacebookSettings(ctx: RequestContext, rawSettings?: MerchantPlatformSettingsEntity): Promise<{
44
42
  autoUpdate: boolean;
45
43
  accessToken: string;
@@ -48,4 +46,3 @@ export declare class FacebookPlatformIntegrationService {
48
46
  } | null>;
49
47
  private prepareFacebookProductPayload;
50
48
  }
51
- export {};
@@ -20,13 +20,22 @@ var __rest = (this && this.__rest) || function (s, e) {
20
20
  return t;
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.FacebookPlatformIntegrationService = void 0;
23
+ exports.FacebookPlatformIntegrationService = exports.executeFacebookBatches = void 0;
24
24
  const core_1 = require("@deenruv/core");
25
25
  const common_1 = require("@nestjs/common");
26
26
  const facebook_nodejs_business_sdk_1 = require("facebook-nodejs-business-sdk");
27
27
  const platform_integration_settings_entity_js_1 = require("../entities/platform-integration-settings.entity.js");
28
28
  const merchant_strategy_service_js_1 = require("./merchant-strategy.service.js");
29
29
  const typeorm_1 = require("typeorm");
30
+ async function executeFacebookBatches(batches, executeBatch) {
31
+ for (const batch of batches) {
32
+ const result = await executeBatch(batch);
33
+ if (result.status === "error")
34
+ return result;
35
+ }
36
+ return { status: "success", message: "Products processed successfully" };
37
+ }
38
+ exports.executeFacebookBatches = executeFacebookBatches;
30
39
  let FacebookPlatformIntegrationService = class FacebookPlatformIntegrationService {
31
40
  constructor(connection, strategy) {
32
41
  this.connection = connection;
@@ -160,12 +169,11 @@ let FacebookPlatformIntegrationService = class FacebookPlatformIntegrationServic
160
169
  }
161
170
  async batchProductsAction({ ctx, products, }) {
162
171
  const flatten = products.flat();
163
- const batchSize = 500;
164
- for (let i = 0; i < flatten.length; i += batchSize) {
165
- const batch = flatten.slice(i, i + batchSize);
166
- await this.sendBatch({ ctx, method: "UPDATE", data: batch });
172
+ const batches = [];
173
+ for (let i = 0; i < flatten.length; i += 500) {
174
+ batches.push(flatten.slice(i, i + 500));
167
175
  }
168
- return { status: "success", message: "Products processed successfully" };
176
+ return executeFacebookBatches(batches, (batch) => this.sendBatch({ ctx, method: "UPDATE", data: batch }));
169
177
  }
170
178
  async setFacebookSettings(ctx, rawSettings) {
171
179
  var _a;
@@ -0,0 +1,84 @@
1
+ import { v1 } from "@google-shopping/products";
2
+ import type { protos } from "@google-shopping/products";
3
+ import type { GoogleProcessedProduct, GoogleProduct, GoogleProductInput, RemoteProduct } from "../types.js";
4
+ export declare const GOOGLE_CONTENT_LANGUAGE = "pl";
5
+ export declare const GOOGLE_FEED_LABEL = "PL";
6
+ export declare const DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
7
+ type MerchantClientOptions = NonNullable<ConstructorParameters<typeof v1.ProductsServiceClient>[0]>;
8
+ export type GoogleMerchantCredentials = NonNullable<MerchantClientOptions["credentials"]>;
9
+ export type GoogleMerchantSettings = {
10
+ accountId: string;
11
+ autoUpdate: boolean;
12
+ brand: string;
13
+ credentials: GoogleMerchantCredentials;
14
+ dataSource: string;
15
+ };
16
+ export type GoogleMerchantClients = {
17
+ productInputs: v1.ProductInputsServiceClient;
18
+ products: v1.ProductsServiceClient;
19
+ };
20
+ type InsertProductInputRequest = protos.google.shopping.merchant.products.v1.IInsertProductInputRequest;
21
+ type UpdateProductInputRequest = protos.google.shopping.merchant.products.v1.IUpdateProductInputRequest;
22
+ type DeleteProductInputRequest = protos.google.shopping.merchant.products.v1.IDeleteProductInputRequest;
23
+ type ListProductsRequest = protos.google.shopping.merchant.products.v1.IListProductsRequest;
24
+ export type GoogleWriteOperation = {
25
+ communicateID: string;
26
+ method: "insert";
27
+ request: InsertProductInputRequest;
28
+ } | {
29
+ communicateID: string;
30
+ method: "update";
31
+ request: UpdateProductInputRequest;
32
+ } | {
33
+ communicateID: string;
34
+ method: "delete";
35
+ request: DeleteProductInputRequest;
36
+ };
37
+ export type MerchantOperationResult<T> = {
38
+ index: number;
39
+ item: T;
40
+ status: "success";
41
+ } | {
42
+ error: unknown;
43
+ index: number;
44
+ item: T;
45
+ status: "error";
46
+ };
47
+ export type MerchantOperationSummary<T> = {
48
+ failures: Array<Extract<MerchantOperationResult<T>, {
49
+ status: "error";
50
+ }>>;
51
+ results: Array<MerchantOperationResult<T>>;
52
+ status: "success" | "error";
53
+ };
54
+ export interface GoogleProductInputsWriter {
55
+ deleteProductInput(request: DeleteProductInputRequest): Promise<unknown>;
56
+ insertProductInput(request: InsertProductInputRequest): Promise<unknown>;
57
+ updateProductInput(request: UpdateProductInputRequest): Promise<unknown>;
58
+ }
59
+ export interface GoogleProductsReader {
60
+ listProductsAsync(request: ListProductsRequest): AsyncIterable<GoogleProcessedProduct>;
61
+ }
62
+ type SettingEntry = {
63
+ key: string;
64
+ value: string;
65
+ };
66
+ export declare function parseGoogleCredentials(rawCredentials: string): GoogleMerchantCredentials;
67
+ export declare function normalizeMerchantId(rawMerchantId: string): string;
68
+ export declare function normalizeDataSource(rawDataSource: string, merchantId: string): string;
69
+ export declare function parseGoogleMerchantSettings(entries: readonly SettingEntry[]): GoogleMerchantSettings;
70
+ export declare function createGoogleMerchantClients(credentials: GoogleMerchantCredentials): GoogleMerchantClients;
71
+ export declare function buildGoogleProductIdentifier(offerId: string): string;
72
+ export declare function buildGoogleProductName(accountId: string, offerId: string): string;
73
+ export declare function buildGoogleProductInputName(accountId: string, offerId: string): string;
74
+ export declare function toGoogleProductInput(product: GoogleProduct, brand: string): GoogleProductInput;
75
+ export declare function buildGoogleUpdateMask(productInput: GoogleProductInput): protos.google.protobuf.IFieldMask;
76
+ export declare function createGoogleInsertOperation(product: GoogleProduct, settings: GoogleMerchantSettings): GoogleWriteOperation;
77
+ export declare function createGoogleUpdateOperation(product: GoogleProduct, settings: GoogleMerchantSettings): GoogleWriteOperation;
78
+ export declare function createGoogleDeleteOperation(communicateID: string, settings: GoogleMerchantSettings): GoogleWriteOperation;
79
+ export declare function runBoundedMerchantOperations<T>(items: readonly T[], operation: (item: T, index: number) => Promise<void>, concurrency?: number): Promise<Array<MerchantOperationResult<T>>>;
80
+ export declare function executeGoogleWriteOperations(client: GoogleProductInputsWriter, operations: readonly GoogleWriteOperation[], concurrency?: number): Promise<Array<MerchantOperationResult<GoogleWriteOperation>>>;
81
+ export declare function summarizeMerchantOperations<T>(results: Array<MerchantOperationResult<T>>): MerchantOperationSummary<T>;
82
+ export declare function collectGoogleProductsForDataSource(client: GoogleProductsReader, accountId: string, dataSource: string): Promise<RemoteProduct[]>;
83
+ export declare function selectRemoteOrphanProducts(remoteProducts: readonly RemoteProduct[], localProducts: readonly Pick<GoogleProduct, "communicateID">[]): RemoteProduct[];
84
+ export {};
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
3
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
4
+ var m = o[Symbol.asyncIterator], i;
5
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
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
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
8
+ };
9
+ 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;
11
+ const products_1 = require("@google-shopping/products");
12
+ const GOOGLE_OAUTH_SCOPE = "https://www.googleapis.com/auth/content";
13
+ const PLAIN_IDENTIFIER_COMPONENT = /^[A-Za-z0-9_-]+$/;
14
+ const MERCHANT_ID_PATTERN = /^[1-9]\d*$/;
15
+ const DATA_SOURCE_PATTERN = /^accounts\/([1-9]\d*)\/dataSources\/([1-9]\d*)$/;
16
+ exports.GOOGLE_CONTENT_LANGUAGE = "pl";
17
+ exports.GOOGLE_FEED_LABEL = "PL";
18
+ exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY = 4;
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ function optionalString(value, key) {
23
+ const candidate = value[key];
24
+ return typeof candidate === "string" && candidate.length > 0
25
+ ? candidate
26
+ : undefined;
27
+ }
28
+ function requiredString(value, key) {
29
+ const candidate = optionalString(value, key);
30
+ if (!candidate || candidate.trim().length === 0) {
31
+ throw new Error(`Google credentials field ${key} is required`);
32
+ }
33
+ return candidate;
34
+ }
35
+ function parseGoogleCredentials(rawCredentials) {
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(rawCredentials);
39
+ }
40
+ catch (_a) {
41
+ throw new Error("Google credentials must be valid JSON");
42
+ }
43
+ if (!isRecord(parsed)) {
44
+ throw new Error("Google credentials must be a JSON object");
45
+ }
46
+ const credentialType = optionalString(parsed, "type");
47
+ if (credentialType === "authorized_user") {
48
+ return Object.assign({ type: "authorized_user", client_id: requiredString(parsed, "client_id").trim(), client_secret: requiredString(parsed, "client_secret"), refresh_token: requiredString(parsed, "refresh_token") }, (optionalString(parsed, "quota_project_id") && {
49
+ quota_project_id: optionalString(parsed, "quota_project_id"),
50
+ }));
51
+ }
52
+ if (credentialType === undefined || credentialType === "service_account") {
53
+ return Object.assign(Object.assign(Object.assign(Object.assign({ type: "service_account", client_email: requiredString(parsed, "client_email").trim(), private_key: requiredString(parsed, "private_key") }, (optionalString(parsed, "private_key_id") && {
54
+ private_key_id: optionalString(parsed, "private_key_id"),
55
+ })), (optionalString(parsed, "project_id") && {
56
+ project_id: optionalString(parsed, "project_id"),
57
+ })), (optionalString(parsed, "client_id") && {
58
+ client_id: optionalString(parsed, "client_id"),
59
+ })), (optionalString(parsed, "quota_project_id") && {
60
+ quota_project_id: optionalString(parsed, "quota_project_id"),
61
+ }));
62
+ }
63
+ throw new Error(`Unsupported Google credentials type: ${credentialType !== null && credentialType !== void 0 ? credentialType : "missing"}`);
64
+ }
65
+ exports.parseGoogleCredentials = parseGoogleCredentials;
66
+ function normalizeMerchantId(rawMerchantId) {
67
+ const merchantId = rawMerchantId.trim();
68
+ if (!MERCHANT_ID_PATTERN.test(merchantId)) {
69
+ throw new Error("Merchant ID must be a positive numeric account ID");
70
+ }
71
+ return merchantId;
72
+ }
73
+ exports.normalizeMerchantId = normalizeMerchantId;
74
+ function normalizeDataSource(rawDataSource, merchantId) {
75
+ const dataSource = rawDataSource.trim();
76
+ const match = DATA_SOURCE_PATTERN.exec(dataSource);
77
+ if (!match) {
78
+ throw new Error("Google dataSource must match accounts/{merchantId}/dataSources/{id}");
79
+ }
80
+ if (match[1] !== merchantId) {
81
+ throw new Error("Google dataSource account must match Merchant ID");
82
+ }
83
+ return `accounts/${match[1]}/dataSources/${match[2]}`;
84
+ }
85
+ exports.normalizeDataSource = normalizeDataSource;
86
+ function parseGoogleMerchantSettings(entries) {
87
+ var _a, _b, _c, _d, _e;
88
+ const getValue = (key) => { var _a; return (_a = entries.find((entry) => entry.key === key)) === null || _a === void 0 ? void 0 : _a.value; };
89
+ const accountId = normalizeMerchantId((_a = getValue("merchantId")) !== null && _a !== void 0 ? _a : "");
90
+ const dataSource = normalizeDataSource((_b = getValue("dataSource")) !== null && _b !== void 0 ? _b : "", accountId);
91
+ const brand = ((_c = getValue("brand")) !== null && _c !== void 0 ? _c : "").trim();
92
+ if (!brand) {
93
+ throw new Error("Google brand is required");
94
+ }
95
+ const credentials = parseGoogleCredentials((_d = getValue("credentials")) !== null && _d !== void 0 ? _d : "");
96
+ return {
97
+ accountId,
98
+ autoUpdate: ((_e = getValue("autoUpdate")) !== null && _e !== void 0 ? _e : "").toLowerCase() === "true",
99
+ brand,
100
+ credentials,
101
+ dataSource,
102
+ };
103
+ }
104
+ exports.parseGoogleMerchantSettings = parseGoogleMerchantSettings;
105
+ function createGoogleMerchantClients(credentials) {
106
+ const options = {
107
+ credentials,
108
+ scopes: [GOOGLE_OAUTH_SCOPE],
109
+ };
110
+ return {
111
+ productInputs: new products_1.v1.ProductInputsServiceClient(options),
112
+ products: new products_1.v1.ProductsServiceClient(options),
113
+ };
114
+ }
115
+ exports.createGoogleMerchantClients = createGoogleMerchantClients;
116
+ function buildGoogleProductIdentifier(offerId) {
117
+ if (!offerId) {
118
+ throw new Error("Google offer ID is required");
119
+ }
120
+ const components = [exports.GOOGLE_CONTENT_LANGUAGE, exports.GOOGLE_FEED_LABEL, offerId];
121
+ const identifier = components.join("~");
122
+ return components.every((component) => PLAIN_IDENTIFIER_COMPONENT.test(component))
123
+ ? identifier
124
+ : Buffer.from(identifier, "utf8").toString("base64url");
125
+ }
126
+ exports.buildGoogleProductIdentifier = buildGoogleProductIdentifier;
127
+ function buildGoogleProductName(accountId, offerId) {
128
+ return `accounts/${accountId}/products/${buildGoogleProductIdentifier(offerId)}`;
129
+ }
130
+ exports.buildGoogleProductName = buildGoogleProductName;
131
+ function buildGoogleProductInputName(accountId, offerId) {
132
+ return `accounts/${accountId}/productInputs/${buildGoogleProductIdentifier(offerId)}`;
133
+ }
134
+ 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 && {
137
+ customAttributes: product.customAttributes,
138
+ })), (product.versionNumber !== undefined && {
139
+ versionNumber: product.versionNumber,
140
+ }));
141
+ }
142
+ exports.toGoogleProductInput = toGoogleProductInput;
143
+ function camelToSnake(value) {
144
+ return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
145
+ }
146
+ function buildGoogleUpdateMask(productInput) {
147
+ var _a, _b;
148
+ const paths = Object.entries((_a = productInput.productAttributes) !== null && _a !== void 0 ? _a : {})
149
+ .filter(([, value]) => value !== undefined)
150
+ .map(([key]) => `product_attributes.${camelToSnake(key)}`);
151
+ for (const customAttribute of (_b = productInput.customAttributes) !== null && _b !== void 0 ? _b : []) {
152
+ if (customAttribute.name) {
153
+ paths.push(`custom_attribute.${customAttribute.name}`);
154
+ }
155
+ }
156
+ if (paths.length === 0) {
157
+ throw new Error("Google update requires at least one mutable attribute");
158
+ }
159
+ return { paths };
160
+ }
161
+ exports.buildGoogleUpdateMask = buildGoogleUpdateMask;
162
+ function createGoogleInsertOperation(product, settings) {
163
+ const communicateID = String(product.communicateID);
164
+ return {
165
+ communicateID,
166
+ method: "insert",
167
+ request: {
168
+ parent: `accounts/${settings.accountId}`,
169
+ dataSource: settings.dataSource,
170
+ productInput: toGoogleProductInput(product, settings.brand),
171
+ },
172
+ };
173
+ }
174
+ exports.createGoogleInsertOperation = createGoogleInsertOperation;
175
+ function createGoogleUpdateOperation(product, settings) {
176
+ 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 && {
179
+ customAttributes: input.customAttributes,
180
+ }));
181
+ return {
182
+ communicateID,
183
+ method: "update",
184
+ request: {
185
+ dataSource: settings.dataSource,
186
+ productInput,
187
+ updateMask: buildGoogleUpdateMask(productInput),
188
+ },
189
+ };
190
+ }
191
+ exports.createGoogleUpdateOperation = createGoogleUpdateOperation;
192
+ function createGoogleDeleteOperation(communicateID, settings) {
193
+ return {
194
+ communicateID,
195
+ method: "delete",
196
+ request: {
197
+ name: buildGoogleProductInputName(settings.accountId, communicateID),
198
+ dataSource: settings.dataSource,
199
+ },
200
+ };
201
+ }
202
+ exports.createGoogleDeleteOperation = createGoogleDeleteOperation;
203
+ async function runBoundedMerchantOperations(items, operation, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY) {
204
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
205
+ throw new Error("Merchant operation concurrency must be a positive integer");
206
+ }
207
+ if (items.length === 0)
208
+ return [];
209
+ const results = new Array(items.length);
210
+ let nextIndex = 0;
211
+ const workerCount = Math.min(concurrency, items.length);
212
+ const workers = Array.from({ length: workerCount }, async () => {
213
+ while (true) {
214
+ const index = nextIndex;
215
+ nextIndex += 1;
216
+ if (index >= items.length)
217
+ return;
218
+ const item = items[index];
219
+ try {
220
+ await operation(item, index);
221
+ results[index] = { index, item, status: "success" };
222
+ }
223
+ catch (error) {
224
+ results[index] = { error, index, item, status: "error" };
225
+ }
226
+ }
227
+ });
228
+ await Promise.all(workers);
229
+ return results;
230
+ }
231
+ exports.runBoundedMerchantOperations = runBoundedMerchantOperations;
232
+ function isNotFoundError(error) {
233
+ if (!isRecord(error))
234
+ return false;
235
+ if (error.code === 5 || error.code === 404 || error.code === "404")
236
+ return true;
237
+ const response = error.response;
238
+ return isRecord(response) && response.status === 404;
239
+ }
240
+ async function executeGoogleWriteOperations(client, operations, concurrency = exports.DEFAULT_GOOGLE_WRITE_CONCURRENCY) {
241
+ 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);
248
+ }
249
+ else {
250
+ await client.deleteProductInput(operation.request);
251
+ }
252
+ }
253
+ catch (error) {
254
+ if (operation.method === "delete" && isNotFoundError(error))
255
+ return;
256
+ throw error;
257
+ }
258
+ }, concurrency);
259
+ }
260
+ exports.executeGoogleWriteOperations = executeGoogleWriteOperations;
261
+ function summarizeMerchantOperations(results) {
262
+ const failures = results.filter((result) => result.status === "error");
263
+ return {
264
+ failures,
265
+ results,
266
+ status: failures.length === 0 ? "success" : "error",
267
+ };
268
+ }
269
+ exports.summarizeMerchantOperations = summarizeMerchantOperations;
270
+ async function collectGoogleProductsForDataSource(client, accountId, dataSource) {
271
+ var _a, e_1, _b, _c;
272
+ var _d, _e;
273
+ const products = new Map();
274
+ try {
275
+ for (var _f = true, _g = __asyncValues(client.listProductsAsync({
276
+ parent: `accounts/${accountId}`,
277
+ pageSize: 1000,
278
+ })), _h; _h = await _g.next(), _a = _h.done, !_a; _f = true) {
279
+ _c = _h.value;
280
+ _f = false;
281
+ const product = _c;
282
+ if (product.dataSource !== dataSource || !product.offerId)
283
+ continue;
284
+ products.set(product.offerId, {
285
+ communicateID: product.offerId,
286
+ name: (_e = (_d = product.productAttributes) === null || _d === void 0 ? void 0 : _d.title) !== null && _e !== void 0 ? _e : undefined,
287
+ });
288
+ }
289
+ }
290
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
291
+ finally {
292
+ try {
293
+ if (!_f && !_a && (_b = _g.return)) await _b.call(_g);
294
+ }
295
+ finally { if (e_1) throw e_1.error; }
296
+ }
297
+ return [...products.values()];
298
+ }
299
+ exports.collectGoogleProductsForDataSource = collectGoogleProductsForDataSource;
300
+ function selectRemoteOrphanProducts(remoteProducts, localProducts) {
301
+ const localIds = new Set(localProducts.map((product) => String(product.communicateID)));
302
+ const selected = new Map();
303
+ for (const remoteProduct of remoteProducts) {
304
+ if (!localIds.has(remoteProduct.communicateID)) {
305
+ selected.set(remoteProduct.communicateID, remoteProduct);
306
+ }
307
+ }
308
+ return [...selected.values()];
309
+ }
310
+ exports.selectRemoteOrphanProducts = selectRemoteOrphanProducts;