@bprotsyk/aso-core 2.1.224 → 2.1.225

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.
@@ -1,20 +1,13 @@
1
1
  import { IKeitaroStream } from "../../keitaro/keitaro-stream";
2
- import { IOffer } from "../../offers/offer";
2
+ import { IOffer } from "../../offers/offer"
3
3
  import keitaroApi from "./http"
4
4
  import { IKeitaroCampaign } from "../../keitaro/keitaro-campaign";
5
5
  import { IKeitaroDomain } from "../../keitaro/keitaro-domain";
6
- import {
7
- IApp,
8
- IAppKeitaroData,
9
- IntegrationType,
10
- OFFERWALL_TEMPLATE_CAMPAIGN_ID,
11
- DIRECT_TEMPLATE_CAMPAIGN_ID
12
- } from "../../app/app";
13
- import { IKeitaroOffer } from "../../keitaro/keitaro-offer";
14
-
15
- // Default group IDs
16
- const DEFAULT_OFFERS_GROUP_ID = 107;
17
- const DEFAULT_CAMPAIGNS_GROUP_ID = 106;
6
+ import { EPlatform, getPlatformName, IApp } from "../../app/app";
7
+ import { TRAFFIC_SOURCE_ID_FLASH_AI, prepareOWCampaignParameters } from "../../utils/keitaro-utils";
8
+ import { convertMillisToDate, getTimestampsForTodayAndYesterday } from "../../utils/general";
9
+ import { IKeitaroOffer } from "index";
10
+ import { IKeitaroClicksRequest, IKeitaroClicksRangeRequest, KeitaroClicksInterval } from "../../keitaro/keitaro-clicks";
18
11
 
