@embedreach/components 0.3.25 → 0.3.27

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.
@@ -20760,6 +20760,7 @@ const createPartnerLocationsFactory = (platformConfig, businessId, platformId, c
20760
20760
  phone: f.phone.number(),
20761
20761
  email: f.internet.email(),
20762
20762
  website: `https://${platformConfig.name.toLowerCase().replace(/\s+/g, "-")}.com`,
20763
+ googleBusinessProfilePlaceId: null,
20763
20764
  businessId,
20764
20765
  platformId,
20765
20766
  createdAt: f.date.past(),
@@ -20835,6 +20836,7 @@ const createReputationConfigFactory = (platformConfig, businessId, partnerLocati
20835
20836
  id: f.string.uuid(),
20836
20837
  businessId,
20837
20838
  locationMappings,
20839
+ // Backend provides this by reading from partner locations
20838
20840
  emailChannelSenderId: null,
20839
20841
  // Will be set when user configures
20840
20842
  smsChannelSenderId: null,
@@ -21432,11 +21434,7 @@ const createWebPresenceContentFactory = (platformConfig, businessConfig) => {
21432
21434
  const createSandboxMeasureAndAcquireData = (platformId, businessId) => {
21433
21435
  const platformConfigKey = getPlatformConfigKey(platformId);
21434
21436
  const platformConfig = {
21435
- id: platformConfigKey,
21436
- name: platformConfigKey === "goose" ? "ACME Pet Spa" : platformConfigKey === "renterra" ? "Tomers Rentals" : "Default",
21437
- brandColors: [],
21438
- locationNames: [],
21439
- locationAreas: []
21437
+ id: platformConfigKey
21440
21438
  };
21441
21439
  const businessConfig = getBusinessConfig(platformId);
21442
21440
  const businessesMe = createBusinessDataFactory(
@@ -21703,7 +21701,7 @@ const native = {
21703
21701
  randomUUID
21704
21702
  };
21705
21703
  function v4(options, buf, offset) {
21706
- if (native.randomUUID && !buf && !options) {
21704
+ if (native.randomUUID && true && !options) {
21707
21705
  return native.randomUUID();
21708
21706
  }
21709
21707
  options = options || {};
@@ -22162,7 +22160,7 @@ const measureAndAcquireHandlers = [
22162
22160
  locations: []
22163
22161
  }
22164
22162
  },
22165
- tracking_template: "{lpurl}?gclid={gclid}&er_campaign_id={campaignid}&utm_source=google&campaignid={campaignid}&adgroupid={adgroupid}&random={random}",
22163
+ tracking_template: "{lpurl}?gclid={gclid}&er_campaign_id={campaignid}&utm_source=google&campaignid={campaignid}&adgroupid={adgroupid}&keyword={keyword}&random={random}",
22166
22164
  reach_managed: true,
22167
22165
  created_at: now,
22168
22166
  updated_at: now
@@ -22801,7 +22799,7 @@ const generateReviewAnalyticsData = ({
22801
22799
  const platformConfig = getPlatformConfig(
22802
22800
  platformData.businessesMe?.platform_id
22803
22801
  );
22804
- const isAllTimeQuery = !queryParams.start_date || !queryParams.end_date;
22802
+ const isAllTimeQuery = !queryParams.start_date || !queryParams.end_date || queryParams.start_date && queryParams.start_date.startsWith("2020-");
22805
22803
  let scalingFactor = 1;
22806
22804
  if (!isAllTimeQuery) {
22807
22805
  const { requestedDays } = parseDateRange(
@@ -22816,14 +22814,15 @@ const generateReviewAnalyticsData = ({
22816
22814
  platformConfig,
22817
22815
  scalingFactor,
22818
22816
  false,
22819
- isAllTimeQuery
22817
+ isAllTimeQuery || false
22820
22818
  );
22821
- if (queryParams.include_comparison) {
22819
+ const includeComparison = Boolean(queryParams.include_comparison);
22820
+ if (includeComparison) {
22822
22821
  const previousPeriodData = generateReviewPeriodData(
22823
22822
  platformConfig,
22824
22823
  scalingFactor,
22825
22824
  true,
22826
- isAllTimeQuery
22825
+ isAllTimeQuery || false
22827
22826
  );
22828
22827
  const comparison = calculateReviewComparison(
22829
22828
  currentPeriodData,
@@ -22846,13 +22845,14 @@ const generateReviewAnalyticsData = ({
22846
22845
  return basicResponse;
22847
22846
  };
22848
22847
  function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
22849
- const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics(platformConfig);
22848
+ const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics$1(platformConfig);
22850
22849
  const scaledTotalCount = isAllTime ? baseMetrics.totalCount : Math.max(1, Math.round(baseMetrics.totalCount * scalingFactor));
22851
- const performanceMultiplier = isPrevious ? f.number.float({ min: 0.85, max: 0.95 }) : 1;
22850
+ const performanceMultiplier = isPrevious ? 0.9 : 1;
22852
22851
  const adjustedTotalCount = Math.round(
22853
22852
  scaledTotalCount * performanceMultiplier
22854
22853
  );
22855
- const adjustedAverageRating = baseMetrics.averageRating * performanceMultiplier;
22854
+ const ratingDelta = isPrevious ? -0.2 : 0;
22855
+ const adjustedAverageRating = baseMetrics.averageRating + ratingDelta;
22856
22856
  const ratingDistribution = generateRatingDistribution(
22857
22857
  adjustedTotalCount,
22858
22858
  adjustedAverageRating
@@ -22863,21 +22863,22 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
22863
22863
  ratingDistribution
22864
22864
  };
22865
22865
  }
22866
- function getBasePlatformReviewMetrics(platformConfig) {
22866
+ function getBasePlatformReviewMetrics$1(platformConfig) {
22867
22867
  const industryMultipliers = {
22868
22868
  // Pet spa industry (Goose) - high customer satisfaction
22869
22869
  "ACME Pet Spa": {
22870
- totalCount: 45,
22870
+ totalCount: 8,
22871
+ // Much lower - reviews are rare compared to emails
22871
22872
  averageRating: 4.6
22872
22873
  },
22873
22874
  // Equipment rental (Renterra) - moderate review volume
22874
22875
  "Tomer's Rentals": {
22875
- totalCount: 28,
22876
+ totalCount: 5,
22876
22877
  averageRating: 4.2
22877
22878
  },
22878
22879
  // Default platform
22879
22880
  default: {
22880
- totalCount: 35,
22881
+ totalCount: 6,
22881
22882
  averageRating: 4.3
22882
22883
  }
22883
22884
  };
@@ -22888,19 +22889,19 @@ function getAllTimePlatformReviewMetrics(platformConfig) {
22888
22889
  const industryMultipliers = {
22889
22890
  // Pet spa industry (Goose) - high customer satisfaction
22890
22891
  "ACME Pet Spa": {
22891
- totalCount: 342,
22892
- // Much higher for all-time
22892
+ totalCount: 124,
22893
+ // Much lower - reviews are rare compared to emails
22893
22894
  averageRating: 4.3
22894
22895
  // Slightly lower all-time average
22895
22896
  },
22896
22897
  // Equipment rental (Renterra) - moderate review volume
22897
22898
  "Tomer's Rentals": {
22898
- totalCount: 189,
22899
+ totalCount: 78,
22899
22900
  averageRating: 4.1
22900
22901
  },
22901
22902
  // Default platform
22902
22903
  default: {
22903
- totalCount: 267,
22904
+ totalCount: 95,
22904
22905
  averageRating: 4.2
22905
22906
  }
22906
22907
  };
@@ -23018,58 +23019,89 @@ const generateFeedbackAnalyticsData = ({
23018
23019
  averageRating: currentPeriodData.averageRating,
23019
23020
  responseRate: currentPeriodData.responseRate,
23020
23021
  positiveResponses: currentPeriodData.positiveResponses,
23021
- negativeResponses: currentPeriodData.negativeResponses
23022
+ negativeResponses: currentPeriodData.negativeResponses,
23023
+ passiveResponses: currentPeriodData.passiveResponses,
23024
+ npsScore: currentPeriodData.npsScore
23022
23025
  };
23023
23026
  return basicResponse;
23024
23027
  };
23025
23028
  function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
23026
- const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics(platformConfig);
23029
+ const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics$1(platformConfig);
23027
23030
  const scaledTotalSent = isAllTime ? baseMetrics.totalSent : Math.max(1, Math.round(baseMetrics.totalSent * scalingFactor));
23028
- const performanceMultiplier = isPrevious ? f.number.float({ min: 0.88, max: 0.95 }) : 1;
23031
+ const performanceMultiplier = isPrevious ? 0.82 : 1;
23029
23032
  const adjustedTotalSent = Math.round(scaledTotalSent * performanceMultiplier);
23030
23033
  const responseRate = baseMetrics.responseRate * performanceMultiplier;
23031
23034
  const totalResponded = Math.round(adjustedTotalSent * (responseRate / 100));
23032
- const positiveRate = baseMetrics.positiveRate * performanceMultiplier;
23033
- const positiveResponses = Math.round(totalResponded * positiveRate);
23034
- const negativeResponses = totalResponded - positiveResponses;
23035
- const averageRating = calculateAverageFromSentiment(
23036
- positiveResponses,
23037
- negativeResponses
23035
+ let promoterRate, passiveRate;
23036
+ if (isPrevious) {
23037
+ promoterRate = 0.5;
23038
+ passiveRate = 0.3;
23039
+ } else {
23040
+ promoterRate = baseMetrics.promoterRate * performanceMultiplier;
23041
+ passiveRate = baseMetrics.passiveRate * performanceMultiplier;
23042
+ }
23043
+ const promoters = Math.round(totalResponded * promoterRate);
23044
+ const passives = Math.round(totalResponded * passiveRate);
23045
+ const detractors = totalResponded - promoters - passives;
23046
+ const npsScore = totalResponded > 0 ? Math.round((promoters - detractors) / totalResponded * 100) : 0;
23047
+ const averageRating = calculateAverageFromNPS(
23048
+ promoters,
23049
+ passives,
23050
+ detractors
23038
23051
  );
23039
23052
  return {
23040
23053
  totalSent: adjustedTotalSent,
23041
23054
  totalResponded,
23042
23055
  averageRating: parseFloat(averageRating.toFixed(1)),
23043
23056
  responseRate: parseFloat(responseRate.toFixed(1)),
23044
- positiveResponses,
23045
- negativeResponses
23057
+ positiveResponses: promoters,
23058
+ // promoters for backward compatibility
23059
+ negativeResponses: detractors,
23060
+ // detractors for backward compatibility
23061
+ passiveResponses: passives,
23062
+ // new field for passives
23063
+ npsScore
23064
+ // real NPS score
23046
23065
  };
23047
23066
  }
23048
- function getBasePlatformFeedbackMetrics(platformConfig) {
23067
+ function getBasePlatformFeedbackMetrics$1(platformConfig) {
23049
23068
  const industryMultipliers = {
23050
23069
  // Pet spa industry (Goose) - high engagement, positive feedback
23051
23070
  "ACME Pet Spa": {
23052
- totalSent: 85,
23071
+ totalSent: 1250,
23072
+ // Much higher - businesses send way more emails than get reviews
23053
23073
  responseRate: 42,
23054
23074
  // 42% response rate
23055
- positiveRate: 0.78
23056
- // 78% positive
23075
+ promoterRate: 0.65,
23076
+ // 65% promoters (9-10) - much higher NPS
23077
+ passiveRate: 0.2,
23078
+ // 20% passives (7-8)
23079
+ detractorRate: 0.15
23080
+ // 15% detractors (1-6) - much lower
23057
23081
  },
23058
23082
  // Equipment rental (Renterra) - moderate engagement
23059
23083
  "Tomer's Rentals": {
23060
- totalSent: 65,
23084
+ totalSent: 980,
23061
23085
  responseRate: 35,
23062
23086
  // 35% response rate
23063
- positiveRate: 0.72
23064
- // 72% positive
23087
+ promoterRate: 0.55,
23088
+ // 55% promoters
23089
+ passiveRate: 0.25,
23090
+ // 25% passives
23091
+ detractorRate: 0.2
23092
+ // 20% detractors
23065
23093
  },
23066
23094
  // Default platform
23067
23095
  default: {
23068
- totalSent: 75,
23096
+ totalSent: 1100,
23069
23097
  responseRate: 38,
23070
23098
  // 38% response rate
23071
- positiveRate: 0.75
23072
- // 75% positive
23099
+ promoterRate: 0.6,
23100
+ // 60% promoters
23101
+ passiveRate: 0.22,
23102
+ // 22% passives
23103
+ detractorRate: 0.18
23104
+ // 18% detractors
23073
23105
  }
23074
23106
  };
23075
23107
  const baseKey = platformConfig.name in industryMultipliers ? platformConfig.name : "default";
@@ -23079,37 +23111,52 @@ function getAllTimePlatformFeedbackMetrics(platformConfig) {
23079
23111
  const industryMultipliers = {
23080
23112
  // Pet spa industry (Goose) - high engagement, positive feedback
23081
23113
  "ACME Pet Spa": {
23082
- totalSent: 1247,
23083
- // Much higher for all-time
23114
+ totalSent: 18500,
23115
+ // WAY higher - businesses send tons of emails over time
23084
23116
  responseRate: 39,
23085
23117
  // Slightly lower all-time response rate
23086
- positiveRate: 0.76
23087
- // Slightly lower positive rate
23118
+ promoterRate: 0.61,
23119
+ // 61% promoters - produces NPS of 42
23120
+ passiveRate: 0.2,
23121
+ // 20% passives
23122
+ detractorRate: 0.19
23123
+ // 19% detractors
23088
23124
  },
23089
23125
  // Equipment rental (Renterra) - moderate engagement
23090
23126
  "Tomer's Rentals": {
23091
- totalSent: 892,
23127
+ totalSent: 14200,
23092
23128
  responseRate: 33,
23093
- positiveRate: 0.7
23129
+ promoterRate: 0.58,
23130
+ // 58% promoters - good NPS
23131
+ passiveRate: 0.22,
23132
+ // 22% passives
23133
+ detractorRate: 0.2
23134
+ // 20% detractors
23094
23135
  },
23095
23136
  // Default platform
23096
23137
  default: {
23097
- totalSent: 1089,
23138
+ totalSent: 16500,
23098
23139
  responseRate: 36,
23099
- positiveRate: 0.73
23140
+ promoterRate: 0.62,
23141
+ // 62% promoters - good NPS
23142
+ passiveRate: 0.2,
23143
+ // 20% passives
23144
+ detractorRate: 0.18
23145
+ // 18% detractors
23100
23146
  }
23101
23147
  };
23102
23148
  const baseKey = platformConfig.name in industryMultipliers ? platformConfig.name : "default";
23103
23149
  return industryMultipliers[baseKey];
23104
23150
  }
23105
- function calculateAverageFromSentiment(positiveCount, negativeCount) {
23106
- if (positiveCount === 0 && negativeCount === 0) {
23151
+ function calculateAverageFromNPS(promoters, passives, detractors) {
23152
+ const totalResponses = promoters + passives + detractors;
23153
+ if (totalResponses === 0) {
23107
23154
  return 0;
23108
23155
  }
23109
- const positiveAverage = 4.3;
23110
- const negativeAverage = 1.8;
23111
- const totalResponses = positiveCount + negativeCount;
23112
- const weightedSum = positiveCount * positiveAverage + negativeCount * negativeAverage;
23156
+ const promoterAverage = 9.5;
23157
+ const passiveAverage = 7.5;
23158
+ const detractorAverage = 3.5;
23159
+ const weightedSum = promoters * promoterAverage + passives * passiveAverage + detractors * detractorAverage;
23113
23160
  return weightedSum / totalResponses;
23114
23161
  }
23115
23162
  function calculateFeedbackComparison(currentPeriod, previousPeriod) {
@@ -23135,6 +23182,12 @@ function calculateFeedbackComparison(currentPeriod, previousPeriod) {
23135
23182
  const responseRateChangePercent = previousPeriod.responseRate > 0 ? parseFloat(
23136
23183
  (responseRateChange / previousPeriod.responseRate * 100).toFixed(1)
23137
23184
  ) : null;
23185
+ const npsScoreChange = currentPeriod.npsScore - previousPeriod.npsScore;
23186
+ const npsScoreChangePercent = previousPeriod.npsScore !== 0 ? parseFloat(
23187
+ (npsScoreChange / Math.abs(previousPeriod.npsScore) * 100).toFixed(
23188
+ 1
23189
+ )
23190
+ ) : null;
23138
23191
  return {
23139
23192
  totalSentChange,
23140
23193
  totalSentChangePercent,
@@ -23143,7 +23196,469 @@ function calculateFeedbackComparison(currentPeriod, previousPeriod) {
23143
23196
  averageRatingChange,
23144
23197
  averageRatingChangePercent,
23145
23198
  responseRateChange: parseFloat(responseRateChange.toFixed(1)),
23146
- responseRateChangePercent
23199
+ responseRateChangePercent,
23200
+ npsScoreChange,
23201
+ npsScoreChangePercent
23202
+ };
23203
+ }
23204
+ const generateAuditLogData = ({
23205
+ startDate,
23206
+ endDate,
23207
+ locationIds,
23208
+ cursor,
23209
+ limit,
23210
+ searchTerm,
23211
+ orderBy
23212
+ }) => {
23213
+ const queryStartDate = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
23214
+ const queryEndDate = endDate ? new Date(endDate) : /* @__PURE__ */ new Date();
23215
+ const queryDays = Math.ceil(
23216
+ (queryEndDate.getTime() - queryStartDate.getTime()) / (1e3 * 60 * 60 * 24)
23217
+ ) + 1;
23218
+ const baseLogCount = 100;
23219
+ const scaledLogCount = Math.max(
23220
+ 20,
23221
+ Math.round(baseLogCount * queryDays / 30)
23222
+ );
23223
+ const allLogs = generateAuditLogPool(
23224
+ queryStartDate,
23225
+ queryEndDate,
23226
+ scaledLogCount
23227
+ );
23228
+ let filteredLogs = allLogs;
23229
+ if (startDate || endDate) {
23230
+ filteredLogs = filteredLogs.filter((item) => {
23231
+ const itemDate = new Date(item.timestamp);
23232
+ if (startDate && itemDate < new Date(startDate)) return false;
23233
+ if (endDate && itemDate > new Date(endDate)) return false;
23234
+ return true;
23235
+ });
23236
+ }
23237
+ if (searchTerm && searchTerm.length >= 3) {
23238
+ const searchLower = searchTerm.toLowerCase();
23239
+ filteredLogs = filteredLogs.filter(
23240
+ (item) => item.message.toLowerCase().includes(searchLower) || item.user.toLowerCase().includes(searchLower) || item.status.toLowerCase().includes(searchLower) || item.channel.toLowerCase().includes(searchLower) || item.type.toLowerCase().includes(searchLower)
23241
+ );
23242
+ }
23243
+ if (locationIds && locationIds.length > 0) ;
23244
+ filteredLogs.sort((a2, b2) => {
23245
+ const dateA = new Date(a2.timestamp).getTime();
23246
+ const dateB = new Date(b2.timestamp).getTime();
23247
+ return orderBy.includes("desc") || orderBy === "timestamp" ? dateB - dateA : dateA - dateB;
23248
+ });
23249
+ let startIndex = 0;
23250
+ if (cursor) {
23251
+ const cursorIndex = filteredLogs.findIndex((item) => item.id === cursor);
23252
+ if (cursorIndex !== -1) {
23253
+ startIndex = cursorIndex + 1;
23254
+ }
23255
+ }
23256
+ const pageData = filteredLogs.slice(startIndex, startIndex + limit);
23257
+ const hasMore = startIndex + limit < filteredLogs.length;
23258
+ const nextCursor = hasMore && pageData.length > 0 ? pageData[pageData.length - 1].id : void 0;
23259
+ return {
23260
+ data: pageData,
23261
+ total: filteredLogs.length,
23262
+ nextCursor
23263
+ };
23264
+ };
23265
+ function generateAuditLogPool(startDate, endDate, count) {
23266
+ const logs = [];
23267
+ const timeSpan = endDate.getTime() - startDate.getTime();
23268
+ const eventSequences = [
23269
+ // Survey flow
23270
+ {
23271
+ type: "SURVEY_REQUEST",
23272
+ channel: "EMAIL",
23273
+ statuses: ["SENT", "DELIVERED", "OPENED", "COMPLETED"],
23274
+ messages: [
23275
+ "Survey request sent to customer",
23276
+ "Survey request delivered to customer",
23277
+ "Customer opened survey request",
23278
+ "Survey completed by customer"
23279
+ ]
23280
+ },
23281
+ // Review flow
23282
+ {
23283
+ type: "REVIEW_REQUEST",
23284
+ channel: "SMS",
23285
+ statuses: ["SENT", "DELIVERED"],
23286
+ messages: ["Review request sent to", "Review request delivered to"]
23287
+ },
23288
+ // Google review flow
23289
+ {
23290
+ type: "GOOGLE_REVIEW",
23291
+ channel: "EMAIL",
23292
+ statuses: ["RECEIVED", "RESPONDED"],
23293
+ messages: [
23294
+ "New Google review received from",
23295
+ "Response sent to Google review from"
23296
+ ]
23297
+ },
23298
+ // Reminder flows
23299
+ {
23300
+ type: "SURVEY_REMINDER",
23301
+ channel: "EMAIL",
23302
+ statuses: ["SENT", "DELIVERED"],
23303
+ messages: ["Survey reminder sent to", "Survey reminder delivered to"]
23304
+ },
23305
+ {
23306
+ type: "REVIEW_REMINDER",
23307
+ channel: "SMS",
23308
+ statuses: ["SENT", "DELIVERED"],
23309
+ messages: ["Review reminder sent to", "Review reminder delivered to"]
23310
+ }
23311
+ ];
23312
+ for (let i2 = 0; i2 < count; i2++) {
23313
+ const sequence = f.helpers.arrayElement(eventSequences);
23314
+ const statusIndex = f.number.int({
23315
+ min: 0,
23316
+ max: sequence.statuses.length - 1
23317
+ });
23318
+ const status = sequence.statuses[statusIndex];
23319
+ const messageTemplate = sequence.messages[statusIndex];
23320
+ const timestamp = new Date(startDate.getTime() + Math.random() * timeSpan);
23321
+ let contactInfo;
23322
+ if (sequence.channel === "EMAIL") {
23323
+ contactInfo = f.internet.email();
23324
+ } else {
23325
+ contactInfo = f.phone.number();
23326
+ }
23327
+ const user = status === "RESPONDED" ? f.internet.email({ provider: "business.com" }) : "system";
23328
+ const customerName = status === "RECEIVED" || status === "RESPONDED" ? `${f.person.firstName()} ${f.person.lastName().charAt(0)}.` : null;
23329
+ let message2;
23330
+ if (customerName) {
23331
+ message2 = `${messageTemplate} ${customerName}`;
23332
+ } else {
23333
+ message2 = `${messageTemplate} ${contactInfo}`;
23334
+ }
23335
+ logs.push({
23336
+ id: f.string.uuid(),
23337
+ timestamp: timestamp.toISOString(),
23338
+ message: message2,
23339
+ user,
23340
+ status,
23341
+ channel: sequence.channel,
23342
+ type: sequence.type
23343
+ });
23344
+ }
23345
+ return logs;
23346
+ }
23347
+ const generateReviewTimeSeriesData = ({
23348
+ platformData,
23349
+ queryParams
23350
+ }) => {
23351
+ const platformConfig = getPlatformConfig(
23352
+ platformData.businessesMe?.platform_id
23353
+ );
23354
+ const { requestedDays, startDate, endDate } = parseDateRange(
23355
+ queryParams.start_date,
23356
+ queryParams.end_date,
23357
+ 30
23358
+ );
23359
+ const baseMetrics = getBasePlatformReviewMetrics(platformConfig);
23360
+ const basePeriodDays = 30;
23361
+ const scalingFactor = requestedDays / basePeriodDays;
23362
+ const totalScaledReviews = Math.max(
23363
+ 1,
23364
+ Math.round(baseMetrics.totalReviews * scalingFactor)
23365
+ );
23366
+ let intervalDays = 7;
23367
+ if (requestedDays <= 30)
23368
+ intervalDays = 1;
23369
+ else if (requestedDays <= 90) intervalDays = 3;
23370
+ const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23371
+ const dataPoints = generateReviewTimeSeriesPoints({
23372
+ totalScaledReviews,
23373
+ startDate,
23374
+ endDate,
23375
+ numDataPoints,
23376
+ intervalDays
23377
+ });
23378
+ return {
23379
+ data: dataPoints
23380
+ };
23381
+ };
23382
+ function generateReviewTimeSeriesPoints({
23383
+ totalScaledReviews,
23384
+ startDate,
23385
+ endDate,
23386
+ numDataPoints,
23387
+ intervalDays
23388
+ }) {
23389
+ const dataPoints = [];
23390
+ let currentDate = new Date(startDate);
23391
+ let cumulativeReviews = 0;
23392
+ const basePointReviews = totalScaledReviews / numDataPoints;
23393
+ const targetRating = 4.2;
23394
+ const startingRating = 4;
23395
+ const endingRating = 4.2;
23396
+ for (let i2 = 0; i2 < numDataPoints; i2++) {
23397
+ const pointDateStr = currentDate.toISOString().split("T")[0];
23398
+ let currentPointReviews;
23399
+ let currentPointRating;
23400
+ if (i2 < numDataPoints - 1) {
23401
+ currentPointReviews = Math.round(
23402
+ basePointReviews * (1 + (Math.random() - 0.5) * 0.5)
23403
+ );
23404
+ const progress = i2 / (numDataPoints - 1);
23405
+ currentPointRating = startingRating + (endingRating - startingRating) * Math.pow(progress, 1.3);
23406
+ currentPointRating *= 1 + (Math.random() - 0.5) * 0.05;
23407
+ } else {
23408
+ currentPointReviews = Math.max(0, totalScaledReviews - cumulativeReviews);
23409
+ currentPointRating = targetRating;
23410
+ }
23411
+ currentPointReviews = Math.max(0, currentPointReviews);
23412
+ currentPointRating = Math.max(1, Math.min(5, currentPointRating));
23413
+ cumulativeReviews += currentPointReviews;
23414
+ const { promoterCount, detractorCount } = calculatePromoterDetractorSplit(
23415
+ currentPointReviews,
23416
+ currentPointRating
23417
+ );
23418
+ dataPoints.push({
23419
+ date: pointDateStr,
23420
+ totalReviews: currentPointReviews,
23421
+ averageRating: currentPointReviews > 0 ? parseFloat(currentPointRating.toFixed(1)) : null,
23422
+ promoterCount,
23423
+ detractorCount
23424
+ });
23425
+ currentDate.setDate(currentDate.getDate() + intervalDays);
23426
+ if (currentDate > endDate && i2 < numDataPoints - 1) {
23427
+ currentDate = new Date(endDate);
23428
+ if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23429
+ break;
23430
+ }
23431
+ }
23432
+ }
23433
+ return dataPoints;
23434
+ }
23435
+ function getBasePlatformReviewMetrics(platformConfig) {
23436
+ const industryMetrics = {
23437
+ // Pet spa industry (Goose) - high customer satisfaction, more reviews
23438
+ "ACME Pet Spa": {
23439
+ totalReviews: 52,
23440
+ // Slightly higher than analytics factory
23441
+ averageRating: 4.5,
23442
+ promoterRate: 0.65,
23443
+ // 65% 5-star
23444
+ detractorRate: 0.15
23445
+ // 15% 1-3 star
23446
+ },
23447
+ // Equipment rental (Renterra) - moderate review volume, good satisfaction
23448
+ "Tomer's Rentals": {
23449
+ totalReviews: 31,
23450
+ averageRating: 4.2,
23451
+ promoterRate: 0.55,
23452
+ // 55% 5-star
23453
+ detractorRate: 0.2
23454
+ // 20% 1-3 star
23455
+ },
23456
+ // Default platform
23457
+ default: {
23458
+ totalReviews: 95,
23459
+ // Match all-time analytics exactly
23460
+ averageRating: 4.2,
23461
+ // Match all-time analytics exactly
23462
+ promoterRate: 0.6,
23463
+ // 60% 5-star
23464
+ detractorRate: 0.18
23465
+ // 18% 1-3 star
23466
+ }
23467
+ };
23468
+ const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
23469
+ return industryMetrics[baseKey];
23470
+ }
23471
+ function calculatePromoterDetractorSplit(totalReviews, averageRating) {
23472
+ if (totalReviews === 0) {
23473
+ return { promoterCount: 0, detractorCount: 0 };
23474
+ }
23475
+ let promoterRate = 0.6;
23476
+ let detractorRate = 0.15;
23477
+ if (averageRating >= 4.5) {
23478
+ promoterRate = 0.7;
23479
+ detractorRate = 0.1;
23480
+ } else if (averageRating >= 4.2) {
23481
+ promoterRate = 0.6;
23482
+ detractorRate = 0.15;
23483
+ } else if (averageRating >= 3.8) {
23484
+ promoterRate = 0.45;
23485
+ detractorRate = 0.25;
23486
+ } else {
23487
+ promoterRate = 0.3;
23488
+ detractorRate = 0.4;
23489
+ }
23490
+ promoterRate *= 1 + (Math.random() - 0.5) * 0.2;
23491
+ detractorRate *= 1 + (Math.random() - 0.5) * 0.2;
23492
+ const promoterCount = Math.round(totalReviews * promoterRate);
23493
+ const detractorCount = Math.round(totalReviews * detractorRate);
23494
+ return { promoterCount, detractorCount };
23495
+ }
23496
+ const generateFeedbackTimeSeriesData = ({
23497
+ platformData,
23498
+ queryParams
23499
+ }) => {
23500
+ const platformConfig = getPlatformConfig(
23501
+ platformData.businessesMe?.platform_id
23502
+ );
23503
+ const { requestedDays, startDate, endDate } = parseDateRange(
23504
+ queryParams.start_date,
23505
+ queryParams.end_date,
23506
+ 30
23507
+ );
23508
+ const baseMetrics = getBasePlatformFeedbackMetrics(platformConfig);
23509
+ const basePeriodDays = 30;
23510
+ const scalingFactor = requestedDays / basePeriodDays;
23511
+ const totalScaledSent = Math.max(
23512
+ 1,
23513
+ Math.round(baseMetrics.totalSent * scalingFactor)
23514
+ );
23515
+ let intervalDays = 7;
23516
+ if (requestedDays <= 30)
23517
+ intervalDays = 1;
23518
+ else if (requestedDays <= 90) intervalDays = 3;
23519
+ const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23520
+ const dataPoints = generateFeedbackTimeSeriesPoints({
23521
+ totalScaledSent,
23522
+ baseMetrics,
23523
+ startDate,
23524
+ endDate,
23525
+ numDataPoints,
23526
+ intervalDays
23527
+ });
23528
+ return {
23529
+ data: dataPoints
23530
+ };
23531
+ };
23532
+ function generateFeedbackTimeSeriesPoints({
23533
+ totalScaledSent,
23534
+ baseMetrics,
23535
+ startDate,
23536
+ endDate,
23537
+ numDataPoints,
23538
+ intervalDays
23539
+ }) {
23540
+ const dataPoints = [];
23541
+ let currentDate = new Date(startDate);
23542
+ let cumulativeSent = 0;
23543
+ const basePointSent = totalScaledSent / numDataPoints;
23544
+ const startingResponseRate = baseMetrics.responseRate * 0.75;
23545
+ const endingResponseRate = baseMetrics.responseRate * 1.15;
23546
+ const startingNPS = 30;
23547
+ const endingNPS = 42;
23548
+ for (let i2 = 0; i2 < numDataPoints; i2++) {
23549
+ const pointDateStr = currentDate.toISOString().split("T")[0];
23550
+ let currentPointSent;
23551
+ let currentPointResponseRate;
23552
+ let currentPointNPS;
23553
+ if (i2 < numDataPoints - 1) {
23554
+ currentPointSent = Math.round(
23555
+ basePointSent * (1 + (Math.random() - 0.5) * 0.5)
23556
+ );
23557
+ const progress = i2 / (numDataPoints - 1);
23558
+ currentPointResponseRate = startingResponseRate + (endingResponseRate - startingResponseRate) * Math.pow(progress, 1.3);
23559
+ currentPointNPS = startingNPS + (endingNPS - startingNPS) * Math.pow(progress, 1.2);
23560
+ currentPointResponseRate *= 1 + (Math.random() - 0.5) * 0.1;
23561
+ currentPointNPS *= 1 + (Math.random() - 0.5) * 0.1;
23562
+ } else {
23563
+ currentPointSent = Math.max(0, totalScaledSent - cumulativeSent);
23564
+ currentPointResponseRate = baseMetrics.responseRate;
23565
+ currentPointNPS = 42;
23566
+ }
23567
+ currentPointSent = Math.max(0, currentPointSent);
23568
+ currentPointResponseRate = Math.max(
23569
+ 5,
23570
+ Math.min(80, currentPointResponseRate)
23571
+ );
23572
+ currentPointNPS = Math.max(-100, Math.min(100, currentPointNPS));
23573
+ cumulativeSent += currentPointSent;
23574
+ const currentPointResponded = Math.round(
23575
+ currentPointSent * (currentPointResponseRate / 100)
23576
+ );
23577
+ const { promoterCount, detractorCount, passiveCount } = calculateNPSBreakdown(currentPointResponded, currentPointNPS);
23578
+ dataPoints.push({
23579
+ date: pointDateStr,
23580
+ totalSent: currentPointSent,
23581
+ totalResponded: currentPointResponded,
23582
+ promoterCount,
23583
+ detractorCount,
23584
+ passiveCount,
23585
+ npsScore: parseFloat(currentPointNPS.toFixed(1))
23586
+ // Use the target NPS (30 → 42)
23587
+ });
23588
+ currentDate.setDate(currentDate.getDate() + intervalDays);
23589
+ if (currentDate > endDate && i2 < numDataPoints - 1) {
23590
+ currentDate = new Date(endDate);
23591
+ if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23592
+ break;
23593
+ }
23594
+ }
23595
+ }
23596
+ return dataPoints;
23597
+ }
23598
+ function getBasePlatformFeedbackMetrics(platformConfig) {
23599
+ const industryMetrics = {
23600
+ // Pet spa industry (Goose) - high engagement, positive feedback
23601
+ "ACME Pet Spa": {
23602
+ totalSent: 1200,
23603
+ // Much higher - realistic email volume
23604
+ responseRate: 44,
23605
+ // 44% response rate
23606
+ targetNPS: 42,
23607
+ // Match the all-time NPS score (42)
23608
+ promoterRate: 0.78
23609
+ // 78% promoters
23610
+ },
23611
+ // Equipment rental (Renterra) - moderate engagement
23612
+ "Tomer's Rentals": {
23613
+ totalSent: 950,
23614
+ responseRate: 37,
23615
+ // 37% response rate
23616
+ targetNPS: 55,
23617
+ // Good NPS for B2B
23618
+ promoterRate: 0.72
23619
+ // 72% promoters
23620
+ },
23621
+ // Default platform
23622
+ default: {
23623
+ totalSent: 1050,
23624
+ responseRate: 40,
23625
+ // 40% response rate
23626
+ targetNPS: 60,
23627
+ // Good overall NPS
23628
+ promoterRate: 0.75
23629
+ // 75% promoters
23630
+ }
23631
+ };
23632
+ const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
23633
+ return industryMetrics[baseKey];
23634
+ }
23635
+ function calculateNPSBreakdown(totalResponses, targetNPS) {
23636
+ if (totalResponses === 0) {
23637
+ return { promoterCount: 0, detractorCount: 0, passiveCount: 0 };
23638
+ }
23639
+ const passiveRate = 0.25;
23640
+ const passiveCount = Math.round(totalResponses * passiveRate);
23641
+ const activeResponses = totalResponses - passiveCount;
23642
+ const npsDecimal = targetNPS / 100;
23643
+ const promoterCount = Math.round(
23644
+ (activeResponses + npsDecimal * totalResponses) / 2
23645
+ );
23646
+ const detractorCount = Math.max(0, activeResponses - promoterCount);
23647
+ const finalPromoterCount = Math.max(
23648
+ 0,
23649
+ Math.min(promoterCount, totalResponses - passiveCount)
23650
+ );
23651
+ const finalDetractorCount = Math.max(
23652
+ 0,
23653
+ Math.min(
23654
+ detractorCount,
23655
+ totalResponses - passiveCount - finalPromoterCount
23656
+ )
23657
+ );
23658
+ return {
23659
+ promoterCount: finalPromoterCount,
23660
+ detractorCount: finalDetractorCount,
23661
+ passiveCount
23147
23662
  };
23148
23663
  }
23149
23664
  const mockStore = {
@@ -23221,7 +23736,8 @@ const reputationHandlers = [
23221
23736
  locationMappings: body.locationMappings || currentConfig.locationMappings,
23222
23737
  emailChannelSenderId: body.emailChannelSenderId,
23223
23738
  smsChannelSenderId: body.smsChannelSenderId,
23224
- completedOnboardingAt: body.completedOnboardingAt,
23739
+ // Preserve existing completedOnboardingAt if not provided in update
23740
+ completedOnboardingAt: body.completedOnboardingAt || currentConfig.completedOnboardingAt,
23225
23741
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23226
23742
  };
23227
23743
  mockStore.reputationConfig = updatedConfig;
@@ -23306,6 +23822,20 @@ const reputationHandlers = [
23306
23822
  }
23307
23823
  });
23308
23824
  }),
23825
+ // OAuth GBP Connection Revoke
23826
+ http.post(`${HOSTNAME}/api/oauth/gbp/revoke`, () => {
23827
+ return HttpResponse.json(
23828
+ {
23829
+ success: false,
23830
+ message: "Error (Sandbox)",
23831
+ data: {
23832
+ success: false,
23833
+ message: "Unable to revoke sandbox account"
23834
+ }
23835
+ },
23836
+ { status: 400 }
23837
+ );
23838
+ }),
23309
23839
  // OAuth GBP Connection Status
23310
23840
  http.get(`${HOSTNAME}/api/oauth/gbp/connection`, () => {
23311
23841
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
@@ -23368,7 +23898,6 @@ const reputationHandlers = [
23368
23898
  const includeComparison = url.searchParams.get("include_comparison") === "true";
23369
23899
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23370
23900
  const queryParams = {
23371
- platform: "google_business_profile",
23372
23901
  start_date: startDate || void 0,
23373
23902
  end_date: endDate || void 0,
23374
23903
  location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0,
@@ -23384,6 +23913,37 @@ const reputationHandlers = [
23384
23913
  data: analyticsData
23385
23914
  });
23386
23915
  }),
23916
+ // Review Time-Series
23917
+ http.get(`${HOSTNAME}/api/reviews/time-series`, ({ request }) => {
23918
+ const url = new URL(request.url);
23919
+ const startDate = url.searchParams.get("start_date");
23920
+ const endDate = url.searchParams.get("end_date");
23921
+ const locationIds = url.searchParams.get("location_ids");
23922
+ if (!startDate || !endDate) {
23923
+ return HttpResponse.json(
23924
+ {
23925
+ success: false,
23926
+ message: "Missing required parameters: start_date and end_date"
23927
+ },
23928
+ { status: 400 }
23929
+ );
23930
+ }
23931
+ const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23932
+ const queryParams = {
23933
+ start_date: startDate,
23934
+ end_date: endDate,
23935
+ location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0
23936
+ };
23937
+ const timeSeriesData = generateReviewTimeSeriesData({
23938
+ platformData,
23939
+ queryParams
23940
+ });
23941
+ return HttpResponse.json({
23942
+ success: true,
23943
+ message: "Success (Sandbox)",
23944
+ data: timeSeriesData
23945
+ });
23946
+ }),
23387
23947
  // Feedback Analytics
23388
23948
  http.get(`${HOSTNAME}/api/feedback/analytics`, ({ request }) => {
23389
23949
  const url = new URL(request.url);
@@ -23409,6 +23969,60 @@ const reputationHandlers = [
23409
23969
  message: "Success (Sandbox)",
23410
23970
  data: analyticsData
23411
23971
  });
23972
+ }),
23973
+ // Feedback Time-Series
23974
+ http.get(`${HOSTNAME}/api/feedback/time-series`, ({ request }) => {
23975
+ const url = new URL(request.url);
23976
+ const startDate = url.searchParams.get("start_date");
23977
+ const endDate = url.searchParams.get("end_date");
23978
+ if (!startDate || !endDate) {
23979
+ return HttpResponse.json(
23980
+ {
23981
+ success: false,
23982
+ message: "Missing required parameters: start_date and end_date"
23983
+ },
23984
+ { status: 400 }
23985
+ );
23986
+ }
23987
+ const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23988
+ const queryParams = {
23989
+ start_date: startDate,
23990
+ end_date: endDate
23991
+ };
23992
+ const timeSeriesData = generateFeedbackTimeSeriesData({
23993
+ platformData,
23994
+ queryParams
23995
+ });
23996
+ return HttpResponse.json({
23997
+ success: true,
23998
+ message: "Success (Sandbox)",
23999
+ data: timeSeriesData
24000
+ });
24001
+ }),
24002
+ // Reputation Audit Logs
24003
+ http.get(`${HOSTNAME}/api/reputation/audit-logs`, ({ request }) => {
24004
+ const url = new URL(request.url);
24005
+ const startDate = url.searchParams.get("start_date");
24006
+ const endDate = url.searchParams.get("end_date");
24007
+ const locationIds = url.searchParams.getAll("location_ids");
24008
+ const cursor = url.searchParams.get("cursor");
24009
+ const limit = parseInt(url.searchParams.get("limit") || "25", 10);
24010
+ const searchTerm = url.searchParams.get("search_term");
24011
+ const orderBy = url.searchParams.get("order_by") || "timestamp";
24012
+ const auditLogResponse = generateAuditLogData({
24013
+ startDate,
24014
+ endDate,
24015
+ locationIds,
24016
+ cursor,
24017
+ limit,
24018
+ searchTerm,
24019
+ orderBy
24020
+ });
24021
+ return HttpResponse.json({
24022
+ success: true,
24023
+ message: "Success (Sandbox)",
24024
+ data: auditLogResponse
24025
+ });
23412
24026
  })
23413
24027
  ];
23414
24028
  const getHandlersByFeatures = (features) => {