@bprotsyk/aso-core 2.1.100 → 2.1.101

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/lib/app/app.d.ts CHANGED
@@ -50,7 +50,8 @@ export declare enum EPlatform {
50
50
  }
51
51
  export declare enum EDirectType {
52
52
  KEITARO_OFFER = "keitaroOffer",
53
- OFFER_DIRECT = "offerDirect"
53
+ OFFER_DIRECT = "offerDirect",
54
+ TRAFFLE = "traffle"
54
55
  }
55
56
  export interface IPublicationHistory {
56
57
  publications: number[];
package/lib/app/app.js CHANGED
@@ -25,6 +25,7 @@ var EDirectType;
25
25
  (function (EDirectType) {
26
26
  EDirectType["KEITARO_OFFER"] = "keitaroOffer";
27
27
  EDirectType["OFFER_DIRECT"] = "offerDirect";
28
+ EDirectType["TRAFFLE"] = "traffle";
28
29
  })(EDirectType = exports.EDirectType || (exports.EDirectType = {}));
29
30
  const getPlatformName = (platform) => {
30
31
  switch (platform) {
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { ICloudflareDomainType } from "general/cloudflare-domain";
26
27
  import { Document, Model, Schema } from "mongoose";
27
28
  export interface IDomain extends Document {
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { Schema, Document } from "mongoose";
26
27
  export interface INamecheapDomain {
27
28
  ID: string;
@@ -24,6 +24,7 @@ export interface IKeitaroCampaign {
24
24
  collect_clicks?: any;
25
25
  uniqueness_type?: any;
26
26
  source?: string;
27
+ bind_visitors?: string;
27
28
  }
28
29
  export interface IKeitaroCampaignParameters {
29
30
  [key: string]: IKeitaroCampaignParameter;
@@ -7,6 +7,7 @@ const axios_1 = __importDefault(require("axios"));
7
7
  exports.default = axios_1.default.create({
8
8
  baseURL: 'https://traffle-tech.com/admin_api/v1/',
9
9
  headers: {
10
- "Api-Key": `3e80a8fcf1ceb01a761457d017e43a0a`,
10
+ // "Api-Key": `3e80a8fcf1ceb01a761457d017e43a0a`,
11
+ "Api-Key": `778a14c0730abe2629e88101cb88d573`,
11
12
  },
12
13
  });
@@ -1,4 +1,6 @@
1
1
  import { IOffer } from "../../../offers/offer";
2
+ import { IKeitaroCampaign } from "../../../keitaro/keitaro-campaign";
3
+ import { EPlatform, IApp } from "../../../app/app";
2
4
  import { IKeitaroOffer } from "index";
3
5
  declare function getAllOffers(): Promise<IKeitaroOffer[]>;
4
6
  export declare enum ITraffleTrafficType {
@@ -35,6 +37,7 @@ declare function getTraffleAffiliateNetworks(): Promise<IAffiliateNetwork[]>;
35
37
  declare function deleteOfferById(id: number): Promise<any>;
36
38
  declare function addOffersToTraffleKeitaro(offers: ITraffleOffer[]): Promise<void>;
37
39
  declare function updateOfferLinkById(id: number, action_payload: string | object): Promise<any>;
40
+ declare function cloneTraffleCampaign(app: IApp, platform: EPlatform, addDefaultStreams?: boolean): Promise<IKeitaroCampaign | any>;
38
41
  export declare const TraffleKeitaroService: {
39
42
  addOffersToTraffleKeitaro: typeof addOffersToTraffleKeitaro;
40
43
  getTraffleOffersGroups: typeof getTraffleOffersGroups;
@@ -43,5 +46,6 @@ export declare const TraffleKeitaroService: {
43
46
  deleteOfferById: typeof deleteOfferById;
44
47
  getAllOffers: typeof getAllOffers;
45
48
  updateOfferLinkById: typeof updateOfferLinkById;
49
+ cloneTraffleCampaign: typeof cloneTraffleCampaign;
46
50
  };
47
51
  export {};
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TraffleKeitaroService = exports.ITraffleTrafficType = void 0;
7
7
  const http_1 = __importDefault(require("./http"));
8
+ const app_1 = require("../../../app/app");
8
9
  const openai_1 = require("../../../network/openai/openai");
9
10
  async function getAllOffers() {
10
11
  const { data: offers } = await http_1.default.get('offers');
@@ -102,6 +103,259 @@ async function updateOfferLinkById(id, action_payload) {
102
103
  const response = await http_1.default.put(`/offers/${id}`, updateData);
103
104
  return response.data;
104
105
  }
106
+ async function getCampaignById(id) {
107
+ const { data: campaign } = await http_1.default.get(`/campaigns/${id}`);
108
+ return campaign;
109
+ }
110
+ async function getAllCampaigns() {
111
+ const { data: campaigns } = await http_1.default.get('campaigns');
112
+ return campaigns;
113
+ }
114
+ async function getDomains(onlyActive) {
115
+ const { data: domains } = await http_1.default.get(`/domains`);
116
+ return onlyActive ? domains.filter((d) => d.network_status == "active") : domains;
117
+ }
118
+ async function getStreamsByCampaignId(campaignId) {
119
+ const { data: streams } = await http_1.default.get(`campaigns/${campaignId}/streams`);
120
+ return streams;
121
+ }
122
+ async function createCampaign(campaignData) {
123
+ let { data: campaign } = await http_1.default.post(`/campaigns`, campaignData);
124
+ return campaign;
125
+ }
126
+ async function TrafleKeitaroParameters(parameters) {
127
+ const { bundle, appsflyerDevKey, name } = parameters;
128
+ let openAIResponse = await openai_1.OpenAI.requestOpenAI({
129
+ model: "o3-mini",
130
+ messages: [
131
+ { role: "user", content: `sid for ${bundle} ${name} generations Generate **one** random English word in full lowercase (still lowercase, no numbers or special characters) for the parameters "naming", "advertisingid", "appsflyerDeviceID". Return **ONLY** an JSON object: { "naming": "word", "advertisingid": "word", "appsflyerDeviceID": "word"}` },
132
+ ]
133
+ });
134
+ if (!openAIResponse) {
135
+ return null;
136
+ }
137
+ let ParamsGenerationsOpenAIResponse = null;
138
+ try {
139
+ ParamsGenerationsOpenAIResponse = JSON.parse(openAIResponse);
140
+ }
141
+ catch (error) {
142
+ console.error("Failed to parse OpenAI response:", error);
143
+ return null;
144
+ }
145
+ console.log(`ParamsGenerationsOpenAIResponse: ${JSON.stringify(ParamsGenerationsOpenAIResponse)}`);
146
+ return {
147
+ parameters: {
148
+ keyword: { name: 'keyword', placeholder: '', alias: '' },
149
+ cost: { name: 'cost', placeholder: '', alias: '' },
150
+ currency: { name: 'currency', placeholder: '', alias: '' },
151
+ external_id: { name: 'external_id', placeholder: '', alias: '' },
152
+ creative_id: { name: 'creative_id', placeholder: '', alias: '' },
153
+ ad_campaign_id: { name: 'ad_campaign_id', placeholder: '', alias: '' },
154
+ source: { name: 'source', placeholder: '', alias: '' },
155
+ sub_id_1: { name: 'uqID', placeholder: '', alias: '' },
156
+ sub_id_2: { name: 'AD_ID', placeholder: '', alias: '' },
157
+ sub_id_3: { name: 'USB', placeholder: '', alias: '' },
158
+ sub_id_4: { name: 'DEV', placeholder: '', alias: '' },
159
+ sub_id_5: { name: 'LANG', placeholder: '', alias: '' },
160
+ sub_id_10: { name: 'market', placeholder: '', alias: '' },
161
+ sub_id_15: { name: ParamsGenerationsOpenAIResponse?.naming, placeholder: '{naming}', alias: 'Naming' },
162
+ sub_id_16: {
163
+ name: 'bundle_id',
164
+ placeholder: bundle,
165
+ alias: 'Bundle ID'
166
+ },
167
+ sub_id_17: {
168
+ name: ParamsGenerationsOpenAIResponse?.advertisingid,
169
+ placeholder: '{advertising_id}',
170
+ alias: 'Advertising ID'
171
+ },
172
+ sub_id_18: {
173
+ name: ParamsGenerationsOpenAIResponse?.appsflyerDeviceID,
174
+ placeholder: '{appsflyer_device_id}',
175
+ alias: 'Appsflyer Device ID'
176
+ },
177
+ sub_id_19: {
178
+ name: 'appsflyer_dev_key',
179
+ placeholder: appsflyerDevKey,
180
+ alias: 'Appsflyer Dev Key'
181
+ }
182
+ }
183
+ };
184
+ }
185
+ async function cloneTraffleCampaign(app, platform, addDefaultStreams) {
186
+ const ORIGINAL_CLONE_CAMPAIGN_ID = 1925;
187
+ const appsflyerAvailability = app.platforms[platform].appsflyerParams?.apiToken ? "[AF]" : "";
188
+ let name = `[${app.id}] ${app.name} ET [${app.bundle}] [Android] ${appsflyerAvailability} ${app.domainParams.name}`;
189
+ let platformName = platform ? (0, app_1.getPlatformName)(platform) : null;
190
+ const platformCampaignName = `#${app.id} [➥] (${platformName})`;
191
+ const generateAlias = () => {
192
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
193
+ return Array.from({ length: 8 }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
194
+ };
195
+ let allCampaigns = await getAllCampaigns();
196
+ let matchingCampaign = [];
197
+ if (platform && platform !== app_1.EPlatform.GENERAL) {
198
+ // Для конкретної платформи
199
+ const platformName = (0, app_1.getPlatformName)(platform);
200
+ matchingCampaign = allCampaigns.filter((c) => {
201
+ // Точна перевірка ID з межами слів
202
+ const idPattern = new RegExp(`${app.id}\\b`);
203
+ const hasId = idPattern.test(c.name);
204
+ const hasBundle = c.name.includes(`[${app.bundle}]`);
205
+ // Перевіряємо наявність платформи в дужках в кінці назви
206
+ const platformPattern = new RegExp(`\\(.*${platformName}.*\\)\\s*$`);
207
+ const hasPlatform = platformPattern.test(c.name);
208
+ return hasId && hasBundle && hasPlatform;
209
+ });
210
+ }
211
+ else {
212
+ // Для General платформи
213
+ matchingCampaign = allCampaigns.filter((c) => {
214
+ // Точна перевірка ID з межами слів
215
+ const idPattern = new RegExp(`${app.id}\\b`);
216
+ const hasId = idPattern.test(c.name);
217
+ const hasBundle = c.name.includes(`[${app.bundle}]`);
218
+ // Перевіряємо відсутність платформи в дужках в кінці
219
+ // Дозволяємо будь-які інші дужки (як для гео або опису)
220
+ const noPlatformAtEnd = !/\(.*(?:iOS|Android|Desktop|Mobile).*\)\s*$/.test(c.name);
221
+ return hasId && hasBundle && noPlatformAtEnd;
222
+ });
223
+ }
224
+ const originalCampaign = await getCampaignById(ORIGINAL_CLONE_CAMPAIGN_ID);
225
+ const maxPosition = Math.max(...allCampaigns.map(c => c.position || 0));
226
+ let allDomains = await getDomains(true);
227
+ if (!allDomains) {
228
+ throw Error(`Failed to get all domains list`);
229
+ }
230
+ const maxGroupId = Math.max(...allCampaigns.map(c => c.group_id || 0));
231
+ const originalStreams = await getStreamsByCampaignId(ORIGINAL_CLONE_CAMPAIGN_ID);
232
+ let parameters = await TrafleKeitaroParameters({ bundle: app.bundle, appsflyerDevKey: app.platforms[platform].appsflyerParams?.apiToken, name: name });
233
+ const createStreamWithoutIds = async (stream, campaignId) => {
234
+ // Створюємо базовий об'єкт потоку без ID
235
+ const newStream = {
236
+ type: stream.type,
237
+ name: stream.name,
238
+ campaign_id: campaignId,
239
+ position: stream.position,
240
+ state: stream.state,
241
+ action_type: stream.action_type,
242
+ action_payload: stream.action_payload || "",
243
+ schema: stream.schema,
244
+ collect_clicks: stream.collect_clicks,
245
+ filter_or: stream.filter_or,
246
+ weight: stream.weight,
247
+ filters: stream.filters.map(filter => ({
248
+ name: filter.name,
249
+ mode: filter.mode,
250
+ payload: filter.payload
251
+ })),
252
+ offers: stream.offers.map(offer => ({
253
+ offer_id: offer.offer_id,
254
+ state: offer.state,
255
+ share: offer.share
256
+ }))
257
+ };
258
+ // Створюємо новий потік
259
+ await http_1.default.post('streams', newStream);
260
+ };
261
+ const updateOrCreateCLOStream = async (campaignId, existingStreams) => {
262
+ const cloTemplate = originalStreams.find(s => s.name === "CLO");
263
+ if (!cloTemplate)
264
+ return;
265
+ const cloStream = {
266
+ type: cloTemplate.type,
267
+ name: "CLO",
268
+ campaign_id: campaignId,
269
+ position: cloTemplate.position,
270
+ state: cloTemplate.state,
271
+ action_type: cloTemplate.action_type,
272
+ action_payload: cloTemplate.action_payload || "",
273
+ schema: cloTemplate.schema,
274
+ collect_clicks: cloTemplate.collect_clicks,
275
+ filter_or: cloTemplate.filter_or,
276
+ weight: cloTemplate.weight,
277
+ filters: [
278
+ {
279
+ name: "country",
280
+ mode: "reject",
281
+ payload: app.platforms[platform].geo || []
282
+ },
283
+ {
284
+ name: "bot",
285
+ mode: "accept",
286
+ payload: null
287
+ }
288
+ ],
289
+ offers: []
290
+ };
291
+ if (existingStreams) {
292
+ const existingCLO = existingStreams.find(s => s.name === "CLO");
293
+ if (existingCLO) {
294
+ await http_1.default.put(`streams/${existingCLO.id}`, cloStream);
295
+ return;
296
+ }
297
+ }
298
+ await http_1.default.post('streams', cloStream);
299
+ };
300
+ // Якщо знайдена існуюча кампанія
301
+ if (matchingCampaign.length > 0) {
302
+ const existingCampaign = matchingCampaign[0];
303
+ const existingStreams = await getStreamsByCampaignId(existingCampaign.id);
304
+ // Завжди оновлюємо CLO потік
305
+ await updateOrCreateCLOStream(existingCampaign.id, existingStreams);
306
+ // Для інших потоків
307
+ if (addDefaultStreams) {
308
+ for (const stream of originalStreams) {
309
+ if (stream.name === "CLO")
310
+ continue; // Пропускаємо CLO
311
+ const existingStream = existingStreams.find(s => s.name === stream.name);
312
+ if (!existingStream) {
313
+ await createStreamWithoutIds(stream, existingCampaign.id);
314
+ }
315
+ }
316
+ }
317
+ return existingCampaign;
318
+ }
319
+ // Створення нової кампанії
320
+ let alias = generateAlias();
321
+ let payload = {
322
+ // Унікальні поля
323
+ name: platformName ? platformCampaignName : name,
324
+ alias: alias,
325
+ domain_id: originalCampaign.domain_id,
326
+ position: [maxPosition + 100],
327
+ group_id: maxGroupId + 1,
328
+ traffic_source_id: 17,
329
+ parameters: (parameters ? parameters.parameters : originalCampaign.parameters),
330
+ source: originalCampaign.source,
331
+ bind_visitors: "slo",
332
+ // Неунікальні поля з оригінальної кампанії
333
+ type: originalCampaign.type,
334
+ state: originalCampaign.state,
335
+ cost_type: originalCampaign.cost_type,
336
+ cost_value: originalCampaign.cost_value,
337
+ cost_currency: originalCampaign.cost_currency,
338
+ uniqueness_period: originalCampaign.uniqueness_period,
339
+ cookies_ttl: originalCampaign.cookies_ttl,
340
+ notes: originalCampaign.notes,
341
+ collect_clicks: originalCampaign.collect_clicks,
342
+ uniqueness_type: originalCampaign.uniqueness_type,
343
+ };
344
+ const newCampaign = await createCampaign(payload);
345
+ // Спочатку створюємо CLO потік з оновленими гео
346
+ await updateOrCreateCLOStream(newCampaign.id);
347
+ // Потім клонуємо всі інші потоки
348
+ for (const stream of originalStreams) {
349
+ if (stream.name === "CLO")
350
+ continue; // Пропускаємо CLO потік
351
+ await createStreamWithoutIds(stream, newCampaign.id);
352
+ }
353
+ await http_1.default.put(`/campaigns/${newCampaign.id}`, {
354
+ group_id: originalCampaign.group_id
355
+ });
356
+ const updatedCampaign = await getCampaignById(newCampaign.id);
357
+ return updatedCampaign;
358
+ }
105
359
  exports.TraffleKeitaroService = {
106
- addOffersToTraffleKeitaro, getTraffleOffersGroups, getTraffleAffiliateNetworks, createGroup, deleteOfferById, getAllOffers, updateOfferLinkById
360
+ addOffersToTraffleKeitaro, getTraffleOffersGroups, getTraffleAffiliateNetworks, createGroup, deleteOfferById, getAllOffers, updateOfferLinkById, cloneTraffleCampaign
107
361
  };
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { Schema } from "mongoose";
26
27
  export interface ISectionsList {
27
28
  id: number;
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { Document, Model, Schema } from "mongoose";
26
27
  export interface IOffer extends Document {
27
28
  id: number;
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { Schema } from "mongoose";
26
27
  export interface IOffersSection {
27
28
  id: number;
@@ -22,6 +22,7 @@
22
22
  /// <reference types="mongoose/types/validation" />
23
23
  /// <reference types="mongoose/types/virtuals" />
24
24
  /// <reference types="mongoose/types/inferschematype" />
25
+ /// <reference types="mongoose/types/inferrawdoctype" />
25
26
  import { Document, Schema } from "mongoose";
26
27
  export declare enum PanelUserAccessScope {
27
28
  ADMIN = 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bprotsyk/aso-core",
3
- "version": "2.1.100",
3
+ "version": "2.1.101",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
package/src/app/app.ts CHANGED
@@ -63,6 +63,7 @@ export enum EPlatform {
63
63
  export enum EDirectType{
64
64
  KEITARO_OFFER = 'keitaroOffer',
65
65
  OFFER_DIRECT = 'offerDirect',
66
+ TRAFFLE = 'traffle'
66
67
  }
67
68
 
68
69
  export interface IPublicationHistory {
@@ -24,6 +24,7 @@ export interface IKeitaroCampaign {
24
24
  collect_clicks?: any
25
25
  uniqueness_type?: any
26
26
  source?: string
27
+ bind_visitors?: string
27
28
  }
28
29
 
29
30
  export interface IKeitaroCampaignParameters {
@@ -26,6 +26,8 @@ async function getAllCampaigns(): Promise<IKeitaroCampaign[]> {
26
26
  return campaigns
27
27
  }
28
28
 
29
+
30
+
29
31
  async function cloneStreams(originalCampaignId: number, streamPositionsToClone: number[], campaignRegExp: RegExp): Promise<void> {
30
32
  // Get the original campaign's streams
31
33
  const originalStreams = await getStreamsByCampaignId(originalCampaignId)
@@ -498,6 +500,10 @@ async function cloneDirectCampaign(app: IApp, platform?: EPlatform, addDefaultSt
498
500
 
499
501
  }
500
502
 
503
+
504
+
505
+
506
+
501
507
  async function cloneDCampaign(app: IApp): Promise<IKeitaroCampaign> {
502
508
  let name = `D #${app.id} (${app.bundle})`
503
509
 
@@ -3,6 +3,7 @@ import axios from "axios";
3
3
  export default axios.create({
4
4
  baseURL: 'https://traffle-tech.com/admin_api/v1/',
5
5
  headers: {
6
- "Api-Key": `3e80a8fcf1ceb01a761457d017e43a0a`,
6
+ // "Api-Key": `3e80a8fcf1ceb01a761457d017e43a0a`,
7
+ "Api-Key": `778a14c0730abe2629e88101cb88d573`,
7
8
  },
8
9
  });
@@ -10,6 +10,7 @@ import { IKeitaroOffer, IKeitaroOffersFilter } from "index";
10
10
  import { IKeitaroClicksRequest, IKeitaroClicksRangeRequest, KeitaroClicksInterval } from "../../../keitaro/keitaro-clicks";
11
11
  import { OpenAI } from "../../../network/openai/openai";
12
12
  import { off } from "process";
13
+ import { IKeitaroCampaignParameters } from "../../../keitaro/keitaro-campaign";
13
14
 
14
15
  async function getAllOffers(): Promise<IKeitaroOffer[]> {
15
16
  const { data: offers } = await keitaroApi.get<IKeitaroOffer[]>('offers')
@@ -43,6 +44,12 @@ export interface IAffiliateNetwork {
43
44
  offers: number;
44
45
  }
45
46
 
47
+ interface IParamsGenerationsOpenAIResponse {
48
+ naming: string;
49
+ advertisingid: string;
50
+ appsflyerDeviceID: string;
51
+ }
52
+
46
53
  async function getTraffleOffersGroups(): Promise<any[]> {
47
54
  try {
48
55
  const response = await keitaroApi.get('/groups?type=offers');
@@ -147,6 +154,302 @@ async function updateOfferLinkById(id: number, action_payload: string | object):
147
154
  }
148
155
 
149
156
 
157
+ async function getCampaignById(id: number): Promise<IKeitaroCampaign> {
158
+ const { data: campaign } = await keitaroApi.get(`/campaigns/${id}`);
159
+ return campaign
160
+ }
161
+
162
+ async function getAllCampaigns(): Promise<IKeitaroCampaign[]> {
163
+ const { data: campaigns } = await keitaroApi.get<IKeitaroCampaign[]>('campaigns');
164
+ return campaigns
165
+ }
166
+ async function getDomains(onlyActive?: boolean): Promise<IKeitaroDomain[]> {
167
+ const { data: domains } = await keitaroApi.get<IKeitaroDomain[]>(`/domains`)
168
+ return onlyActive ? domains.filter((d) => d.network_status == "active") : domains
169
+ }
170
+
171
+ async function getStreamsByCampaignId(campaignId: number): Promise<IKeitaroStream[]> {
172
+ const { data: streams } = await keitaroApi.get<IKeitaroStream[]>(`campaigns/${campaignId}/streams`);
173
+
174
+ return streams
175
+ }
176
+ async function createCampaign(campaignData: Partial<IKeitaroCampaign>): Promise<IKeitaroCampaign> {
177
+ let { data: campaign } = await keitaroApi.post(`/campaigns`, campaignData)
178
+
179
+ return campaign
180
+ }
181
+
182
+
183
+ async function TrafleKeitaroParameters(parameters: any) {
184
+ const {bundle, appsflyerDevKey, name} = parameters
185
+
186
+ let openAIResponse = await OpenAI.requestOpenAI({
187
+ model: "o3-mini",
188
+ messages: [
189
+ { role: "user", content: `sid for ${bundle} ${name} generations Generate **one** random English word in full lowercase (still lowercase, no numbers or special characters) for the parameters "naming", "advertisingid", "appsflyerDeviceID". Return **ONLY** an JSON object: { "naming": "word", "advertisingid": "word", "appsflyerDeviceID": "word"}`},
190
+ ]
191
+ })
192
+
193
+ if (!openAIResponse) {
194
+ return null;
195
+ }
196
+
197
+ let ParamsGenerationsOpenAIResponse: IParamsGenerationsOpenAIResponse | null = null;
198
+ try {
199
+ ParamsGenerationsOpenAIResponse = JSON.parse(openAIResponse);
200
+ } catch (error) {
201
+ console.error("Failed to parse OpenAI response:", error);
202
+ return null;
203
+ }
204
+
205
+ console.log(`ParamsGenerationsOpenAIResponse: ${JSON.stringify(ParamsGenerationsOpenAIResponse)}`)
206
+
207
+
208
+ return {
209
+ parameters: {
210
+ keyword: { name: 'keyword', placeholder: '', alias: '' },
211
+ cost: { name: 'cost', placeholder: '', alias: '' },
212
+ currency: { name: 'currency', placeholder: '', alias: '' },
213
+ external_id: { name: 'external_id', placeholder: '', alias: '' },
214
+ creative_id: { name: 'creative_id', placeholder: '', alias: '' },
215
+ ad_campaign_id: { name: 'ad_campaign_id', placeholder: '', alias: '' },
216
+ source: { name: 'source', placeholder: '', alias: '' },
217
+ sub_id_1: { name: 'uqID', placeholder: '', alias: '' },
218
+ sub_id_2: { name: 'AD_ID', placeholder: '', alias: '' },
219
+ sub_id_3: { name: 'USB', placeholder: '', alias: '' },
220
+ sub_id_4: { name: 'DEV', placeholder: '', alias: '' },
221
+ sub_id_5: { name: 'LANG', placeholder: '', alias: '' },
222
+ sub_id_10: { name: 'market', placeholder: '', alias: '' },
223
+ sub_id_15: { name: ParamsGenerationsOpenAIResponse?.naming, placeholder: '{naming}', alias: 'Naming' },
224
+ sub_id_16: {
225
+ name: 'bundle_id',
226
+ placeholder: bundle,
227
+ alias: 'Bundle ID'
228
+ },
229
+ sub_id_17: {
230
+ name: ParamsGenerationsOpenAIResponse?.advertisingid,
231
+ placeholder: '{advertising_id}',
232
+ alias: 'Advertising ID'
233
+ },
234
+ sub_id_18: {
235
+ name: ParamsGenerationsOpenAIResponse?.appsflyerDeviceID,
236
+ placeholder: '{appsflyer_device_id}',
237
+ alias: 'Appsflyer Device ID'
238
+ },
239
+ sub_id_19: {
240
+ name: 'appsflyer_dev_key',
241
+ placeholder: appsflyerDevKey,
242
+ alias: 'Appsflyer Dev Key'
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ async function cloneTraffleCampaign(app: IApp, platform: EPlatform, addDefaultStreams?: boolean): Promise<IKeitaroCampaign | any> {
249
+
250
+ const ORIGINAL_CLONE_CAMPAIGN_ID = 1925;
251
+ const appsflyerAvailability = app.platforms[platform].appsflyerParams?.apiToken ? "[AF]" : "";
252
+ let name = `[${app.id}] ${app.name} ET [${app.bundle}] [Android] ${appsflyerAvailability} ${app.domainParams.name}`
253
+ let platformName = platform ? getPlatformName(platform) : null;
254
+ const platformCampaignName = `#${app.id} [➥] (${platformName})`;
255
+ const generateAlias = () => {
256
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
257
+ return Array.from(
258
+ { length: 8 },
259
+ () => chars.charAt(Math.floor(Math.random() * chars.length))
260
+ ).join('');
261
+ };
262
+
263
+ let allCampaigns = await getAllCampaigns();
264
+ let matchingCampaign: IKeitaroCampaign[] = [];
265
+
266
+ if (platform && platform !== EPlatform.GENERAL) {
267
+ // Для конкретної платформи
268
+ const platformName = getPlatformName(platform);
269
+ matchingCampaign = allCampaigns.filter((c) => {
270
+ // Точна перевірка ID з межами слів
271
+ const idPattern = new RegExp(`${app.id}\\b`);
272
+ const hasId = idPattern.test(c.name);
273
+ const hasBundle = c.name.includes(`[${app.bundle}]`);
274
+
275
+ // Перевіряємо наявність платформи в дужках в кінці назви
276
+ const platformPattern = new RegExp(`\\(.*${platformName}.*\\)\\s*$`);
277
+ const hasPlatform = platformPattern.test(c.name);
278
+ return hasId && hasBundle && hasPlatform;
279
+ });
280
+ } else {
281
+ // Для General платформи
282
+ matchingCampaign = allCampaigns.filter((c) => {
283
+ // Точна перевірка ID з межами слів
284
+ const idPattern = new RegExp(`${app.id}\\b`);
285
+ const hasId = idPattern.test(c.name);
286
+ const hasBundle = c.name.includes(`[${app.bundle}]`);
287
+
288
+ // Перевіряємо відсутність платформи в дужках в кінці
289
+ // Дозволяємо будь-які інші дужки (як для гео або опису)
290
+ const noPlatformAtEnd = !/\(.*(?:iOS|Android|Desktop|Mobile).*\)\s*$/.test(c.name);
291
+ return hasId && hasBundle && noPlatformAtEnd;
292
+ });
293
+ }
294
+
295
+
296
+
297
+ const originalCampaign: IKeitaroCampaign = await getCampaignById(ORIGINAL_CLONE_CAMPAIGN_ID);
298
+ const maxPosition = Math.max(...allCampaigns.map(c => c.position || 0));
299
+
300
+ let allDomains = await getDomains(true);
301
+ if (!allDomains) {
302
+ throw Error(`Failed to get all domains list`);
303
+ }
304
+
305
+
306
+ const maxGroupId = Math.max(...allCampaigns.map(c => c.group_id || 0));
307
+ const originalStreams = await getStreamsByCampaignId(ORIGINAL_CLONE_CAMPAIGN_ID);
308
+
309
+ let parameters = await TrafleKeitaroParameters({bundle: app.bundle, appsflyerDevKey: app.platforms[platform].appsflyerParams?.apiToken, name: name})
310
+
311
+ const createStreamWithoutIds = async (stream: IKeitaroStream, campaignId: number) => {
312
+ // Створюємо базовий об'єкт потоку без ID
313
+ const newStream = {
314
+ type: stream.type,
315
+ name: stream.name,
316
+ campaign_id: campaignId,
317
+ position: stream.position,
318
+ state: stream.state,
319
+ action_type: stream.action_type,
320
+ action_payload: stream.action_payload || "",
321
+ schema: stream.schema,
322
+ collect_clicks: stream.collect_clicks,
323
+ filter_or: stream.filter_or,
324
+ weight: stream.weight,
325
+ filters: stream.filters.map(filter => ({
326
+ name: filter.name,
327
+ mode: filter.mode,
328
+ payload: filter.payload
329
+ })),
330
+ offers: stream.offers.map(offer => ({
331
+ offer_id: offer.offer_id,
332
+ state: offer.state,
333
+ share: offer.share
334
+ }))
335
+ };
336
+
337
+ // Створюємо новий потік
338
+ await keitaroApi.post('streams', newStream);
339
+ };
340
+
341
+ const updateOrCreateCLOStream = async (campaignId: number, existingStreams?: IKeitaroStream[]) => {
342
+ const cloTemplate = originalStreams.find(s => s.name === "CLO");
343
+ if (!cloTemplate) return;
344
+
345
+ const cloStream = {
346
+ type: cloTemplate.type,
347
+ name: "CLO",
348
+ campaign_id: campaignId,
349
+ position: cloTemplate.position,
350
+ state: cloTemplate.state,
351
+ action_type: cloTemplate.action_type,
352
+ action_payload: cloTemplate.action_payload || "",
353
+ schema: cloTemplate.schema,
354
+ collect_clicks: cloTemplate.collect_clicks,
355
+ filter_or: cloTemplate.filter_or,
356
+ weight: cloTemplate.weight,
357
+ filters: [
358
+ {
359
+ name: "country",
360
+ mode: "reject",
361
+ payload: app.platforms[platform].geo || []
362
+ },
363
+ {
364
+ name: "bot",
365
+ mode: "accept",
366
+ payload: null
367
+ }
368
+ ],
369
+ offers: []
370
+ };
371
+
372
+ if (existingStreams) {
373
+ const existingCLO = existingStreams.find(s => s.name === "CLO");
374
+ if (existingCLO) {
375
+ await keitaroApi.put(`streams/${existingCLO.id}`, cloStream);
376
+ return;
377
+ }
378
+ }
379
+
380
+ await keitaroApi.post('streams', cloStream);
381
+ };
382
+
383
+ // Якщо знайдена існуюча кампанія
384
+ if (matchingCampaign.length > 0) {
385
+ const existingCampaign = matchingCampaign[0];
386
+ const existingStreams = await getStreamsByCampaignId(existingCampaign.id);
387
+
388
+ // Завжди оновлюємо CLO потік
389
+ await updateOrCreateCLOStream(existingCampaign.id, existingStreams);
390
+
391
+ // Для інших потоків
392
+ if (addDefaultStreams) {
393
+ for (const stream of originalStreams) {
394
+ if (stream.name === "CLO") continue; // Пропускаємо CLO
395
+
396
+ const existingStream = existingStreams.find(s => s.name === stream.name);
397
+ if (!existingStream) {
398
+ await createStreamWithoutIds(stream, existingCampaign.id);
399
+ }
400
+ }
401
+ }
402
+
403
+ return existingCampaign;
404
+ }
405
+
406
+ // Створення нової кампанії
407
+ let alias = generateAlias();
408
+
409
+ let payload: Partial<IKeitaroCampaign> = {
410
+ // Унікальні поля
411
+ name: platformName ? platformCampaignName : name,
412
+ alias: alias,
413
+ domain_id: originalCampaign.domain_id,
414
+ position: [maxPosition + 100],
415
+ group_id: maxGroupId + 1,
416
+ traffic_source_id: 17,
417
+ parameters: (parameters ? parameters.parameters : originalCampaign.parameters) as IKeitaroCampaignParameters,
418
+ source: originalCampaign.source,
419
+ bind_visitors: "slo",
420
+ // Неунікальні поля з оригінальної кампанії
421
+ type: originalCampaign.type,
422
+ state: originalCampaign.state,
423
+ cost_type: originalCampaign.cost_type,
424
+ cost_value: originalCampaign.cost_value,
425
+ cost_currency: originalCampaign.cost_currency,
426
+ uniqueness_period: originalCampaign.uniqueness_period,
427
+ cookies_ttl: originalCampaign.cookies_ttl,
428
+ notes: originalCampaign.notes,
429
+ collect_clicks: originalCampaign.collect_clicks,
430
+ uniqueness_type: originalCampaign.uniqueness_type,
431
+ };
432
+
433
+ const newCampaign: IKeitaroCampaign = await createCampaign(payload);
434
+
435
+ // Спочатку створюємо CLO потік з оновленими гео
436
+ await updateOrCreateCLOStream(newCampaign.id);
437
+
438
+ // Потім клонуємо всі інші потоки
439
+ for (const stream of originalStreams) {
440
+ if (stream.name === "CLO") continue; // Пропускаємо CLO потік
441
+ await createStreamWithoutIds(stream, newCampaign.id);
442
+ }
443
+
444
+ await keitaroApi.put(`/campaigns/${newCampaign.id}`, {
445
+ group_id: originalCampaign.group_id
446
+ });
447
+
448
+ const updatedCampaign = await getCampaignById(newCampaign.id);
449
+ return updatedCampaign;
450
+ }
451
+
452
+
150
453
  export const TraffleKeitaroService = {
151
- addOffersToTraffleKeitaro, getTraffleOffersGroups, getTraffleAffiliateNetworks, createGroup, deleteOfferById, getAllOffers, updateOfferLinkById
454
+ addOffersToTraffleKeitaro, getTraffleOffersGroups, getTraffleAffiliateNetworks, createGroup, deleteOfferById, getAllOffers, updateOfferLinkById, cloneTraffleCampaign
152
455
  }
package/test-keitaro.js CHANGED
@@ -1,8 +1,33 @@
1
- const { KeitaroService } = require('./lib/network/keitaro/keitaro-service');
1
+ const { TraffleKeitaroService } = require('./lib/network/keitaro/traffle/traffle-keitaro-service');
2
+ const { EPlatform } = require('./lib/app/app');
2
3
 
3
4
  async function testClone() {
4
5
  try {
5
- const result = await KeitaroService.cloneDirectCampaign({ id: 901} );
6
+ // Тестові дані для створення кампанії
7
+ const testApp = {
8
+ id: 901,
9
+ bundle: "com.test.app",
10
+ name: "Test App",
11
+ platforms: {
12
+ '@': {
13
+ geo: ["UA", "RU"],
14
+ appsflyerParams: {
15
+ apiToken: "test_token"
16
+ }
17
+ }
18
+ },
19
+ domainParams: {
20
+ name: "test.com"
21
+ }
22
+ };
23
+
24
+ // Створюємо Traffle кампанію
25
+ const result = await TraffleKeitaroService.cloneTraffleCampaign(
26
+ testApp,
27
+ EPlatform.GENERAL,
28
+ true // addDefaultStreams
29
+ );
30
+
6
31
  console.log('Cloned campaign:', result);
7
32
  } catch (error) {
8
33
  console.error('Error:', error);
package/tsc ADDED
File without changes
@@ -1,52 +0,0 @@
1
- import { KeitaroClicksInterval } from './keitaro-clicks';
2
- export declare enum KeitaroOperator {
3
- EQUALS = "EQUALS",
4
- NOT_EQUAL = "NOT_EQUAL",
5
- EQUALS_OR_GREATER_THAN = "EQUALS_OR_GREATER_THAN",
6
- EQUALS_OR_LESS_THAN = "EQUALS_OR_LESS_THAN",
7
- GREATER_THAN = "GREATER_THAN",
8
- LESS_THAN = "LESS_THAN",
9
- MATCH_REGEXP = "MATCH_REGEXP",
10
- NOT_MATCH_REGEXP = "NOT_MATCH_REGEXP",
11
- BEGINS_WITH = "BEGINS_WITH",
12
- ENDS_WITH = "ENDS_WITH",
13
- CONTAINS = "CONTAINS",
14
- NOT_CONTAIN = "NOT_CONTAIN",
15
- IN_LIST = "IN_LIST",
16
- NOT_IN_LIST = "NOT_IN_LIST",
17
- BETWEEN = "BETWEEN",
18
- IS_SET = "IS_SET",
19
- IS_NOT_SET = "IS_NOT_SET",
20
- IS_TRUE = "IS_TRUE",
21
- IS_FALSE = "IS_FALSE",
22
- HAS_LABEL = "HAS_LABEL",
23
- NOT_HAS_LABEL = "NOT_HAS_LABEL"
24
- }
25
- export interface IKeitaroConversionRangeRequest {
26
- from?: string;
27
- to?: string;
28
- timezone?: string;
29
- interval?: KeitaroClicksInterval;
30
- }
31
- export interface IKeitaroConversionFilterRequest {
32
- name: string;
33
- operator: KeitaroOperator;
34
- expression?: string | number | Array<string | number>;
35
- }
36
- export interface IKeitaroConversionSortRequest {
37
- name: string;
38
- order: 'ASC' | 'DESC';
39
- }
40
- export interface IKeitaroConversionsRequest {
41
- range: IKeitaroConversionRangeRequest;
42
- limit?: number;
43
- offset?: number;
44
- columns?: string[];
45
- filters?: IKeitaroConversionFilterRequest[];
46
- sort?: IKeitaroConversionSortRequest[];
47
- }
48
- export interface IKeitaroConversionsResponse {
49
- rows: any[];
50
- total: number;
51
- meta: string[];
52
- }
@@ -1,27 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KeitaroOperator = void 0;
4
- var KeitaroOperator;
5
- (function (KeitaroOperator) {
6
- KeitaroOperator["EQUALS"] = "EQUALS";
7
- KeitaroOperator["NOT_EQUAL"] = "NOT_EQUAL";
8
- KeitaroOperator["EQUALS_OR_GREATER_THAN"] = "EQUALS_OR_GREATER_THAN";
9
- KeitaroOperator["EQUALS_OR_LESS_THAN"] = "EQUALS_OR_LESS_THAN";
10
- KeitaroOperator["GREATER_THAN"] = "GREATER_THAN";
11
- KeitaroOperator["LESS_THAN"] = "LESS_THAN";
12
- KeitaroOperator["MATCH_REGEXP"] = "MATCH_REGEXP";
13
- KeitaroOperator["NOT_MATCH_REGEXP"] = "NOT_MATCH_REGEXP";
14
- KeitaroOperator["BEGINS_WITH"] = "BEGINS_WITH";
15
- KeitaroOperator["ENDS_WITH"] = "ENDS_WITH";
16
- KeitaroOperator["CONTAINS"] = "CONTAINS";
17
- KeitaroOperator["NOT_CONTAIN"] = "NOT_CONTAIN";
18
- KeitaroOperator["IN_LIST"] = "IN_LIST";
19
- KeitaroOperator["NOT_IN_LIST"] = "NOT_IN_LIST";
20
- KeitaroOperator["BETWEEN"] = "BETWEEN";
21
- KeitaroOperator["IS_SET"] = "IS_SET";
22
- KeitaroOperator["IS_NOT_SET"] = "IS_NOT_SET";
23
- KeitaroOperator["IS_TRUE"] = "IS_TRUE";
24
- KeitaroOperator["IS_FALSE"] = "IS_FALSE";
25
- KeitaroOperator["HAS_LABEL"] = "HAS_LABEL";
26
- KeitaroOperator["NOT_HAS_LABEL"] = "NOT_HAS_LABEL";
27
- })(KeitaroOperator = exports.KeitaroOperator || (exports.KeitaroOperator = {}));
@@ -1,2 +0,0 @@
1
- import { ConversionComparison } from '../utils/comparing';
2
- export declare function displayComparisonResults(comparison: ConversionComparison): void;
@@ -1,108 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.displayComparisonResults = void 0;
4
- function displayComparisonResults(comparison) {
5
- const isValidStatus = (status) => ['sale', 'rebill'].includes(status);
6
- let wpTotalSum = comparison.wpConversions
7
- .reduce((sum, conv) => sum + conv.sum, 0);
8
- let keitaroTotalSum = comparison.keitaroConversions
9
- .filter(conv => isValidStatus(conv.status))
10
- .reduce((sum, conv) => sum + (parseFloat(conv.revenue) || 0), 0);
11
- // Get all Keitaro rejected conversions
12
- const allKeitaroRejected = [
13
- ...comparison.rejected.keitaro,
14
- ...comparison.matches
15
- .filter(match => match.keitaro.status === 'rejected')
16
- .map(match => match.keitaro)
17
- ];
18
- // Group mismatches by project/offer
19
- const wpMismatchesByProject = new Map();
20
- const keitaroMismatchesByOffer = new Map();
21
- const rejectedByOffer = new Map();
22
- comparison.mismatches.onlyInWP.forEach(conv => {
23
- if (!wpMismatchesByProject.has(conv.project)) {
24
- wpMismatchesByProject.set(conv.project, { count: 0, sum: 0, conversions: [] });
25
- }
26
- const projectStats = wpMismatchesByProject.get(conv.project);
27
- projectStats.count++;
28
- projectStats.sum += conv.sum;
29
- projectStats.conversions.push(conv);
30
- });
31
- // Include all conversions in mismatches, including rejected ones
32
- [...comparison.mismatches.onlyInKeitaro, ...allKeitaroRejected].forEach(conv => {
33
- if (!keitaroMismatchesByOffer.has(conv.offer)) {
34
- keitaroMismatchesByOffer.set(conv.offer, { count: 0, sum: 0, conversions: [] });
35
- }
36
- const offerStats = keitaroMismatchesByOffer.get(conv.offer);
37
- offerStats.count++;
38
- offerStats.sum += parseFloat(conv.revenue) || 0;
39
- offerStats.conversions.push(conv);
40
- });
41
- // Group rejected conversions by offer
42
- allKeitaroRejected.forEach(conv => {
43
- if (!rejectedByOffer.has(conv.offer)) {
44
- rejectedByOffer.set(conv.offer, { count: 0, sum: 0, conversions: [] });
45
- }
46
- const offerStats = rejectedByOffer.get(conv.offer);
47
- offerStats.count++;
48
- offerStats.sum += parseFloat(conv.revenue) || 0;
49
- offerStats.conversions.push(conv);
50
- });
51
- console.log('\n\x1b[1mЗагальна статистика:\x1b[0m');
52
- console.log(`Всього конверсій у WP: ${comparison.wpConversions.length} (Сума: \x1b[33m$${wpTotalSum.toFixed(2)}\x1b[0m)`);
53
- console.log(`Всього конверсій у Keitaro: ${comparison.keitaroConversions.filter(conv => isValidStatus(conv.status)).length} (Сума: \x1b[33m$${keitaroTotalSum.toFixed(2)}\x1b[0m)`);
54
- console.log(`Співпадінь: ${comparison.matches.length}`);
55
- console.log(`Відхилених конверсій: ${allKeitaroRejected.length}`);
56
- const totalDiff = Math.abs(wpTotalSum - keitaroTotalSum);
57
- console.log(`\x1b[1mЗагальна різниця:\x1b[0m \x1b[33m$${totalDiff.toFixed(2)}\x1b[0m`);
58
- // Always show rejected conversions first if there are any
59
- if (allKeitaroRejected.length > 0) {
60
- const rejectedTotal = allKeitaroRejected.reduce((sum, conv) => sum + (parseFloat(conv.revenue) || 0), 0);
61
- console.log('\n\x1b[1mВідхилені конверсії в Keitaro за офферами:\x1b[0m');
62
- console.log(`Загальна кількість: ${allKeitaroRejected.length}`);
63
- console.log(`Загальна сума: \x1b[33m$${rejectedTotal.toFixed(2)}\x1b[0m`);
64
- for (const [offer, stats] of rejectedByOffer.entries()) {
65
- console.log(`\n\x1b[33m${offer}:\x1b[0m`);
66
- console.log(`Кількість: ${stats.count}, Сума: $${stats.sum.toFixed(2)}`);
67
- stats.conversions.forEach(conv => {
68
- const matchMark = comparison.matches.some(match => match.keitaro.sub_id === conv.sub_id) ? '[matched] ' : '';
69
- console.log(` ${matchMark}${conv.sub_id} | ${conv.sale_datetime.split(' ')[0]} | $${conv.revenue}`);
70
- });
71
- }
72
- }
73
- console.log('\n\x1b[1mВідсутні в Keitaro за проектами:\x1b[0m');
74
- for (const [project, stats] of wpMismatchesByProject) {
75
- console.log(`\n\x1b[31m${project}:\x1b[0m`);
76
- console.log(`Кількість: ${stats.count}, Сума: $${stats.sum.toFixed(2)}`);
77
- stats.conversions.forEach(conv => {
78
- console.log(` ${conv.clickId} | ${conv.regDate.toISOString().split('T')[0]} | $${conv.sum}`);
79
- });
80
- }
81
- console.log('\n\x1b[1mВідсутні в WP за офферами:\x1b[0m');
82
- for (const [offer, stats] of keitaroMismatchesByOffer) {
83
- console.log(`\n\x1b[32m${offer}:\x1b[0m`);
84
- console.log(`Кількість: ${stats.count}, Сума: $${stats.sum.toFixed(2)}`);
85
- stats.conversions.forEach(conv => {
86
- const statusMark = conv.status === 'rejected' ? '(rejected) ' : '';
87
- const matchMark = comparison.matches.some(match => match.keitaro.sub_id === conv.sub_id) ? '[matched] ' : '';
88
- console.log(` ${statusMark}${matchMark}${conv.sub_id} | ${conv.sale_datetime.split(' ')[0]} | $${conv.revenue}`);
89
- });
90
- }
91
- let mismatchWPSum = comparison.mismatches.onlyInWP
92
- .reduce((sum, conv) => sum + conv.sum, 0);
93
- let mismatchKeitaroSum = [...comparison.mismatches.onlyInKeitaro, ...allKeitaroRejected]
94
- .reduce((sum, conv) => sum + (parseFloat(conv.revenue) || 0), 0);
95
- console.log('\n\x1b[1mПідсумок розбіжностей:\x1b[0m');
96
- console.log(`Сума відсутніх в Keitaro: \x1b[31m$${mismatchWPSum.toFixed(2)}\x1b[0m`);
97
- console.log(`Сума відсутніх в WP: \x1b[32m$${mismatchKeitaroSum.toFixed(2)}\x1b[0m`);
98
- console.log(`Різниця у відсутніх конверсіях: \x1b[33m$${Math.abs(mismatchWPSum - mismatchKeitaroSum).toFixed(2)}\x1b[0m`);
99
- // Debug information
100
- const matchingRejected = comparison.matches.filter(match => match.keitaro.status === 'rejected').length;
101
- console.log('\n\x1b[1mДебаг інформація:\x1b[0m');
102
- console.log(`Кількість конверсій відсутніх в WP: ${comparison.mismatches.onlyInKeitaro.length + comparison.rejected.keitaro.length + matchingRejected}`);
103
- console.log(`З них:`);
104
- console.log(`- Активних: ${comparison.mismatches.onlyInKeitaro.length}`);
105
- console.log(`- Відхилених (унікальних): ${comparison.rejected.keitaro.length}`);
106
- console.log(`- Відхилених (що є в WP): ${matchingRejected}`);
107
- }
108
- exports.displayComparisonResults = displayComparisonResults;
@@ -1,2 +0,0 @@
1
- import { ConversionComparison } from './utils/comparing';
2
- export declare function displayComparisonResults(comparison: ConversionComparison): void;
@@ -1,136 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.displayComparisonResults = void 0;
27
- const keitaro_service_1 = require("./network/keitaro/keitaro-service");
28
- const keitaro_conversions_1 = require("./keitaro/keitaro-conversions");
29
- const path = __importStar(require("path"));
30
- const comparing_1 = require("./utils/comparing");
31
- function displayComparisonResults(comparison) {
32
- const isValidStatus = (status) => ['sale', 'rebill'].includes(status);
33
- // Считаем общие суммы
34
- let wpTotalSum = comparison.wpConversions
35
- .reduce((sum, conv) => sum + conv.sum, 0);
36
- let keitaroTotalSum = comparison.keitaroConversions
37
- .filter((conv) => isValidStatus(conv.status))
38
- .reduce((sum, conv) => sum + (parseFloat(conv.revenue) || 0), 0);
39
- // Находим разницу
40
- const totalDiff = keitaroTotalSum - wpTotalSum;
41
- console.log('\n\x1b[1mЗагальна статистика:\x1b[0m');
42
- console.log(`WP сума: $${wpTotalSum.toFixed(2)}`);
43
- console.log(`Keitaro сума: $${keitaroTotalSum.toFixed(2)}`);
44
- console.log(`Різниця: $${totalDiff.toFixed(2)}`);
45
- // Получаем конверсии, которые есть в Keitaro, но нет в WP
46
- const extraKeitaroConversions = comparison.mismatches.onlyInKeitaro
47
- .filter(conv => isValidStatus(conv.status))
48
- .sort((a, b) => (parseFloat(b.revenue) || 0) - (parseFloat(a.revenue) || 0));
49
- // Группируем по офферам для удобства
50
- const byOffer = new Map();
51
- extraKeitaroConversions.forEach(conv => {
52
- const offer = conv.offer || 'Unknown Offer';
53
- if (!byOffer.has(offer)) {
54
- byOffer.set(offer, { count: 0, sum: 0, conversions: [] });
55
- }
56
- const stats = byOffer.get(offer);
57
- const revenue = parseFloat(conv.revenue) || 0;
58
- stats.count++;
59
- stats.sum += revenue;
60
- stats.conversions.push(conv);
61
- });
62
- console.log('\n\x1b[1mКонверсії, які є в Keitaro, але відсутні в WP:\x1b[0m');
63
- let totalExtraSum = 0;
64
- for (const [offer, stats] of byOffer) {
65
- console.log(`\n\x1b[33m${offer}:\x1b[0m`);
66
- console.log(`Кількість: ${stats.count}, Сума: $${stats.sum.toFixed(2)}`);
67
- stats.conversions.forEach(conv => {
68
- if (conv.sub_id && conv.revenue) {
69
- console.log(` ${conv.sub_id} | ${conv.revenue}`);
70
- totalExtraSum += parseFloat(conv.revenue) || 0;
71
- }
72
- });
73
- }
74
- console.log('\n\x1b[1mПідсумок:\x1b[0m');
75
- console.log(`Загальна сума "зайвих" конверсій: \x1b[33m$${totalExtraSum.toFixed(2)}\x1b[0m`);
76
- console.log('Це конверсії, які потрібно видалити з Keitaro, щоб суми співпадали з WP');
77
- }
78
- exports.displayComparisonResults = displayComparisonResults;
79
- // Self-executing function to fetch and compare conversions
80
- (async () => {
81
- const request = {
82
- range: {
83
- from: '2025-01-01',
84
- to: '2025-01-31',
85
- timezone: 'Europe/Kyiv'
86
- },
87
- filters: [
88
- {
89
- name: 'status',
90
- operator: keitaro_conversions_1.KeitaroOperator.IN_LIST,
91
- expression: ['sale', 'rejected']
92
- },
93
- {
94
- name: 'affiliate_network_id',
95
- operator: keitaro_conversions_1.KeitaroOperator.EQUALS,
96
- expression: 2
97
- }
98
- ],
99
- columns: [
100
- 'sign', 'version', 'campaign', 'campaign_group', 'landing_group', 'offer_group',
101
- 'landing', 'offer', 'affiliate_network', 'ts', 'stream', 'source',
102
- 'x_requested_with', 'referrer', 'search_engine', 'keyword', 'click_id',
103
- 'sub_id', 'visitor_code', 'campaign_id', 'campaign_group_id', 'offer_group_id',
104
- 'landing_group_id', 'landing_id', 'offer_id', 'affiliate_network_id', 'ts_id',
105
- 'stream_id', 'ad_campaign_id', 'external_id', 'creative_id',
106
- 'sub_id_1', 'sub_id_2', 'sub_id_3', 'sub_id_4', 'sub_id_5',
107
- 'sub_id_6', 'sub_id_7', 'sub_id_8', 'sub_id_9', 'sub_id_10',
108
- 'sub_id_11', 'sub_id_12', 'sub_id_13', 'sub_id_14', 'sub_id_15',
109
- 'sub_id_16', 'sub_id_17', 'sub_id_18', 'sub_id_19', 'sub_id_20',
110
- 'sub_id_21', 'sub_id_22', 'sub_id_23', 'sub_id_24', 'sub_id_25',
111
- 'sub_id_26', 'sub_id_27', 'sub_id_28', 'sub_id_29', 'sub_id_30',
112
- 'connection_type', 'operator', 'isp', 'country_code', 'country_flag',
113
- 'country', 'region', 'region_code', 'city', 'language', 'device_type',
114
- 'user_agent', 'os_icon', 'os', 'os_version', 'browser', 'browser_version',
115
- 'device_model', 'browser_icon', 'ip', 'ip_mask1', 'ip_mask2', 'ipv4only',
116
- 'ipv6only', 'cost', 'postback_datetime', 'sale_datetime', 'click_datetime',
117
- 'sale_period', 'status', 'previous_status', 'original_status',
118
- 'previous_conversion_id', 'params', 'conversion_id', 'tid',
119
- 'profitability', 'revenue', 'profit'
120
- ],
121
- sort: [{
122
- name: 'sale_datetime',
123
- order: 'DESC'
124
- }]
125
- };
126
- try {
127
- const conversions = await keitaro_service_1.KeitaroService.getConversions(request);
128
- const wpFilePath = path.join(process.cwd(), 'src', 'utils', 'wp-january.csv');
129
- const comparison = await (0, comparing_1.compareConversions)(wpFilePath, conversions);
130
- // Use our improved display function
131
- displayComparisonResults(comparison);
132
- }
133
- catch (error) {
134
- console.error('Error fetching conversions:', error);
135
- }
136
- })();
@@ -1,31 +0,0 @@
1
- export interface WPConversion {
2
- status: string;
3
- regDate: Date;
4
- qualDate: Date;
5
- approveOrRejectDate: Date;
6
- project: string;
7
- offerName: string;
8
- sum: number;
9
- currency: string;
10
- geo: string;
11
- subId?: string;
12
- clickId: string;
13
- }
14
- export interface ConversionComparison {
15
- wpConversions: WPConversion[];
16
- keitaroConversions: any[];
17
- matches: {
18
- wp: WPConversion;
19
- keitaro: any;
20
- }[];
21
- mismatches: {
22
- onlyInWP: WPConversion[];
23
- onlyInKeitaro: any[];
24
- };
25
- rejected: {
26
- wp: WPConversion[];
27
- keitaro: any[];
28
- };
29
- }
30
- export declare function parseWPConversions(filePath: string): Promise<WPConversion[]>;
31
- export declare function compareConversions(wpFilePath: string, keitaroConversions: any[]): Promise<ConversionComparison>;
@@ -1,104 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.compareConversions = exports.parseWPConversions = void 0;
27
- const sync_1 = require("csv-parse/sync");
28
- const fs = __importStar(require("fs"));
29
- function parseDate(dateStr) {
30
- const [month, day, year] = dateStr.split('/').map(num => parseInt(num));
31
- return new Date(2000 + year, month - 1, day);
32
- }
33
- async function parseWPConversions(filePath) {
34
- const fileContent = fs.readFileSync(filePath, 'utf-8');
35
- const records = (0, sync_1.parse)(fileContent, {
36
- delimiter: ';',
37
- columns: true,
38
- skip_empty_lines: true
39
- });
40
- return records.map((record) => ({
41
- status: record.Status,
42
- regDate: parseDate(record.RegDate),
43
- qualDate: parseDate(record.QualDate),
44
- approveOrRejectDate: parseDate(record.Approve_or_Reject_Date),
45
- project: record.Project,
46
- offerName: record.Offer_Name,
47
- sum: parseFloat(record.Sum),
48
- currency: record.Currency,
49
- geo: record.Geo,
50
- subId: record.Sub_ID || undefined,
51
- clickId: record.Click_ID
52
- }));
53
- }
54
- exports.parseWPConversions = parseWPConversions;
55
- async function compareConversions(wpFilePath, keitaroConversions) {
56
- const wpConversions = await parseWPConversions(wpFilePath);
57
- const matches = [];
58
- const onlyInWP = [];
59
- const onlyInKeitaro = [];
60
- const rejectedWP = [];
61
- const rejectedKeitaro = [];
62
- // Create a map of Keitaro conversions by their sub_id to match with WP's Click_ID
63
- const keitaroMap = new Map(keitaroConversions.map(conv => [conv.sub_id, conv]));
64
- // First, find matches and conversions only in WP
65
- for (const wpConv of wpConversions) {
66
- // Skip rejected conversions
67
- if (wpConv.status === 'Удержан') {
68
- rejectedWP.push(wpConv);
69
- continue;
70
- }
71
- const keitaroMatch = keitaroMap.get(wpConv.clickId);
72
- if (keitaroMatch) {
73
- matches.push({ wp: wpConv, keitaro: keitaroMatch });
74
- keitaroMap.delete(wpConv.clickId);
75
- }
76
- else {
77
- onlyInWP.push(wpConv);
78
- }
79
- }
80
- // Remaining conversions in keitaroMap are only in Keitaro
81
- // We need to check their status before adding them to onlyInKeitaro
82
- for (const [_, conv] of keitaroMap) {
83
- if (conv.status === 'rejected') {
84
- rejectedKeitaro.push(conv);
85
- }
86
- else {
87
- onlyInKeitaro.push(conv);
88
- }
89
- }
90
- return {
91
- wpConversions: wpConversions.filter(conv => conv.status !== 'Удержан'),
92
- keitaroConversions: keitaroConversions.filter(conv => conv.status !== 'rejected'),
93
- matches,
94
- mismatches: {
95
- onlyInWP,
96
- onlyInKeitaro
97
- },
98
- rejected: {
99
- wp: rejectedWP,
100
- keitaro: rejectedKeitaro
101
- }
102
- };
103
- }
104
- exports.compareConversions = compareConversions;