19
12
  export interface IKeitaroOffersFilter {
20
13
  keitaroId?: number,
@@ -22,9 +15,9 @@ export interface IKeitaroOffersFilter {
22
15
  caption?: string
23
16
  }
24
17
 
25
- // Basic campaign operations
26
18
  async function getStreamsByCampaignId(campaignId: number): Promise<IKeitaroStream[]> {
27
19
  const { data: streams } = await keitaroApi.get<IKeitaroStream[]>(`campaigns/${campaignId}/streams`);
20
+
28
21
  return streams
29
22
  }
30
23
 
@@ -33,213 +26,450 @@ async function getAllCampaigns(): Promise<IKeitaroCampaign[]> {
33
26
  return campaigns
34
27
  }
35
28
 
36
- async function getCampaignById(id: number): Promise<IKeitaroCampaign> {
37
- const { data: campaign } = await keitaroApi.get(`/campaigns/${id}`);
38
- return campaign
29
+
30
+
31
+ async function cloneStreams(originalCampaignId: number, streamPositionsToClone: number[], campaignRegExp: RegExp): Promise<void> {
32
+ // Get the original campaign's streams
33
+ const originalStreams = await getStreamsByCampaignId(originalCampaignId)
34
+
35
+ // Filter the original streams by the given stream positions to clone
36
+ const streamsToClone = originalStreams.filter(stream => streamPositionsToClone.includes(stream.position));
37
+
38
+ // Get a list of all campaigns
39
+ const allCampaigns = await getAllCampaigns()
40
+
41
+ // Filter the campaigns by the given RegExp
42
+ const matchingCampaigns = allCampaigns.filter(campaign => campaignRegExp.exec(campaign.name) && campaign.id != originalCampaignId);
43
+ // console.log(matchingCampaigns)
44
+
45
+ // For each matching campaign, clone the streams
46
+ for (const matchingCampaign of matchingCampaigns) {
47
+ for (const streamToClone of streamsToClone) {
48
+ await keitaroApi.post('streams', {
49
+ name: streamToClone.name,
50
+ campaign_id: matchingCampaign.id,
51
+ schema: streamToClone.schema,
52
+ type: streamToClone.type,
53
+ action_type: streamToClone.action_type,
54
+ weight: 100,
55
+ offers: streamToClone.offers.map((offer) => {
56
+ return {
57
+ offer_id: offer.offer_id,
58
+ share: 100
59
+ }
60
+ }),
61
+ filters: streamToClone.filters.map((filter) => {
62
+ return {
63
+ name: filter.name,
64
+ mode: filter.mode,
65
+ payload: filter.payload
66
+ }
67
+ }),
68
+ });
69
+ }
70
+ }
39
71
  }
40
72
 
41
73
  async function updateCampaign(id: number, payload: Partial<IKeitaroCampaign>) {
42
74
  await keitaroApi.put(`campaigns/${id}`, payload)
43
75
  }
44
76
 
45
- async function createCampaign(campaignData: Partial<IKeitaroCampaign>): Promise<IKeitaroCampaign> {
46
- let { data: campaign } = await keitaroApi.post(`/campaigns`, campaignData)
47
- return campaign
48
- }
77
+ async function createStreamForMatchingCampaigns(streamPayloads: { payload: any, offerId: string }[], campaignRegExp: RegExp, avoidGroup?: number) {
78
+ // Get all campaigns once
79
+ const allCampaigns = await getAllCampaigns();
80
+ const matchingCampaigns = allCampaigns.filter(campaign =>
81
+ (avoidGroup ? campaign.group_id != avoidGroup : true) &&
82
+ campaignRegExp.exec(campaign.name)
83
+ );
49
84
 
50
- // Domain operations
51
- async function getDomains(onlyActive?: boolean): Promise<IKeitaroDomain[]> {
52
- const { data: domains } = await keitaroApi.get<IKeitaroDomain[]>(`/domains`)
53
- return onlyActive ? domains.filter((d) => d.network_status == "active") : domains
85
+ // Get all streams for matching campaigns once
86
+ const campaignStreams = new Map<number, IKeitaroStream[]>();
87
+ for (const campaign of matchingCampaigns) {
88
+ campaignStreams.set(campaign.id, await getStreamsByCampaignId(campaign.id));
89
+ }
90
+
91
+ // Process each campaign
92
+ for (const matchingCampaign of matchingCampaigns) {
93
+ const streams = campaignStreams.get(matchingCampaign.id) || [];
94
+
95
+ // Process each stream payload
96
+ for (const { payload, offerId } of streamPayloads) {
97
+ const identicalStream = streams.find(stream => stream.name.includes(offerId));
98
+
99
+ try {
100
+ if (identicalStream) {
101
+ await keitaroApi.put(`streams/${identicalStream.id}`, {
102
+ campaign_id: matchingCampaign.id,
103
+ ...payload
104
+ });
105
+ } else {
106
+ await keitaroApi.post('streams', {
107
+ campaign_id: matchingCampaign.id,
108
+ ...payload
109
+ });
110
+ }
111
+ } catch (error) {
112
+ console.error(`Error updating stream ${offerId} (${payload.name}) in campaign ${matchingCampaign.id} (${matchingCampaign.name}):`, error);
113
+ }
114
+ }
115
+ }
54
116
  }
55
117
 
56
- // Offer operations
57
118
  async function getAllOffers(): Promise<IKeitaroOffer[]> {
58
- const { data: offers } = await keitaroApi.get<IKeitaroOffer[]>('offers');
119
+ const { data: offers } = await keitaroApi.get<IKeitaroOffer[]>('offers')
120
+
59
121
  return offers
60
122
  }
61
123
 
62
- /**
63
- * Create offer in Keitaro from our IOffer
64
- * @param offer - Our internal IOffer object
65
- * @returns Keitaro offer with ID
66
- */
67
- async function createOfferInKeitaro(offer: IOffer): Promise<IKeitaroOffer> {
68
- const payload = {
69
- name: `${offer.caption} ${offer.geo} (${offer.name})`,
70
- group_id: DEFAULT_OFFERS_GROUP_ID,
71
- action_type: 'http',
72
- action_payload: offer.link,
73
- affiliate_network_id: 0, // Can be set if we have partner mapping
74
- payout_value: 0,
75
- payout_currency: 'USD',
76
- payout_type: 'CPA',
77
- state: offer.hidden ? 'disabled' : 'active',
78
- country: [offer.geo],
79
- offer_type: 'external',
80
- payout_auto: true,
81
- payout_upsell: true,
82
- };
83
- const { data: keitaroOffer } = await keitaroApi.post<IKeitaroOffer>('offers', payload);
84
- return keitaroOffer;
124
+
125
+ async function findKeitaroOffers(filter: IKeitaroOffersFilter): Promise<IKeitaroOffer[]> {
126
+ let offers = await getAllOffers()
127
+
128
+ if (filter.caption) offers = offers.filter((o) => o.name.includes(filter.caption!))
129
+ if (filter.keitaroId) offers = offers.filter((o) => o.id == filter.keitaroId!)
130
+ if (filter.name) offers = offers.filter((o) => o.name.includes(filter.name!))
131
+
132
+ return offers
85
133
  }
86
134
 
87
- // Stream operations
88
- async function createStream(campaignId: number, streamData: Partial<IKeitaroStream>): Promise<IKeitaroStream> {
89
- const { data: stream } = await keitaroApi.post<IKeitaroStream>('streams', {
90
- ...streamData,
91
- campaign_id: campaignId
92
- });
93
- return stream;
135
+ async function getOfferByKeitaroId(id: number): Promise<IKeitaroOffer> {
136
+ const { data: offer } = await keitaroApi.get<IKeitaroOffer>(`offers/${id}`)
137
+
138
+ return offer
94
139
  }
95
140
 
96
- /**
97
- * Get all offerwall campaigns (campaigns with [◈] prefix in group 106)
98
- */
99
- async function getOfferwallCampaigns(): Promise<IKeitaroCampaign[]> {
100
- const allCampaigns = await getAllCampaigns();
101
- // Offerwall campaigns have [◈] in their name and are in group 106
102
- return allCampaigns.filter(c =>
103
- c.name.includes('[◈]') && c.group_id === DEFAULT_CAMPAIGNS_GROUP_ID
104
- );
141
+ async function updateOffer(offer: any): Promise<IKeitaroOffer> {
142
+ const { data: o } = await keitaroApi.put<IKeitaroOffer>(`offers/${offer.id}`, offer)
143
+
144
+ return o
145
+ }
146
+
147
+ function createStreamPartialPayload(keitaroOfferId: number, offerName: string, offerId: string, offerGeo: string) {
148
+ return {
149
+ name: `${offerName} ${offerGeo} (${offerId})`,
150
+ schema: "landings",
151
+ type: "regular",
152
+ action_type: "http",
153
+ weight: 100,
154
+ offers: [{
155
+ offer_id: keitaroOfferId,
156
+ share: 100,
157
+ state: "active"
158
+ }],
159
+ filters: [{
160
+ name: "sub_id_15",
161
+ mode: "accept",
162
+ payload: [offerId]
163
+ }],
164
+ }
105
165
  }
106
166
 
107
- /**
108
- * Add our IOffer to Keitaro and distribute to all offerwall campaigns
109
- * @param offer - Our internal IOffer object
110
- * @returns Result with Keitaro offer ID and distribution stats
111
- */
112
- async function addOfferToKeitaro(offer: IOffer): Promise<{
113
- keitaroOfferId: number;
114
- success: number;
115
- failed: number;
116
- campaigns: string[]
117
- }> {
118
- // First, create the offer in Keitaro
119
- const keitaroOffer = await createOfferInKeitaro(offer);
120
- console.log(`Created Keitaro offer #${keitaroOffer.id}: ${keitaroOffer.name}`);
167
+ async function addOffersToKeitaro(offers: IOffer[], affiliateId: number, links: string[], avoidGroup?: number, groupId?: number) {
168
+ const allOffers = await getAllOffers();
121
169
 
122
- // Then distribute to all offerwall campaigns
123
- const offerwallCampaigns = await getOfferwallCampaigns();
124
- let success = 0;
125
- let failed = 0;
126
- const campaignNames: string[] = [];
170
+ const streamPayloads = [];
127
171
 
128
- for (const campaign of offerwallCampaigns) {
129
- try {
130
- // Check if stream with this offer already exists
131
- const existingStreams = await getStreamsByCampaignId(campaign.id);
132
- const streamExists = existingStreams.some(stream =>
133
- stream.offers?.some(o => o.offer_id === keitaroOffer.id)
134
- );
135
-
136
- if (streamExists) {
137
- console.log(`Stream with offer ${keitaroOffer.id} already exists in campaign ${campaign.name}`);
138
- continue;
139
- }
140
-
141
- // Create a new stream with the offer
142
- await createStream(campaign.id, {
143
- name: `${offer.geo} - ${offer.caption} (${offer.name})`,
144
- type: 'regular',
145
- action_type: 'http',
146
- weight: 100,
147
- state: offer.hidden ? 'disabled' : 'active',
148
- offers: [{
149
- offer_id: keitaroOffer.id,
150
- share: 100,
151
- state: 'active'
152
- }],
153
- filters: [{
154
- name: 'country',
155
- mode: 'accept',
156
- payload: [offer.geo]
157
- }]
158
- });
172
+ for (let i = 0; i < offers.length; i++) {
173
+ const offer = offers[i];
174
+ const link = links[i];
175
+
176
+ const identicalOffer = allOffers.find(o => o.name.includes(offer.name));
177
+
178
+ let keitaroOfferId;
179
+ if (identicalOffer) {
180
+ keitaroOfferId = identicalOffer.id;
181
+ } else {
182
+ const offerPayload = {
183
+ name: `${offer.caption} ${offer.geo} (${offer.name})`,
184
+ group_id: groupId || 1,
185
+ action_payload: link,
186
+ affiliate_network_id: affiliateId,
187
+ country: [offer.geo],
188
+ action_type: "http",
189
+ offer_type: "external",
190
+ payout_auto: true,
191
+ payout_upsell: true,
192
+ };
159
193
 
160
- success++;
161
- campaignNames.push(campaign.name);
162
- } catch (error) {
163
- console.error(`Failed to add offer to campaign ${campaign.name}:`, error);
164
- failed++;
194
+ const { data: keitaroOffer } = await keitaroApi.post('offers', offerPayload);
195
+ keitaroOfferId = keitaroOffer.id;
165
196
  }
197
+
198
+ streamPayloads.push({
199
+ payload: createStreamPartialPayload(
200
+ keitaroOfferId,
201
+ offer.caption,
202
+ offer.name,
203
+ offer.geo
204
+ ),
205
+ offerId: offer.name
206
+ });
166
207
  }
167
208
 
168
- return { keitaroOfferId: keitaroOffer.id, success, failed, campaigns: campaignNames };
209
+ await createStreamForMatchingCampaigns(streamPayloads, /◈/, avoidGroup);
169
210
  }
170
211
 
171
- // Generate random alias for campaigns
172
- function generateAlias(): string {
173
- const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
174
- return Array.from(
175
- { length: 8 },
176
- () => chars.charAt(Math.floor(Math.random() * chars.length))
177
- ).join('');
212
+ async function createCampaign(campaignData: Partial<IKeitaroCampaign>): Promise<IKeitaroCampaign> {
213
+ let { data: campaign } = await keitaroApi.post(`/campaigns`, campaignData)
214
+
215
+ return campaign
178
216
  }
179
217
 
180
- /**
181
- * Clone a campaign for an app based on integration type
182
- * @param app - The app to create campaign for
183
- * @param integrationType - OFFERWALL or DIRECT
184
- * @returns Keitaro data for the cloned campaign
185
- */
186
- async function cloneCampaignForApp(app: IApp, integrationType: IntegrationType): Promise<IAppKeitaroData> {
187
- const templateId = integrationType === IntegrationType.OFFERWALL
188
- ? OFFERWALL_TEMPLATE_CAMPAIGN_ID
189
- : DIRECT_TEMPLATE_CAMPAIGN_ID;
190
-
191
- const campaignPrefix = integrationType === IntegrationType.OFFERWALL ? '[◈]' : '[➥]';
192
- const campaignName = `#${app.id} ${campaignPrefix} ${app.name || app.bundle}`;
193
-
194
- // Check if campaign already exists
195
- const allCampaigns = await getAllCampaigns();
196
- const existingCampaign = allCampaigns.find(c =>
197
- c.name.includes(`#${app.id}`) && c.name.includes(campaignPrefix)
198
- );
199
-
200
- if (existingCampaign) {
201
- return campaignToKeitaroData(existingCampaign);
218
+ async function getCampaignById(id: number): Promise<IKeitaroCampaign> {
219
+ const { data: campaign } = await keitaroApi.get(`/campaigns/${id}`);
220
+ return campaign
221
+ }
222
+
223
+ export async function upsertStreamToCampaign(campaign: IKeitaroCampaign, stream: Partial<IKeitaroStream>) {
224
+ let streams = await getStreamsByCampaignId(campaign.id)
225
+
226
+ let identicalStream = streams.find((s) => stream.name == s.name)
227
+
228
+ if (identicalStream) {
229
+ console.log(`Found identical! Name: ${stream.name}`)
230
+ await keitaroApi.put(`streams/${identicalStream.id}`, {
231
+ campaign_id: campaign.id,
232
+ ...stream
233
+ });
234
+ } else {
235
+ await keitaroApi.post('streams', {
236
+ campaign_id: campaign.id,
237
+ ...stream
238
+ });
202
239
  }
203
-
204
- // Get template campaign and its streams
205
- const templateCampaign = await getCampaignById(templateId);
206
- const templateStreams = await getStreamsByCampaignId(templateId);
207
-
208
- // Get random active domain
209
- const allDomains = await getDomains(true);
210
- if (!allDomains || allDomains.length === 0) {
211
- throw new Error('No active domains available');
240
+ }
241
+
242
+ async function cloneOWCampaign(app: IApp, platform?: EPlatform): Promise<IKeitaroCampaign | any> {
243
+ let name = `#${app.id} [◈]`
244
+ let platformName = platform ? getPlatformName(platform) : null;
245
+ const platformCampaignName = `#${app.id} [◈] (${platformName})`;
246
+ const generateAlias = () => {
247
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
248
+ return Array.from(
249
+ { length: 8 },
250
+ () => chars.charAt(Math.floor(Math.random() * chars.length))
251
+ ).join('');
252
+ };
253
+
254
+ let allCampaigns = await getAllCampaigns();
255
+
256
+ let matchingCampaign: IKeitaroCampaign[] = [];
257
+
258
+ if (platform && platform !== EPlatform.GENERAL) {
259
+ // Шукаємо кампанію з платформою
260
+ matchingCampaign = allCampaigns.filter((c) =>
261
+ c.name.includes(`#${app.id}`) &&
262
+ c.name.includes(`[◈]`) &&
263
+ c.name.includes(`(${platformName})`)
264
+ );
265
+ } else {
266
+ // Шукаємо кампанію без платформи або з суфіксом
267
+ matchingCampaign = allCampaigns.filter((c) =>
268
+ c.name.includes(`#${app.id}`) &&
269
+ c.name.includes(`[◈]`) &&
270
+ !c.name.includes('(')
271
+ );
272
+ }
273
+
274
+ if (matchingCampaign.length > 0) return matchingCampaign[0];
275
+
276
+ const originalCampaign: IKeitaroCampaign = await getCampaignById(2673);
277
+ const maxPosition = Math.max(...allCampaigns.map(c => c.position || 0));
278
+ const originalStreams = await getStreamsByCampaignId(2673);
279
+
280
+ let allDomains = await KeitaroService.getDomains(true);
281
+ if (!allDomains) {
282
+ throw Error(`Failed to get all domains list`);
212
283
  }
284
+
213
285
  const domain = allDomains[Math.floor(Math.random() * allDomains.length)];
214
-
215
- // Create campaign payload
286
+ const maxGroupId = Math.max(...allCampaigns.map(c => c.group_id || 0));
287
+
288
+ let alias = generateAlias();
289
+
290
+ let payload: Partial<IKeitaroCampaign> = {
291
+ // Унікальні поля
292
+ name: platformName ? platformCampaignName : name,
293
+ alias: alias,
294
+ domain_id: domain.id,
295
+ position: [maxPosition + 100],
296
+ group_id: maxGroupId + 1,
297
+ traffic_source_id: TRAFFIC_SOURCE_ID_FLASH_AI,
298
+ parameters: prepareOWCampaignParameters(app),
299
+
300
+ // Неунікальні поля з оригінальної кампанії
301
+ type: originalCampaign.type,
302
+ state: originalCampaign.state,
303
+ cost_type: originalCampaign.cost_type,
304
+ cost_value: originalCampaign.cost_value,
305
+ cost_currency: originalCampaign.cost_currency,
306
+ uniqueness_period: originalCampaign.uniqueness_period,
307
+ cookies_ttl: originalCampaign.cookies_ttl,
308
+ notes: originalCampaign.notes,
309
+ collect_clicks: originalCampaign.collect_clicks,
310
+ uniqueness_type: originalCampaign.uniqueness_type,
311
+ };
312
+
313
+ const newCampaign: IKeitaroCampaign = await createCampaign(payload);
314
+
315
+ for (const stream of originalStreams) {
316
+ await keitaroApi.post('streams', {
317
+ name: stream.name,
318
+ campaign_id: newCampaign.id,
319
+ schema: stream.schema,
320
+ type: stream.type,
321
+ action_type: stream.action_type,
322
+ weight: stream.weight,
323
+ offers: stream.offers.map((offer) => ({
324
+ offer_id: offer.offer_id,
325
+ share: offer.share,
326
+ state: offer.state
327
+ })),
328
+ filters: stream.filters.map((filter) => ({
329
+ name: filter.name,
330
+ mode: filter.mode,
331
+ payload: filter.payload
332
+ })),
333
+ position: stream.position,
334
+ state: stream.state
335
+ });
336
+ }
337
+ await keitaroApi.put(`/campaigns/${newCampaign.id}`, {
338
+ group_id: originalCampaign.group_id
339
+ });
340
+
341
+ const updatedCampaign = await getCampaignById(newCampaign.id);
342
+
343
+ return updatedCampaign;
344
+ }
345
+
346
+ async function cloneDirectCampaign(app: IApp, platform?: EPlatform, addDefaultStreams?: boolean): Promise<IKeitaroCampaign | any> {
347
+ let name = `#${app.id} [➥]`
348
+ let platformName = platform ? getPlatformName(platform) : null;
349
+ const platformCampaignName = `#${app.id} [➥] (${platformName})`;
350
+ const generateAlias = () => {
351
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
352
+ return Array.from(
353
+ { length: 8 },
354
+ () => chars.charAt(Math.floor(Math.random() * chars.length))
355
+ ).join('');
356
+ };
357
+
358
+ let allCampaigns = await getAllCampaigns();
359
+ let matchingCampaign: IKeitaroCampaign[] = [];
360
+
361
+ if (platform && platform !== EPlatform.GENERAL) {
362
+ // Для конкретної платформи
363
+ const platformName = getPlatformName(platform);
364
+ matchingCampaign = allCampaigns.filter((c) => {
365
+ // Точна перевірка ID з межами слів
366
+ const idPattern = new RegExp(`#${app.id}\\b`);
367
+ const hasId = idPattern.test(c.name);
368
+ // Перевіряємо наявність стрілки (може бути в комбінації з іншими символами)
369
+ const hasArrow = c.name.includes('➥');
370
+ // Перевіряємо наявність платформи в дужках в кінці назви
371
+ const platformPattern = new RegExp(`\\(.*${platformName}.*\\)\\s*$`);
372
+ const hasPlatform = platformPattern.test(c.name);
373
+ return hasId && hasArrow && hasPlatform;
374
+ });
375
+ } else {
376
+ // Для General платформи
377
+ matchingCampaign = allCampaigns.filter((c) => {
378
+ // Точна перевірка ID з межами слів
379
+ const idPattern = new RegExp(`#${app.id}\\b`);
380
+ const hasId = idPattern.test(c.name);
381
+ // Перевіряємо наявність стрілки (може бути в комбінації з іншими символами)
382
+ const hasArrow = c.name.includes('➥');
383
+ // Перевіряємо відсутність платформи в дужках в кінці
384
+ // Дозволяємо будь-які інші дужки (як для гео або опису)
385
+ const noPlatformAtEnd = !/\(.*(?:iOS|Android|Desktop|Mobile).*\)\s*$/.test(c.name);
386
+ return hasId && hasArrow && noPlatformAtEnd;
387
+ });
388
+ }
389
+
390
+ if (matchingCampaign.length > 0) {
391
+ const existingCampaign = matchingCampaign[0];
392
+
393
+ // Якщо addDefaultStreams === true, додаємо потоки до існуючої кампанії
394
+ if (addDefaultStreams) {
395
+ const originalStreams = await getStreamsByCampaignId(3175);
396
+ const existingStreams = await getStreamsByCampaignId(existingCampaign.id);
397
+
398
+ for (const stream of originalStreams) {
399
+ // Перевіряємо чи потік вже існує
400
+ const existingStream = existingStreams.find(s => s.name === stream.name);
401
+ if (!existingStream) {
402
+ await keitaroApi.post('streams', {
403
+ name: stream.name,
404
+ campaign_id: existingCampaign.id,
405
+ schema: stream.schema,
406
+ type: stream.type,
407
+ action_type: stream.action_type,
408
+ weight: stream.weight,
409
+ offers: stream.offers.map((offer) => ({
410
+ offer_id: offer.offer_id,
411
+ share: offer.share,
412
+ state: offer.state
413
+ })),
414
+ filters: stream.filters.map((filter) => ({
415
+ name: filter.name,
416
+ mode: filter.mode,
417
+ payload: filter.payload
418
+ })),
419
+ position: stream.position,
420
+ state: stream.state
421
+ });
422
+ }
423
+ }
424
+ }
425
+
426
+ return existingCampaign;
427
+ }
428
+
429
+ const originalCampaign: IKeitaroCampaign = await getCampaignById(3175);
216
430
  const maxPosition = Math.max(...allCampaigns.map(c => c.position || 0));
217
- const alias = generateAlias();
218
-
219
- const payload: Partial<IKeitaroCampaign> = {
220
- name: campaignName,
431
+ const originalStreams = await getStreamsByCampaignId(3175);
432
+
433
+ let allDomains = await KeitaroService.getDomains(true);
434
+ if (!allDomains) {
435
+ throw Error(`Failed to get all domains list`);
436
+ }
437
+
438
+ const domain = allDomains[Math.floor(Math.random() * allDomains.length)];
439
+ const maxGroupId = Math.max(...allCampaigns.map(c => c.group_id || 0));
440
+
441
+ let alias = generateAlias();
442
+
443
+ let payload: Partial<IKeitaroCampaign> = {
444
+ // Унікальні поля
445
+ name: platformName ? platformCampaignName : name,
221
446
  alias: alias,
222
447
  domain_id: domain.id,
223
448
  position: [maxPosition + 100],
224
- group_id: DEFAULT_CAMPAIGNS_GROUP_ID,
225
- type: templateCampaign.type,
226
- state: templateCampaign.state,
227
- cost_type: templateCampaign.cost_type,
228
- cost_value: templateCampaign.cost_value,
229
- cost_currency: templateCampaign.cost_currency,
230
- uniqueness_period: templateCampaign.uniqueness_period,
231
- cookies_ttl: templateCampaign.cookies_ttl,
232
- notes: templateCampaign.notes,
233
- collect_clicks: templateCampaign.collect_clicks,
234
- uniqueness_type: templateCampaign.uniqueness_type,
235
- parameters: templateCampaign.parameters,
449
+ group_id: maxGroupId + 1,
450
+ traffic_source_id: TRAFFIC_SOURCE_ID_FLASH_AI,
451
+ parameters: prepareOWCampaignParameters(app),
452
+ source: originalCampaign.source,
453
+ bind_visitors: "slo",
454
+ // Неунікальні поля з оригінальної кампанії
455
+ type: originalCampaign.type,
456
+ state: originalCampaign.state,
457
+ cost_type: originalCampaign.cost_type,
458
+ cost_value: originalCampaign.cost_value,
459
+ cost_currency: originalCampaign.cost_currency,
460
+ uniqueness_period: originalCampaign.uniqueness_period,
461
+ cookies_ttl: originalCampaign.cookies_ttl,
462
+ notes: originalCampaign.notes,
463
+ collect_clicks: originalCampaign.collect_clicks,
464
+ uniqueness_type: originalCampaign.uniqueness_type,
236
465
  };
466
+
467
+ const newCampaign: IKeitaroCampaign = await createCampaign(payload);
468
+
469
+ // Додаємо потоки: всі якщо addDefaultStreams === true, інакше тільки перший
470
+ const streamsToAdd = addDefaultStreams ? originalStreams : originalStreams.slice(0, 1);
237
471
 
238
- // Create the new campaign
239
- const newCampaign = await createCampaign(payload);
240
-
241
- // Clone streams from template
242
- for (const stream of templateStreams) {
472
+ for (const stream of streamsToAdd) {
243
473
  await keitaroApi.post('streams', {
244
474
  name: stream.name,
245
475
  campaign_id: newCampaign.id,
@@ -261,62 +491,133 @@ async function cloneCampaignForApp(app: IApp, integrationType: IntegrationType):
261
491
  state: stream.state
262
492
  });
263
493
  }
264
-
265
- // Get updated campaign with token
494
+ await keitaroApi.put(`/campaigns/${newCampaign.id}`, {
495
+ group_id: originalCampaign.group_id
496
+ });
497
+
266
498
  const updatedCampaign = await getCampaignById(newCampaign.id);
499
+
500
+ return updatedCampaign;
267
501
 
268
- return campaignToKeitaroData(updatedCampaign);
269
502
  }
270
503
 
271
- /**
272
- * Convert a Keitaro campaign to IAppKeitaroData
273
- */
274
- function campaignToKeitaroData(campaign: IKeitaroCampaign): IAppKeitaroData {
275
- return {
276
- trackingCampaignId: campaign.id,
277
- trackingCampaignName: campaign.name,
278
- trackingCampaignAlias: campaign.alias,
279
- trackingDomainId: campaign.domain_id,
280
- trackingDomainName: campaign.domain || '',
281
- campingToken: campaign.token,
282
- trackingParams: {
283
- naming: campaign.parameters?.sub_id_15?.name || 'naming',
284
- firebase_app_instance_id: campaign.parameters?.sub_id_11?.name || 'firebase_app_instance_id',
285
- firebase_user_id: campaign.parameters?.sub_id_12?.name || 'firebase_user_id',
286
- firebase_device_language_code: campaign.parameters?.sub_id_13?.name || 'firebase_device_language_code',
287
- firebase_push_token: campaign.parameters?.sub_id_14?.name || 'firebase_push_token',
288
- bundle_id: campaign.parameters?.sub_id_16?.name || 'bundle_id',
289
- advertising_id: campaign.parameters?.sub_id_17?.name || 'advertising_id',
290
- appsflyer_device_id: campaign.parameters?.sub_id_18?.name || 'appsflyer_device_id',
291
- campaign: campaign.parameters?.sub_id_20?.name || 'campaign',
292
- }
504
+
505
+
506
+
507
+
508
+ async function cloneDCampaign(app: IApp): Promise<IKeitaroCampaign> {
509
+ let name = `D #${app.id} (${app.bundle})`
510
+
511
+ let allCampaigns = await getAllCampaigns()
512
+ let matchingCampaign = allCampaigns.filter((c) => c.name.includes(`D #${app.id}`))
513
+ if (matchingCampaign.length > 0) return matchingCampaign[0]
514
+
515
+ const { data: campaigns } = await keitaroApi.post(`/campaigns/2693/clone`);
516
+
517
+ if (campaigns.length == 0) throw Error("Campaign cloning falied")
518
+
519
+ let clonedCampaign: IKeitaroCampaign = campaigns[0]
520
+
521
+ let allDomains = await KeitaroService.getDomains(true)
522
+ if (!allDomains) {
523
+ throw Error(`Failed to get all domains list`)
524
+ }
525
+
526
+ const domain = allDomains[Math.floor(Math.random() * allDomains.length)];
527
+
528
+ let payload: Partial<IKeitaroCampaign> = {
529
+ name: name,
530
+ traffic_source_id: TRAFFIC_SOURCE_ID_FLASH_AI,
531
+ domain_id: domain.id,
532
+ group_id: 93,
533
+ }
534
+
535
+ const { data: campaign } = await keitaroApi.put(`/campaigns/${clonedCampaign.id}`, payload)
536
+
537
+ return campaign
538
+ }
539
+
540
+ async function changeCampaignsGroup(fromId: number, toId: number, exceptForCampaignIds: number[], onlyForCampaignIds?: number[]) {
541
+ let campaigns = await getAllCampaigns()
542
+ const matchingCampaigns = campaigns.filter(campaign => campaign.group_id == fromId
543
+ && exceptForCampaignIds.length > 0 ? !exceptForCampaignIds.includes(campaign.id) : onlyForCampaignIds?.includes(campaign.id))
544
+
545
+ for (let campaign of matchingCampaigns) {
546
+ await updateCampaign(campaign.id, { group_id: toId })
547
+ }
548
+ }
549
+
550
+ // Domain
551
+ async function getDomains(onlyActive?: boolean): Promise<IKeitaroDomain[]> {
552
+ const { data: domains } = await keitaroApi.get<IKeitaroDomain[]>(`/domains`)
553
+ return onlyActive ? domains.filter((d) => d.network_status == "active") : domains
554
+ }
555
+
556
+ async function getProfitForTimeRange(from: number, to: number): Promise<number> {
557
+ let fromString = convertMillisToDate(from)
558
+ let toString = convertMillisToDate(to)
559
+
560
+ let { data: data } = await keitaroApi.post(`/report/build`, {
561
+ range: {
562
+ from: fromString,
563
+ to: toString,
564
+ timezone: 'Europe/Kyiv'
565
+ },
566
+ metrics: ['profit']
567
+ })
568
+
569
+ return data.rows[0].profit
570
+ }
571
+
572
+ // async function getProfitForTodayAndYesterday(): Promise<any> {
573
+ // let timestamps = getTimestampsForTodayAndYesterday()
574
+ // let today = await KeitaroService.getProfitForTimeRange(timestamps.today.start, timestamps.today.end)
575
+ // let yesterday = await KeitaroService.getProfitForTimeRange(timestamps.yesterday.start, timestamps.yesterday.end)
576
+
577
+ // return {
578
+ // today: today,
579
+ // yesterday: yesterday
580
+ // }
581
+ // }
582
+
583
+ async function fixBrokenClickCosts(startDate: string, endDate: string): Promise<void> {
584
+ // Get all campaigns to update
585
+ const campaigns = await getAllCampaigns();
586
+ const campaignIds = campaigns.map(campaign => campaign.id);
587
+
588
+ // Prepare the payload for the API request
589
+ const payload = {
590
+ campaign_ids: campaignIds,
591
+ costs: [{
592
+ start_date: startDate,
593
+ end_date: endDate,
594
+ cost: 0,
595
+ filters: {}
596
+ }],
597
+ timezone: "Europe/Kyiv",
598
+ currency: "USD",
599
+ only_campaign_uniques: 0
293
600
  };
601
+
602
+ // Make the API request to update costs
603
+ await keitaroApi.post('/clicks/update_costs', payload);
294
604
  }
295
605
 
296
- export const KeitaroService = {
297
- // Campaign operations
298
- getAllCampaigns,
299
- getCampaignById,
300
- createCampaign,
301
- updateCampaign,
302
- cloneCampaignForApp,
303
- getOfferwallCampaigns,
304
-
305
- // Stream operations
306
- getStreamsByCampaignId,
307
- createStream,
308
-
309
- // Domain operations
310
- getDomains,
311
-
312
- // Offer operations
313
- getAllOffers,
314
- createOfferInKeitaro,
315
- addOfferToKeitaro,
606
+ async function getClicks(request: IKeitaroClicksRequest | KeitaroClicksInterval) {
607
+ const defaultRequest: IKeitaroClicksRequest = typeof request === 'string' ? {
608
+ range: { interval: request }
609
+ } : {
610
+ ...request
611
+ };
316
612
 
317
- // Utilities
318
- generateAlias,
319
- campaignToKeitaroData,
613
+ const { data } = await keitaroApi.post('/clicks/log', defaultRequest);
614
+ return data.rows;
320
615
  }
321
616
 
322
- export default KeitaroService;
617
+
618
+ export const KeitaroService = {
619
+ getStreamsByCampaignId, updateCampaign, getAllCampaigns, getAllOffers, cloneStreams, addOffersToKeitaro, getOfferByKeitaroId, getDomains, createCampaign, getCampaignById, upsertStreamToCampaign, cloneOWCampaign,
620
+ updateOffer, changeCampaignsGroup, getProfitForTimeRange, getClicks,
621
+ // getProfitForTodayAndYesterday,
622
+ cloneDCampaign, findKeitaroOffers, fixBrokenClickCosts, cloneDirectCampaign
623
+ }