@embedreach/components 0.3.31 → 0.3.33

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.
@@ -20663,27 +20663,22 @@ const getPlatformConfigKey = (platformId) => {
20663
20663
  const defaultPlatformConfigs = {
20664
20664
  default: {
20665
20665
  id: "default",
20666
- name: "Sandbox",
20666
+ name: "ACME SaaS",
20667
20667
  brandColors: ["#2563eb", "#059669", "#dc2626", "#9333ea"],
20668
20668
  locationNames: ["Manhattan", "Brooklyn", "Queens", "Bronx"],
20669
20669
  locationAreas: ["New York, NY", "Brooklyn, NY", "Queens, NY", "Bronx, NY"]
20670
20670
  },
20671
20671
  goose: {
20672
20672
  id: "goose",
20673
- name: "ACME Pet Spa",
20673
+ name: "Paws Pet Hotel",
20674
20674
  brandColors: ["#f59e0b", "#dc2626", "#059669", "#7c3aed"],
20675
20675
  locationNames: [
20676
- "Pampered Paws Spa",
20677
- "Fluffy Coat Grooming",
20678
- "Tail Waggers Retreat",
20679
- "Premier Pet Palace"
20676
+ "Paws Pet Hotel Manhattan",
20677
+ "Paws Pet Hotel Brooklyn",
20678
+ "Paws Pet Hotel Queens",
20679
+ "Paws Pet Hotel Bronx"
20680
20680
  ],
20681
- locationAreas: [
20682
- "San Francisco, CA",
20683
- "Oakland, CA",
20684
- "Berkeley, CA",
20685
- "Palo Alto, CA"
20686
- ]
20681
+ locationAreas: ["Manhattan, NY", "Brooklyn, NY", "Queens, NY", "Bronx, NY"]
20687
20682
  },
20688
20683
  renterra: {
20689
20684
  id: "renterra",
@@ -20710,7 +20705,6 @@ const getPlatformConfig = (platformId) => {
20710
20705
  return defaultPlatformConfigs[configKey];
20711
20706
  };
20712
20707
  const createGbpConnectionFactory = (platformConfig, businessId, status = "connected") => {
20713
- const accountName = `${platformConfig.name} GBP Account`;
20714
20708
  const locations = Array.from(
20715
20709
  { length: Math.min(platformConfig.locationNames.length, 4) },
20716
20710
  (_2, index) => ({
@@ -20734,12 +20728,15 @@ const createGbpConnectionFactory = (platformConfig, businessId, status = "connec
20734
20728
  platform: "gbp",
20735
20729
  selectedAccount: {
20736
20730
  name: "accounts/123456789",
20737
- accountName,
20731
+ accountName: platformConfig.name,
20738
20732
  permissionLevel: "OWNER_LEVEL",
20739
20733
  verificationState: "VERIFIED",
20740
20734
  locationCount: locations.length
20741
20735
  },
20742
- selectedLocations: locations
20736
+ selectedLocations: locations.map((location2) => ({
20737
+ ...location2,
20738
+ locationState: location2.locationState
20739
+ }))
20743
20740
  },
20744
20741
  created_at: f.date.past().toISOString(),
20745
20742
  updated_at: f.date.recent().toISOString()
@@ -20760,7 +20757,7 @@ const createPartnerLocationsFactory = (platformConfig, businessId, platformId, c
20760
20757
  phone: f.phone.number(),
20761
20758
  email: f.internet.email(),
20762
20759
  website: `https://${platformConfig.name.toLowerCase().replace(/\s+/g, "-")}.com`,
20763
- googleBusinessProfilePlaceId: null,
20760
+ googleBusinessProfilePlaceId: `accounts/123456789/locations/${2e3 + index}`,
20764
20761
  businessId,
20765
20762
  platformId,
20766
20763
  createdAt: f.date.past(),
@@ -20826,12 +20823,11 @@ const createChannelDataFactory = (platformConfig) => {
20826
20823
  ];
20827
20824
  return { channelSenders, channelAccounts };
20828
20825
  };
20829
- const createReputationConfigFactory = (platformConfig, businessId, partnerLocations, gbpLocations) => {
20826
+ const createReputationConfigFactory = (platformConfig, businessId, partnerLocations) => {
20830
20827
  const locationMappings = {};
20831
- const maxMappings = Math.min(partnerLocations.length, gbpLocations.length);
20832
- for (let i2 = 0; i2 < maxMappings; i2++) {
20833
- locationMappings[partnerLocations[i2].externalId] = gbpLocations[i2].name;
20834
- }
20828
+ partnerLocations.forEach((location2) => {
20829
+ locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
20830
+ });
20835
20831
  return {
20836
20832
  id: f.string.uuid(),
20837
20833
  businessId,
@@ -20860,12 +20856,10 @@ const createSandboxReputationData = (platformId, businessId) => {
20860
20856
  );
20861
20857
  const { channelSenders, channelAccounts } = createChannelDataFactory(platformConfig);
20862
20858
  const smsApplication = createSmsApplicationFactory();
20863
- const gbpLocations = gbpConnection?.metadata?.selectedLocations || [];
20864
20859
  const reputationConfig = createReputationConfigFactory(
20865
20860
  platformConfig,
20866
20861
  businessId,
20867
- partnerLocations,
20868
- gbpLocations
20862
+ partnerLocations
20869
20863
  );
20870
20864
  return {
20871
20865
  gbpConnection,
@@ -20879,8 +20873,8 @@ const createSandboxReputationData = (platformId, businessId) => {
20879
20873
  f.seed(42);
20880
20874
  const businessConfigs = {
20881
20875
  default: {
20882
- name: "Reach Sandbox",
20883
- website: "https://sandbox.example.com",
20876
+ name: "ACME SaaS",
20877
+ website: "https://acme-saas.com",
20884
20878
  tagline: "Your sandbox environment for testing",
20885
20879
  locations: [
20886
20880
  {
@@ -20893,7 +20887,7 @@ const businessConfigs = {
20893
20887
  }
20894
20888
  ],
20895
20889
  branding: {
20896
- brandName: "Reach Sandbox",
20890
+ brandName: "ACME SaaS",
20897
20891
  primaryColor: "#2563eb",
20898
20892
  secondaryColors: ["#059669", "#dc2626", "#9333ea"],
20899
20893
  brandTagline: "Your sandbox environment for testing",
@@ -20907,7 +20901,8 @@ const businessConfigs = {
20907
20901
  accent: ["#059669", "#dc2626", "#9333ea"],
20908
20902
  background: ["#ffffff", "#f8fafc", "#f1f5f9"],
20909
20903
  text: ["#1e293b", "#475569", "#64748b"]
20910
- }
20904
+ },
20905
+ logoUrl: "https://media.licdn.com/dms/image/v2/C4D0BAQEqer-Bku3Feg/company-logo_200_200/company-logo_200_200/0/1631339163505?e=2147483647&v=beta&t=90F79drLbd8Tiz6oOGqrEDPvC7WbP4Dyp0qGTRToPIo"
20911
20906
  }
20912
20907
  },
20913
20908
  goose: {
@@ -20939,7 +20934,8 @@ const businessConfigs = {
20939
20934
  accent: ["#dc2626", "#059669", "#7c3aed"],
20940
20935
  background: ["#fef3c7", "#fef2f2", "#f0fdf4"],
20941
20936
  text: ["#92400e", "#991b1b", "#166534"]
20942
- }
20937
+ },
20938
+ logoUrl: "https://static.vecteezy.com/system/resources/previews/009/651/674/non_2x/cute-dog-logo-free-vector.jpg"
20943
20939
  }
20944
20940
  },
20945
20941
  renterra: {
@@ -20971,7 +20967,8 @@ const businessConfigs = {
20971
20967
  accent: ["#059669", "#dc2626", "#7c2d12"],
20972
20968
  background: ["#dbeafe", "#f0fdf4", "#fef2f2"],
20973
20969
  text: ["#1e3a8a", "#166534", "#991b1b"]
20974
- }
20970
+ },
20971
+ logoUrl: "https://static.vecteezy.com/system/resources/previews/043/794/103/non_2x/heavy-equipment-rental-and-service-logo-design-vector.jpg"
20975
20972
  }
20976
20973
  }
20977
20974
  };
@@ -22578,9 +22575,18 @@ const generateReputationResponsesData = ({
22578
22575
  }
22579
22576
  if (searchTerm && searchTerm.length >= 3) {
22580
22577
  const searchLower = searchTerm.toLowerCase();
22581
- filteredResponses = filteredResponses.filter(
22582
- (response) => response.content.toLowerCase().includes(searchLower) || response.userName.toLowerCase().includes(searchLower) || response.userEmail.toLowerCase().includes(searchLower)
22583
- );
22578
+ filteredResponses = filteredResponses.filter((response) => {
22579
+ if (response.userName.toLowerCase().includes(searchLower)) {
22580
+ return true;
22581
+ }
22582
+ if (response.content.toLowerCase().includes(searchLower)) {
22583
+ return true;
22584
+ }
22585
+ if (response.replies && "reply_comment" in response.replies && typeof response.replies.reply_comment === "string" && response.replies.reply_comment.toLowerCase().includes(searchLower)) {
22586
+ return true;
22587
+ }
22588
+ return false;
22589
+ });
22584
22590
  }
22585
22591
  filteredResponses.sort(
22586
22592
  (a2, b2) => new Date(b2.createdAt).getTime() - new Date(a2.createdAt).getTime()
@@ -22637,41 +22643,79 @@ function createGoogleReview(platformConfig, partnerLocations, startDate, endDate
22637
22643
  ]);
22638
22644
  const reviewContent = generateReviewContent(platformConfig, rating);
22639
22645
  const location2 = partnerLocations.length > 0 ? f.helpers.arrayElement(partnerLocations) : null;
22646
+ const hasReply = f.helpers.weightedArrayElement([
22647
+ { weight: 30, value: true },
22648
+ // 30% have business replies
22649
+ { weight: 70, value: false }
22650
+ ]);
22651
+ const replyData = hasReply ? {
22652
+ source: "google_business_profile",
22653
+ reply_comment: generateReplyContent(platformConfig, rating),
22654
+ reply_updated_at: f.date.between({
22655
+ from: new Date(
22656
+ f.date.between({ from: startDate, to: endDate }).getTime() + 24 * 60 * 60 * 1e3
22657
+ ),
22658
+ // At least 1 day after review
22659
+ to: endDate
22660
+ }).toISOString()
22661
+ } : void 0;
22662
+ const isLinked = f.helpers.weightedArrayElement([
22663
+ { weight: 30, value: true },
22664
+ // 30% are linked
22665
+ { weight: 70, value: false }
22666
+ // 70% are not linked
22667
+ ]);
22668
+ const userName = isLinked ? f.person.fullName() : "Anonymous";
22640
22669
  return {
22641
22670
  id: f.string.uuid(),
22642
22671
  source: "google",
22672
+ reviewSource: "google_business_profile",
22643
22673
  rating,
22644
- userName: f.person.fullName(),
22645
- userEmail: f.internet.email(),
22646
- // In real Google reviews, email might not be available
22674
+ userName,
22675
+ // userEmail: faker.internet.email(), // In real Google reviews, email might not be available
22647
22676
  content: reviewContent,
22648
22677
  createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
22649
- hasReply: f.helpers.weightedArrayElement([
22650
- { weight: 30, value: true },
22651
- // 30% have business replies
22652
- { weight: 70, value: false }
22653
- ]),
22678
+ hasReply,
22654
22679
  isResponded: false,
22655
22680
  // Not applicable for Google reviews
22656
- actionUrl: `https://business.google.com/reviews/l/${f.string.alphanumeric(21)}`,
22657
- locationId: location2?.externalId || void 0
22681
+ locationId: location2?.externalId || void 0,
22682
+ ...replyData ? { replies: replyData } : {},
22683
+ partnerUserId: isLinked ? f.string.uuid() : void 0,
22684
+ reviewExternalId: f.string.alphanumeric(21)
22658
22685
  };
22659
22686
  }
22660
22687
  function createInternalFeedback(platformConfig, partnerLocations, startDate, endDate) {
22661
22688
  const rating = f.helpers.weightedArrayElement([
22662
- { weight: 80, value: 5 },
22663
- // Thumbs up (positive feedback) - 80%
22664
- { weight: 20, value: 1 }
22665
- // Thumbs down (negative feedback) - 20%
22689
+ { weight: 15, value: 10 },
22690
+ // 15% - 10 rating (promoters)
22691
+ { weight: 25, value: 9 },
22692
+ // 25% - 9 rating (promoters)
22693
+ { weight: 15, value: 8 },
22694
+ // 15% - 8 rating (passives)
22695
+ { weight: 10, value: 7 },
22696
+ // 10% - 7 rating (passives)
22697
+ { weight: 8, value: 6 },
22698
+ // 8% - 6 rating (detractors)
22699
+ { weight: 7, value: 5 },
22700
+ // 7% - 5 rating (detractors)
22701
+ { weight: 6, value: 4 },
22702
+ // 6% - 4 rating (detractors)
22703
+ { weight: 5, value: 3 },
22704
+ // 5% - 3 rating (detractors)
22705
+ { weight: 4, value: 2 },
22706
+ // 4% - 2 rating (detractors)
22707
+ { weight: 5, value: 1 }
22708
+ // 5% - 1 rating (detractors)
22666
22709
  ]);
22667
22710
  const feedbackContent = generateFeedbackContent(platformConfig, rating);
22668
22711
  const location2 = partnerLocations.length > 0 ? f.helpers.arrayElement(partnerLocations) : null;
22669
22712
  return {
22670
22713
  id: f.string.uuid(),
22671
22714
  source: "internal",
22715
+ reviewSource: "internal_feedback",
22672
22716
  rating,
22673
22717
  userName: f.person.fullName(),
22674
- userEmail: f.internet.email(),
22718
+ // userEmail: faker.internet.email(),
22675
22719
  content: feedbackContent,
22676
22720
  createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
22677
22721
  hasReply: false,
@@ -22681,8 +22725,6 @@ function createInternalFeedback(platformConfig, partnerLocations, startDate, end
22681
22725
  // 40% have customer responses
22682
22726
  { weight: 60, value: false }
22683
22727
  ]),
22684
- actionUrl: void 0,
22685
- // Internal feedback doesn't have external action URLs
22686
22728
  locationId: location2?.externalId || void 0
22687
22729
  };
22688
22730
  }
@@ -22755,42 +22797,78 @@ function getReviewTemplates(platformId, rating) {
22755
22797
  if (rating <= 2) return commonNegative;
22756
22798
  return commonNeutral;
22757
22799
  }
22800
+ function generateReplyContent(platformConfig, rating) {
22801
+ const platformName = platformConfig.name || "Business";
22802
+ const positiveReplies = [
22803
+ `Thank you so much for your kind words! We're thrilled to hear you had a great experience with ${platformName}. We look forward to serving you again soon!`,
22804
+ `We really appreciate your positive feedback! It's wonderful to know that our team at ${platformName} was able to exceed your expectations. Thank you for choosing us!`,
22805
+ `Thank you for taking the time to leave such a wonderful review! We're so glad you enjoyed your experience at ${platformName}. See you again soon!`,
22806
+ `Your review made our day! We're delighted that you had such a positive experience with ${platformName}. Thank you for your support!`
22807
+ ];
22808
+ const neutralReplies = [
22809
+ `Thank you for your feedback. We appreciate you taking the time to share your experience with ${platformName}. We're always working to improve our services and would love to hear more about how we can serve you better.`,
22810
+ `We appreciate your honest feedback. At ${platformName}, we're committed to continuous improvement. Please feel free to reach out to us directly if there's anything specific we can address.`,
22811
+ `Thank you for your review. We value your input and are always looking for ways to enhance our customers' experience at ${platformName}. We hope to have the opportunity to serve you better in the future.`
22812
+ ];
22813
+ const negativeReplies = [
22814
+ `We sincerely apologize that your experience at ${platformName} did not meet your expectations. Your feedback is very important to us, and we would like the opportunity to make this right. Please contact us directly so we can address your concerns.`,
22815
+ `Thank you for bringing this to our attention. We're sorry to hear about your experience at ${platformName}. We take all feedback seriously and would appreciate the chance to discuss this with you further. Please reach out to us at your convenience.`,
22816
+ `We're truly sorry about your disappointing experience with ${platformName}. This is not the standard of service we strive for. We'd like to learn more about what happened and work towards a resolution. Please contact our team directly.`,
22817
+ `Your feedback is concerning to us, and we apologize for falling short of your expectations at ${platformName}. We'd very much like to understand what went wrong and how we can improve. Please get in touch with us so we can make this right.`
22818
+ ];
22819
+ if (rating >= 4) return f.helpers.arrayElement(positiveReplies);
22820
+ if (rating === 3) return f.helpers.arrayElement(neutralReplies);
22821
+ return f.helpers.arrayElement(negativeReplies);
22822
+ }
22758
22823
  function getFeedbackTemplates(platformId, rating) {
22759
- const commonPositive = [
22760
- "I wanted to share my positive experience with {business}. The entire process was smooth and professional. The staff was knowledgeable and took the time to explain everything clearly. I'm very satisfied with the service and would definitely return.",
22761
- "Excellent service from {business}! From the initial consultation to the final delivery, everything exceeded my expectations. The team was responsive to my questions and made sure I was completely satisfied with the outcome.",
22762
- "Outstanding experience with {business}. The quality of work was exceptional and the customer service was top-notch. I appreciated the attention to detail and the follow-up communication. Highly recommend!"
22824
+ const commonPromoters = [
22825
+ "I wanted to share my exceptional experience with {business}. The entire process was smooth and professional. The staff was knowledgeable and took the time to explain everything clearly. I'm extremely satisfied with the service and would definitely return.",
22826
+ "Outstanding service from {business}! From the initial consultation to the final delivery, everything exceeded my expectations. The team was responsive to my questions and made sure I was completely satisfied with the outcome.",
22827
+ "Excellent experience with {business}. The quality of work was exceptional and the customer service was top-notch. I appreciated the attention to detail and the follow-up communication. Highly recommend!",
22828
+ "Absolutely fantastic service! {business} went above and beyond my expectations. Professional, efficient, and genuinely cared about delivering the best possible outcome."
22763
22829
  ];
22764
- const commonNegative = [
22830
+ const commonPassives = [
22831
+ "My experience with {business} was good overall. The service was completed as requested, with only minor issues along the way. The staff was generally helpful, though response times could be improved. Met my expectations.",
22832
+ "Solid experience with {business}. Some aspects of the service were very good, while others were just okay. The pricing was reasonable, and the outcome was satisfactory. Room for improvement but generally positive.",
22833
+ "Decent service from {business}. The work was completed professionally, though not exceptional. Staff was courteous and the process was straightforward. Would consider using them again."
22834
+ ];
22835
+ const commonDetractors = [
22765
22836
  "I had several issues with my recent experience at {business}. The initial promise was not met, and when I tried to address my concerns, the customer service was lacking. The quality of service was below what I expected based on the pricing.",
22766
22837
  "Unfortunately, my experience with {business} was disappointing. There were delays in service delivery, and the communication was poor throughout the process. I hope they can improve their operations for future customers.",
22767
- "Not satisfied with the service from {business}. The staff seemed unprepared and the final result didn't match what was discussed initially. For the price paid, I expected much better quality and professionalism."
22768
- ];
22769
- const commonNeutral = [
22770
- "My experience with {business} was average. The service was completed as requested, but there were some minor issues along the way. The staff was generally helpful, though response times could be improved. Overall, it met basic expectations.",
22771
- "Mixed feelings about {business}. Some aspects of the service were good, while others fell short. The pricing was fair, but the execution could have been better. They have potential but need some improvements."
22838
+ "Not satisfied with the service from {business}. The staff seemed unprepared and the final result didn't match what was discussed initially. For the price paid, I expected much better quality and professionalism.",
22839
+ "Poor experience overall. {business} needs significant improvements in customer service and service delivery. Multiple issues that were not resolved satisfactorily."
22772
22840
  ];
22773
22841
  if (platformId === "goose") {
22774
- const petSpaDetailed = rating >= 4 ? [
22775
- "My experience at {business} was wonderful. The staff clearly has extensive training in animal care and grooming techniques. My pet was comfortable throughout the entire process, and the grooming results were exactly what I requested. The facility is clean and well-organized, and I appreciate the care they take with each animal."
22776
- ] : [
22842
+ const petSpaPromoters = [
22843
+ "My experience at {business} was absolutely wonderful. The staff clearly has extensive training in animal care and grooming techniques. My pet was comfortable throughout the entire process, and the grooming results exceeded what I requested. The facility is immaculate and well-organized, and I truly appreciate the exceptional care they take with each animal."
22844
+ ];
22845
+ const petSpaPassives = [
22846
+ "Good experience at {business}. The staff was knowledgeable and my pet was handled well. The grooming results were as requested, though the wait time was a bit longer than expected. Overall satisfied with the service."
22847
+ ];
22848
+ const petSpaDetractors = [
22777
22849
  "I had concerns about my pet's experience at {business}. While the grooming was completed, my animal seemed stressed afterward. The staff could benefit from additional training on handling anxious pets. The facility was clean, but the process could be more gentle and patient."
22778
22850
  ];
22779
- if (rating >= 4) return [...commonPositive, ...petSpaDetailed];
22780
- if (rating <= 2) return [...commonNegative, ...petSpaDetailed];
22851
+ if (rating >= 9) return [...commonPromoters, ...petSpaPromoters];
22852
+ if (rating >= 7) return [...commonPassives, ...petSpaPassives];
22853
+ return [...commonDetractors, ...petSpaDetractors];
22781
22854
  }
22782
22855
  if (platformId === "renterra") {
22783
- const rentalDetailed = rating >= 4 ? [
22784
- "Excellent equipment rental experience with {business}. The reservation process was straightforward, and the equipment was delivered on time in perfect working condition. The staff provided clear instructions for operation and maintenance. When we had a minor question during the rental period, they were quick to respond and helpful."
22785
- ] : [
22856
+ const rentalPromoters = [
22857
+ "Exceptional equipment rental experience with {business}! The reservation process was seamless, and the equipment was delivered ahead of schedule in perfect working condition. The staff provided comprehensive instructions and was incredibly responsive to our questions throughout the rental period."
22858
+ ];
22859
+ const rentalPassives = [
22860
+ "Solid rental experience with {business}. The equipment worked as expected and delivery was on time. Staff was helpful when contacted. Met our needs for the project, though nothing particularly stood out as exceptional."
22861
+ ];
22862
+ const rentalDetractors = [
22786
22863
  "Had some challenges with my rental from {business}. The equipment arrived later than scheduled, which impacted our project timeline. Additionally, the equipment showed signs of wear that weren't disclosed upfront. Customer service was slow to respond when we reported issues."
22787
22864
  ];
22788
- if (rating >= 4) return [...commonPositive, ...rentalDetailed];
22789
- if (rating <= 2) return [...commonNegative, ...rentalDetailed];
22865
+ if (rating >= 9) return [...commonPromoters, ...rentalPromoters];
22866
+ if (rating >= 7) return [...commonPassives, ...rentalPassives];
22867
+ return [...commonDetractors, ...rentalDetractors];
22790
22868
  }
22791
- if (rating >= 4) return commonPositive;
22792
- if (rating <= 2) return commonNegative;
22793
- return commonNeutral;
22869
+ if (rating >= 9) return commonPromoters;
22870
+ if (rating >= 7) return commonPassives;
22871
+ return commonDetractors;
22794
22872
  }
22795
22873
  const generateReviewAnalyticsData = ({
22796
22874
  platformData,
@@ -22816,36 +22894,55 @@ const generateReviewAnalyticsData = ({
22816
22894
  false,
22817
22895
  isAllTimeQuery || false
22818
22896
  );
22819
- const includeComparison = Boolean(queryParams.include_comparison);
22820
- if (includeComparison) {
22821
- const previousPeriodData = generateReviewPeriodData(
22822
- platformConfig,
22823
- scalingFactor,
22824
- true,
22825
- isAllTimeQuery || false
22826
- );
22827
- const comparison = calculateReviewComparison(
22828
- currentPeriodData,
22829
- previousPeriodData
22830
- );
22831
- const responseWithComparison = {
22832
- reviewSource: "google_business_profile",
22833
- current: currentPeriodData,
22834
- previous: previousPeriodData,
22835
- comparison
22836
- };
22837
- return responseWithComparison;
22838
- }
22839
- const basicResponse = {
22897
+ const previousPeriodData = generateReviewPeriodData(
22898
+ platformConfig,
22899
+ scalingFactor,
22900
+ true,
22901
+ // isPrevious = true
22902
+ isAllTimeQuery || false
22903
+ );
22904
+ const totalData = generateReviewPeriodData(
22905
+ platformConfig,
22906
+ 1,
22907
+ // No scaling for all-time
22908
+ false,
22909
+ true
22910
+ // isAllTime = true
22911
+ );
22912
+ const totalCountChange = currentPeriodData.totalCount - previousPeriodData.totalCount;
22913
+ const totalCountChangePercent = previousPeriodData.totalCount > 0 ? parseFloat(
22914
+ (totalCountChange / previousPeriodData.totalCount * 100).toFixed(1)
22915
+ ) : null;
22916
+ const averageRatingChange = parseFloat(
22917
+ (currentPeriodData.averageRating - previousPeriodData.averageRating).toFixed(1)
22918
+ );
22919
+ const averageRatingChangePercent = previousPeriodData.averageRating > 0 ? parseFloat(
22920
+ (averageRatingChange / previousPeriodData.averageRating * 100).toFixed(1)
22921
+ ) : 0;
22922
+ const fullResponse = {
22840
22923
  reviewSource: "google_business_profile",
22841
- totalCount: currentPeriodData.totalCount,
22842
- averageRating: currentPeriodData.averageRating,
22843
- ratingDistribution: currentPeriodData.ratingDistribution
22924
+ current: {
22925
+ totalCount: currentPeriodData.totalCount,
22926
+ averageRating: currentPeriodData.averageRating,
22927
+ ratingDistribution: currentPeriodData.ratingDistribution
22928
+ },
22929
+ previous: {
22930
+ totalCount: previousPeriodData.totalCount,
22931
+ averageRating: previousPeriodData.averageRating,
22932
+ ratingDistribution: previousPeriodData.ratingDistribution
22933
+ },
22934
+ total: totalData,
22935
+ comparison: {
22936
+ totalCountChange,
22937
+ totalCountChangePercent,
22938
+ averageRatingChange,
22939
+ averageRatingChangePercent
22940
+ }
22844
22941
  };
22845
- return basicResponse;
22942
+ return fullResponse;
22846
22943
  };
22847
22944
  function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
22848
- const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics$1(platformConfig);
22945
+ const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics(platformConfig);
22849
22946
  const scaledTotalCount = isAllTime ? baseMetrics.totalCount : Math.max(1, Math.round(baseMetrics.totalCount * scalingFactor));
22850
22947
  const performanceMultiplier = isPrevious ? 0.9 : 1;
22851
22948
  const adjustedTotalCount = Math.round(
@@ -22853,7 +22950,7 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
22853
22950
  );
22854
22951
  const ratingDelta = isPrevious ? -0.2 : 0;
22855
22952
  const adjustedAverageRating = baseMetrics.averageRating + ratingDelta;
22856
- const ratingDistribution = generateRatingDistribution(
22953
+ const ratingDistribution = generateRatingDistribution$1(
22857
22954
  adjustedTotalCount,
22858
22955
  adjustedAverageRating
22859
22956
  );
@@ -22863,22 +22960,24 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
22863
22960
  ratingDistribution
22864
22961
  };
22865
22962
  }
22866
- function getBasePlatformReviewMetrics$1(platformConfig) {
22963
+ function getBasePlatformReviewMetrics(platformConfig) {
22867
22964
  const industryMultipliers = {
22868
22965
  // Pet spa industry (Goose) - high customer satisfaction
22869
- "ACME Pet Spa": {
22870
- totalCount: 8,
22871
- // Much lower - reviews are rare compared to emails
22872
- averageRating: 4.6
22966
+ "Paws Pet Hotel": {
22967
+ totalCount: 12,
22968
+ // ~12 reviews per 30 days
22969
+ averageRating: 4.5
22873
22970
  },
22874
22971
  // Equipment rental (Renterra) - moderate review volume
22875
22972
  "Tomer's Rentals": {
22876
- totalCount: 5,
22973
+ totalCount: 8,
22974
+ // ~8 reviews per 30 days
22877
22975
  averageRating: 4.2
22878
22976
  },
22879
22977
  // Default platform
22880
22978
  default: {
22881
- totalCount: 6,
22979
+ totalCount: 10,
22980
+ // ~10 reviews per 30 days
22882
22981
  averageRating: 4.3
22883
22982
  }
22884
22983
  };
@@ -22888,27 +22987,28 @@ function getBasePlatformReviewMetrics$1(platformConfig) {
22888
22987
  function getAllTimePlatformReviewMetrics(platformConfig) {
22889
22988
  const industryMultipliers = {
22890
22989
  // Pet spa industry (Goose) - high customer satisfaction
22891
- "ACME Pet Spa": {
22892
- totalCount: 124,
22893
- // Much lower - reviews are rare compared to emails
22894
- averageRating: 4.3
22895
- // Slightly lower all-time average
22990
+ "Paws Pet Hotel": {
22991
+ totalCount: 40,
22992
+ // ~100 days of reviews (12 per 30 days)
22993
+ averageRating: 4.4
22896
22994
  },
22897
22995
  // Equipment rental (Renterra) - moderate review volume
22898
22996
  "Tomer's Rentals": {
22899
- totalCount: 78,
22997
+ totalCount: 27,
22998
+ // ~100 days of reviews (8 per 30 days)
22900
22999
  averageRating: 4.1
22901
23000
  },
22902
23001
  // Default platform
22903
23002
  default: {
22904
- totalCount: 95,
23003
+ totalCount: 33,
23004
+ // ~100 days of reviews (10 per 30 days)
22905
23005
  averageRating: 4.2
22906
23006
  }
22907
23007
  };
22908
23008
  const baseKey = platformConfig.name in industryMultipliers ? platformConfig.name : "default";
22909
23009
  return industryMultipliers[baseKey];
22910
23010
  }
22911
- function generateRatingDistribution(totalCount, targetAverage) {
23011
+ function generateRatingDistribution$1(totalCount, targetAverage) {
22912
23012
  const baseDistribution = {
22913
23013
  5: 0.6,
22914
23014
  // 60% 5-star
@@ -22949,28 +23049,6 @@ function adjustDistributionForAverage(baseDistribution, targetAverage) {
22949
23049
  }
22950
23050
  return adjusted;
22951
23051
  }
22952
- function calculateReviewComparison(currentPeriod, previousPeriod) {
22953
- const totalCountChange = currentPeriod.totalCount - previousPeriod.totalCount;
22954
- const totalCountChangePercent = previousPeriod.totalCount > 0 ? parseFloat(
22955
- (totalCountChange / previousPeriod.totalCount * 100).toFixed(1)
22956
- ) : null;
22957
- const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
22958
- (currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
22959
- 2
22960
- )
22961
- ) : null;
22962
- const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
22963
- (averageRatingChange / previousPeriod.averageRating * 100).toFixed(
22964
- 1
22965
- )
22966
- ) : null;
22967
- return {
22968
- totalCountChange,
22969
- totalCountChangePercent,
22970
- averageRatingChange,
22971
- averageRatingChangePercent
22972
- };
22973
- }
22974
23052
  const generateFeedbackAnalyticsData = ({
22975
23053
  platformData,
22976
23054
  queryParams
@@ -22995,38 +23073,86 @@ const generateFeedbackAnalyticsData = ({
22995
23073
  false,
22996
23074
  isAllTimeQuery
22997
23075
  );
22998
- if (queryParams.include_comparison) {
22999
- const previousPeriodData = generateFeedbackPeriodData(
23000
- platformConfig,
23001
- scalingFactor,
23002
- true,
23003
- isAllTimeQuery
23004
- );
23005
- const comparison = calculateFeedbackComparison(
23006
- currentPeriodData,
23007
- previousPeriodData
23008
- );
23009
- const responseWithComparison = {
23010
- current: currentPeriodData,
23011
- previous: previousPeriodData,
23012
- comparison
23013
- };
23014
- return responseWithComparison;
23015
- }
23016
- const basicResponse = {
23017
- totalSent: currentPeriodData.totalSent,
23018
- totalResponded: currentPeriodData.totalResponded,
23019
- averageRating: currentPeriodData.averageRating,
23020
- responseRate: currentPeriodData.responseRate,
23021
- positiveResponses: currentPeriodData.positiveResponses,
23022
- negativeResponses: currentPeriodData.negativeResponses,
23023
- passiveResponses: currentPeriodData.passiveResponses,
23024
- npsScore: currentPeriodData.npsScore
23076
+ const previousPeriodData = generateFeedbackPeriodData(
23077
+ platformConfig,
23078
+ scalingFactor,
23079
+ true,
23080
+ // isPrevious = true
23081
+ isAllTimeQuery
23082
+ );
23083
+ const totalData = generateFeedbackPeriodData(
23084
+ platformConfig,
23085
+ 1,
23086
+ // No scaling for all-time
23087
+ false,
23088
+ true
23089
+ // isAllTime = true
23090
+ );
23091
+ const totalSentChange = currentPeriodData.totalSent - previousPeriodData.totalSent;
23092
+ const totalSentChangePercent = previousPeriodData.totalSent > 0 ? parseFloat(
23093
+ (totalSentChange / previousPeriodData.totalSent * 100).toFixed(1)
23094
+ ) : null;
23095
+ const npsScoreChange = currentPeriodData.npsScore - previousPeriodData.npsScore;
23096
+ const npsScoreChangePercent = previousPeriodData.npsScore !== 0 ? parseFloat(
23097
+ (npsScoreChange / Math.abs(previousPeriodData.npsScore) * 100).toFixed(1)
23098
+ ) : null;
23099
+ const responseRateChange = parseFloat(
23100
+ (currentPeriodData.responseRate - previousPeriodData.responseRate).toFixed(
23101
+ 1
23102
+ )
23103
+ );
23104
+ const responseRateChangePercent = previousPeriodData.responseRate > 0 ? parseFloat(
23105
+ (responseRateChange / previousPeriodData.responseRate * 100).toFixed(1)
23106
+ ) : 0;
23107
+ const totalRespondedChange = currentPeriodData.totalResponded - previousPeriodData.totalResponded;
23108
+ const totalRespondedChangePercent = previousPeriodData.totalResponded > 0 ? parseFloat(
23109
+ (totalRespondedChange / previousPeriodData.totalResponded * 100).toFixed(1)
23110
+ ) : null;
23111
+ const averageRatingChange = parseFloat(
23112
+ (currentPeriodData.averageRating - previousPeriodData.averageRating).toFixed(1)
23113
+ );
23114
+ const averageRatingChangePercent = previousPeriodData.averageRating > 0 ? parseFloat(
23115
+ (averageRatingChange / previousPeriodData.averageRating * 100).toFixed(1)
23116
+ ) : 0;
23117
+ const fullResponse = {
23118
+ current: {
23119
+ totalSent: currentPeriodData.totalSent,
23120
+ totalResponded: currentPeriodData.totalResponded,
23121
+ averageRating: currentPeriodData.averageRating,
23122
+ responseRate: currentPeriodData.responseRate,
23123
+ positiveResponses: currentPeriodData.positiveResponses,
23124
+ negativeResponses: currentPeriodData.negativeResponses,
23125
+ passiveResponses: currentPeriodData.passiveResponses,
23126
+ npsScore: currentPeriodData.npsScore
23127
+ },
23128
+ previous: {
23129
+ totalSent: previousPeriodData.totalSent,
23130
+ totalResponded: previousPeriodData.totalResponded,
23131
+ averageRating: previousPeriodData.averageRating,
23132
+ responseRate: previousPeriodData.responseRate,
23133
+ positiveResponses: previousPeriodData.positiveResponses,
23134
+ negativeResponses: previousPeriodData.negativeResponses,
23135
+ passiveResponses: previousPeriodData.passiveResponses,
23136
+ npsScore: previousPeriodData.npsScore
23137
+ },
23138
+ total: totalData,
23139
+ comparison: {
23140
+ totalSentChange,
23141
+ totalSentChangePercent,
23142
+ npsScoreChange,
23143
+ npsScoreChangePercent,
23144
+ responseRateChange,
23145
+ responseRateChangePercent,
23146
+ totalRespondedChange,
23147
+ totalRespondedChangePercent,
23148
+ averageRatingChange,
23149
+ averageRatingChangePercent
23150
+ }
23025
23151
  };
23026
- return basicResponse;
23152
+ return fullResponse;
23027
23153
  };
23028
23154
  function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
23029
- const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics$1(platformConfig);
23155
+ const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics(platformConfig);
23030
23156
  const scaledTotalSent = isAllTime ? baseMetrics.totalSent : Math.max(1, Math.round(baseMetrics.totalSent * scalingFactor));
23031
23157
  const performanceMultiplier = isPrevious ? 0.82 : 1;
23032
23158
  const adjustedTotalSent = Math.round(scaledTotalSent * performanceMultiplier);
@@ -23064,10 +23190,10 @@ function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious =
23064
23190
  // real NPS score
23065
23191
  };
23066
23192
  }
23067
- function getBasePlatformFeedbackMetrics$1(platformConfig) {
23193
+ function getBasePlatformFeedbackMetrics(platformConfig) {
23068
23194
  const industryMultipliers = {
23069
23195
  // Pet spa industry (Goose) - high engagement, positive feedback
23070
- "ACME Pet Spa": {
23196
+ "Paws Pet Hotel": {
23071
23197
  totalSent: 1250,
23072
23198
  // Much higher - businesses send way more emails than get reviews
23073
23199
  responseRate: 42,
@@ -23110,7 +23236,7 @@ function getBasePlatformFeedbackMetrics$1(platformConfig) {
23110
23236
  function getAllTimePlatformFeedbackMetrics(platformConfig) {
23111
23237
  const industryMultipliers = {
23112
23238
  // Pet spa industry (Goose) - high engagement, positive feedback
23113
- "ACME Pet Spa": {
23239
+ "Paws Pet Hotel": {
23114
23240
  totalSent: 18500,
23115
23241
  // WAY higher - businesses send tons of emails over time
23116
23242
  responseRate: 39,
@@ -23159,48 +23285,6 @@ function calculateAverageFromNPS(promoters, passives, detractors) {
23159
23285
  const weightedSum = promoters * promoterAverage + passives * passiveAverage + detractors * detractorAverage;
23160
23286
  return weightedSum / totalResponses;
23161
23287
  }
23162
- function calculateFeedbackComparison(currentPeriod, previousPeriod) {
23163
- const totalSentChange = currentPeriod.totalSent - previousPeriod.totalSent;
23164
- const totalSentChangePercent = previousPeriod.totalSent > 0 ? parseFloat(
23165
- (totalSentChange / previousPeriod.totalSent * 100).toFixed(1)
23166
- ) : null;
23167
- const totalRespondedChange = currentPeriod.totalResponded - previousPeriod.totalResponded;
23168
- const totalRespondedChangePercent = previousPeriod.totalResponded > 0 ? parseFloat(
23169
- (totalRespondedChange / previousPeriod.totalResponded * 100).toFixed(1)
23170
- ) : null;
23171
- const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
23172
- (currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
23173
- 2
23174
- )
23175
- ) : null;
23176
- const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
23177
- (averageRatingChange / previousPeriod.averageRating * 100).toFixed(
23178
- 1
23179
- )
23180
- ) : null;
23181
- const responseRateChange = currentPeriod.responseRate - previousPeriod.responseRate;
23182
- const responseRateChangePercent = previousPeriod.responseRate > 0 ? parseFloat(
23183
- (responseRateChange / previousPeriod.responseRate * 100).toFixed(1)
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;
23191
- return {
23192
- totalSentChange,
23193
- totalSentChangePercent,
23194
- totalRespondedChange,
23195
- totalRespondedChangePercent,
23196
- averageRatingChange,
23197
- averageRatingChangePercent,
23198
- responseRateChange: parseFloat(responseRateChange.toFixed(1)),
23199
- responseRateChangePercent,
23200
- npsScoreChange,
23201
- npsScoreChangePercent
23202
- };
23203
- }
23204
23288
  const generateAuditLogData = ({
23205
23289
  startDate,
23206
23290
  endDate,
@@ -23266,24 +23350,27 @@ function generateAuditLogPool(startDate, endDate, count) {
23266
23350
  const logs = [];
23267
23351
  const timeSpan = endDate.getTime() - startDate.getTime();
23268
23352
  const eventSequences = [
23269
- // Survey flow
23353
+ // Private Feedback flow
23270
23354
  {
23271
23355
  type: "SURVEY_REQUEST",
23272
23356
  channel: "EMAIL",
23273
23357
  statuses: ["SENT", "DELIVERED", "OPENED", "COMPLETED"],
23274
23358
  messages: [
23275
- "Survey request sent to customer",
23276
- "Survey request delivered to customer",
23277
- "Customer opened survey request",
23278
- "Survey completed by customer"
23359
+ "Private feedback request sent to customer",
23360
+ "Private feedback request delivered to customer",
23361
+ "Customer opened private feedback request",
23362
+ "Private feedback completed by customer"
23279
23363
  ]
23280
23364
  },
23281
- // Review flow
23365
+ // Google Review flow
23282
23366
  {
23283
23367
  type: "REVIEW_REQUEST",
23284
23368
  channel: "SMS",
23285
23369
  statuses: ["SENT", "DELIVERED"],
23286
- messages: ["Review request sent to", "Review request delivered to"]
23370
+ messages: [
23371
+ "Google review request sent to",
23372
+ "Google review request delivered to"
23373
+ ]
23287
23374
  },
23288
23375
  // Google review flow
23289
23376
  {
@@ -23300,13 +23387,19 @@ function generateAuditLogPool(startDate, endDate, count) {
23300
23387
  type: "SURVEY_REMINDER",
23301
23388
  channel: "EMAIL",
23302
23389
  statuses: ["SENT", "DELIVERED"],
23303
- messages: ["Survey reminder sent to", "Survey reminder delivered to"]
23390
+ messages: [
23391
+ "Private feedback reminder sent to",
23392
+ "Private feedback reminder delivered to"
23393
+ ]
23304
23394
  },
23305
23395
  {
23306
23396
  type: "REVIEW_REMINDER",
23307
23397
  channel: "SMS",
23308
23398
  statuses: ["SENT", "DELIVERED"],
23309
- messages: ["Review reminder sent to", "Review reminder delivered to"]
23399
+ messages: [
23400
+ "Google review reminder sent to",
23401
+ "Google review reminder delivered to"
23402
+ ]
23310
23403
  }
23311
23404
  ];
23312
23405
  for (let i2 = 0; i2 < count; i2++) {
@@ -23348,125 +23441,112 @@ const generateReviewTimeSeriesData = ({
23348
23441
  platformData,
23349
23442
  queryParams
23350
23443
  }) => {
23351
- const platformConfig = getPlatformConfig(
23352
- platformData.businessesMe?.platform_id
23353
- );
23444
+ const analyticsData = generateReviewAnalyticsData({
23445
+ platformData,
23446
+ queryParams
23447
+ });
23354
23448
  const { requestedDays, startDate, endDate } = parseDateRange(
23355
23449
  queryParams.start_date,
23356
23450
  queryParams.end_date,
23357
23451
  30
23358
23452
  );
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
- );
23453
+ const isAllTimeQuery = !queryParams.start_date || !queryParams.end_date || queryParams.start_date.startsWith("2020-");
23366
23454
  let intervalDays = 7;
23367
23455
  if (requestedDays <= 30)
23368
23456
  intervalDays = 1;
23369
23457
  else if (requestedDays <= 90) intervalDays = 3;
23370
23458
  const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23371
23459
  const dataPoints = generateReviewTimeSeriesPoints({
23372
- totalScaledReviews,
23460
+ analyticsData,
23373
23461
  startDate,
23374
23462
  endDate,
23375
23463
  numDataPoints,
23376
- intervalDays
23464
+ intervalDays,
23465
+ isAllTimeQuery
23377
23466
  });
23378
23467
  return {
23379
23468
  data: dataPoints
23380
23469
  };
23381
23470
  };
23382
23471
  function generateReviewTimeSeriesPoints({
23383
- totalScaledReviews,
23472
+ analyticsData,
23384
23473
  startDate,
23385
23474
  endDate,
23386
23475
  numDataPoints,
23387
- intervalDays
23476
+ intervalDays,
23477
+ isAllTimeQuery
23388
23478
  }) {
23479
+ const reviewCounts = distributeReviewsAcrossPoints(
23480
+ analyticsData.current.totalCount,
23481
+ numDataPoints
23482
+ );
23483
+ const targetRating = analyticsData.total.averageRating ?? 4.2;
23484
+ const startingRating = targetRating - 0.3;
23485
+ const endingRating = targetRating + 0.15;
23389
23486
  const dataPoints = [];
23390
23487
  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;
23488
+ let runningReviewCount = 0;
23489
+ let runningWeightedSum = 0;
23490
+ const targetEndCumulative = targetRating;
23491
+ const ratingChange = analyticsData.comparison.averageRatingChange ?? 0;
23492
+ const targetStartCumulative = targetRating - ratingChange;
23396
23493
  for (let i2 = 0; i2 < numDataPoints; i2++) {
23397
23494
  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;
23495
+ const currentPointReviews = reviewCounts[i2];
23496
+ const progress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
23497
+ let currentPointRating = startingRating + (endingRating - startingRating) * Math.pow(progress, 1.3);
23498
+ currentPointRating += (Math.random() - 0.5) * 0.08;
23499
+ currentPointRating = Math.max(1, Math.min(5, currentPointRating));
23500
+ let cumulativeAverageRating;
23501
+ if (isAllTimeQuery) {
23502
+ runningReviewCount += currentPointReviews;
23503
+ runningWeightedSum += currentPointReviews * currentPointRating;
23504
+ cumulativeAverageRating = runningReviewCount > 0 ? parseFloat((runningWeightedSum / runningReviewCount).toFixed(2)) : null;
23407
23505
  } else {
23408
- currentPointReviews = Math.max(0, totalScaledReviews - cumulativeReviews);
23409
- currentPointRating = targetRating;
23506
+ const cumulativeProgress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
23507
+ cumulativeAverageRating = parseFloat(
23508
+ (targetStartCumulative + (targetEndCumulative - targetStartCumulative) * cumulativeProgress).toFixed(2)
23509
+ );
23410
23510
  }
23411
- currentPointReviews = Math.max(0, currentPointReviews);
23412
- currentPointRating = Math.max(1, Math.min(5, currentPointRating));
23413
- cumulativeReviews += currentPointReviews;
23414
23511
  const { promoterCount, detractorCount } = calculatePromoterDetractorSplit(
23415
23512
  currentPointReviews,
23416
23513
  currentPointRating
23417
23514
  );
23515
+ const ratingDistribution = generateRatingDistribution(
23516
+ currentPointReviews,
23517
+ currentPointRating
23518
+ );
23418
23519
  dataPoints.push({
23419
23520
  date: pointDateStr,
23420
23521
  totalReviews: currentPointReviews,
23421
23522
  averageRating: currentPointReviews > 0 ? parseFloat(currentPointRating.toFixed(1)) : null,
23523
+ cumulativeAverageRating,
23422
23524
  promoterCount,
23423
- detractorCount
23525
+ detractorCount,
23526
+ ratingDistribution
23424
23527
  });
23425
23528
  currentDate.setDate(currentDate.getDate() + intervalDays);
23426
23529
  if (currentDate > endDate && i2 < numDataPoints - 1) {
23427
23530
  currentDate = new Date(endDate);
23428
- if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23429
- break;
23430
- }
23431
23531
  }
23432
23532
  }
23433
23533
  return dataPoints;
23434
23534
  }
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];
23535
+ function distributeReviewsAcrossPoints(totalReviews, numDataPoints) {
23536
+ if (numDataPoints === 0) return [];
23537
+ if (numDataPoints === 1) return [totalReviews];
23538
+ const counts = [];
23539
+ let remainingReviews = totalReviews;
23540
+ const basePerPoint = totalReviews / numDataPoints;
23541
+ for (let i2 = 0; i2 < numDataPoints - 1; i2++) {
23542
+ const variation = 0.7 + Math.random() * 0.6;
23543
+ const count = Math.round(basePerPoint * variation);
23544
+ const actualCount = Math.max(0, Math.min(count, remainingReviews));
23545
+ counts.push(actualCount);
23546
+ remainingReviews -= actualCount;
23547
+ }
23548
+ counts.push(Math.max(0, remainingReviews));
23549
+ return counts;
23470
23550
  }
23471
23551
  function calculatePromoterDetractorSplit(totalReviews, averageRating) {
23472
23552
  if (totalReviews === 0) {
@@ -23493,33 +23573,80 @@ function calculatePromoterDetractorSplit(totalReviews, averageRating) {
23493
23573
  const detractorCount = Math.round(totalReviews * detractorRate);
23494
23574
  return { promoterCount, detractorCount };
23495
23575
  }
23576
+ function generateRatingDistribution(totalReviews, averageRating) {
23577
+ if (totalReviews === 0) {
23578
+ return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
23579
+ }
23580
+ let distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
23581
+ if (averageRating >= 4.5) {
23582
+ distribution[5] = Math.round(totalReviews * 0.6);
23583
+ distribution[4] = Math.round(totalReviews * 0.3);
23584
+ distribution[3] = Math.round(totalReviews * 0.08);
23585
+ distribution[2] = Math.round(totalReviews * 0.015);
23586
+ distribution[1] = Math.round(totalReviews * 5e-3);
23587
+ } else if (averageRating >= 4) {
23588
+ distribution[5] = Math.round(totalReviews * 0.45);
23589
+ distribution[4] = Math.round(totalReviews * 0.35);
23590
+ distribution[3] = Math.round(totalReviews * 0.15);
23591
+ distribution[2] = Math.round(totalReviews * 0.04);
23592
+ distribution[1] = Math.round(totalReviews * 0.01);
23593
+ } else if (averageRating >= 3.5) {
23594
+ distribution[5] = Math.round(totalReviews * 0.3);
23595
+ distribution[4] = Math.round(totalReviews * 0.35);
23596
+ distribution[3] = Math.round(totalReviews * 0.25);
23597
+ distribution[2] = Math.round(totalReviews * 0.07);
23598
+ distribution[1] = Math.round(totalReviews * 0.03);
23599
+ } else {
23600
+ distribution[5] = Math.round(totalReviews * 0.15);
23601
+ distribution[4] = Math.round(totalReviews * 0.25);
23602
+ distribution[3] = Math.round(totalReviews * 0.35);
23603
+ distribution[2] = Math.round(totalReviews * 0.15);
23604
+ distribution[1] = Math.round(totalReviews * 0.1);
23605
+ }
23606
+ Object.keys(distribution).forEach((rating) => {
23607
+ const key = parseInt(rating);
23608
+ const variation = Math.round(
23609
+ distribution[key] * (Math.random() - 0.5) * 0.2
23610
+ );
23611
+ distribution[key] = Math.max(0, distribution[key] + variation);
23612
+ });
23613
+ const currentTotal = Object.values(distribution).reduce(
23614
+ (sum, count) => sum + count,
23615
+ 0
23616
+ );
23617
+ const difference = totalReviews - currentTotal;
23618
+ if (difference !== 0) {
23619
+ const mostCommonRating = Object.entries(distribution).reduce(
23620
+ (max, [rating, count]) => count > distribution[max] ? rating : max,
23621
+ "5"
23622
+ );
23623
+ distribution[mostCommonRating] = Math.max(
23624
+ 0,
23625
+ distribution[mostCommonRating] + difference
23626
+ );
23627
+ }
23628
+ return distribution;
23629
+ }
23496
23630
  const generateFeedbackTimeSeriesData = ({
23497
23631
  platformData,
23498
23632
  queryParams
23499
23633
  }) => {
23500
- const platformConfig = getPlatformConfig(
23501
- platformData.businessesMe?.platform_id
23502
- );
23634
+ const analyticsData = generateFeedbackAnalyticsData({
23635
+ platformData,
23636
+ queryParams
23637
+ });
23503
23638
  const { requestedDays, startDate, endDate } = parseDateRange(
23504
23639
  queryParams.start_date,
23505
23640
  queryParams.end_date,
23506
23641
  30
23507
23642
  );
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
23643
  let intervalDays = 7;
23516
23644
  if (requestedDays <= 30)
23517
23645
  intervalDays = 1;
23518
23646
  else if (requestedDays <= 90) intervalDays = 3;
23519
23647
  const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23520
23648
  const dataPoints = generateFeedbackTimeSeriesPoints({
23521
- totalScaledSent,
23522
- baseMetrics,
23649
+ analyticsData,
23523
23650
  startDate,
23524
23651
  endDate,
23525
23652
  numDataPoints,
@@ -23529,52 +23656,54 @@ const generateFeedbackTimeSeriesData = ({
23529
23656
  data: dataPoints
23530
23657
  };
23531
23658
  };
23659
+ function distributeFeedbackAcrossPoints(total, numDataPoints) {
23660
+ if (numDataPoints === 0) return [];
23661
+ if (numDataPoints === 1) return [total];
23662
+ const counts = [];
23663
+ let remaining = total;
23664
+ const basePerPoint = total / numDataPoints;
23665
+ for (let i2 = 0; i2 < numDataPoints - 1; i2++) {
23666
+ const variation = 0.7 + Math.random() * 0.6;
23667
+ const count = Math.round(basePerPoint * variation);
23668
+ const actualCount = Math.max(0, Math.min(count, remaining));
23669
+ counts.push(actualCount);
23670
+ remaining -= actualCount;
23671
+ }
23672
+ counts.push(Math.max(0, remaining));
23673
+ return counts;
23674
+ }
23532
23675
  function generateFeedbackTimeSeriesPoints({
23533
- totalScaledSent,
23534
- baseMetrics,
23676
+ analyticsData,
23535
23677
  startDate,
23536
23678
  endDate,
23537
23679
  numDataPoints,
23538
23680
  intervalDays
23539
23681
  }) {
23682
+ const sentCounts = distributeFeedbackAcrossPoints(
23683
+ analyticsData.current.totalSent,
23684
+ numDataPoints
23685
+ );
23686
+ const respondedCounts = distributeFeedbackAcrossPoints(
23687
+ analyticsData.current.totalResponded,
23688
+ numDataPoints
23689
+ );
23690
+ const targetEndNPS = analyticsData.total.npsScore;
23691
+ const npsChange = analyticsData.comparison.npsScoreChange ?? 0;
23692
+ const targetStartNPS = targetEndNPS - npsChange;
23540
23693
  const dataPoints = [];
23541
23694
  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
23695
  for (let i2 = 0; i2 < numDataPoints; i2++) {
23549
23696
  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
- );
23697
+ const currentPointSent = sentCounts[i2];
23698
+ const currentPointResponded = respondedCounts[i2];
23699
+ const progress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
23700
+ let currentPointNPS = targetStartNPS + (targetEndNPS - targetStartNPS) * Math.pow(progress, 1.2);
23701
+ currentPointNPS += (Math.random() - 0.5) * 8;
23572
23702
  currentPointNPS = Math.max(-100, Math.min(100, currentPointNPS));
23573
- cumulativeSent += currentPointSent;
23574
- const currentPointResponded = Math.round(
23575
- currentPointSent * (currentPointResponseRate / 100)
23576
- );
23577
23703
  const { promoterCount, detractorCount, passiveCount } = calculateNPSBreakdown(currentPointResponded, currentPointNPS);
23704
+ const cumulativeNPS = parseFloat(
23705
+ (targetStartNPS + (targetEndNPS - targetStartNPS) * progress).toFixed(1)
23706
+ );
23578
23707
  dataPoints.push({
23579
23708
  date: pointDateStr,
23580
23709
  totalSent: currentPointSent,
@@ -23582,56 +23711,16 @@ function generateFeedbackTimeSeriesPoints({
23582
23711
  promoterCount,
23583
23712
  detractorCount,
23584
23713
  passiveCount,
23585
- npsScore: parseFloat(currentPointNPS.toFixed(1))
23586
- // Use the target NPS (30 42)
23714
+ npsScore: cumulativeNPS
23715
+ // Use cumulative NPS for the chart line
23587
23716
  });
23588
23717
  currentDate.setDate(currentDate.getDate() + intervalDays);
23589
23718
  if (currentDate > endDate && i2 < numDataPoints - 1) {
23590
23719
  currentDate = new Date(endDate);
23591
- if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23592
- break;
23593
- }
23594
23720
  }
23595
23721
  }
23596
23722
  return dataPoints;
23597
23723
  }
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
23724
  function calculateNPSBreakdown(totalResponses, targetNPS) {
23636
23725
  if (totalResponses === 0) {
23637
23726
  return { promoterCount: 0, detractorCount: 0, passiveCount: 0 };
@@ -23662,7 +23751,8 @@ function calculateNPSBreakdown(totalResponses, targetNPS) {
23662
23751
  };
23663
23752
  }
23664
23753
  const mockStore = {
23665
- reputationConfig: null
23754
+ reputationConfig: null,
23755
+ partnerLocations: null
23666
23756
  };
23667
23757
  const reputationHandlers = [
23668
23758
  // External OAuth - GBP Connection
@@ -23677,14 +23767,51 @@ const reputationHandlers = [
23677
23767
  // Partner Locations
23678
23768
  http.get(`${HOSTNAME}/api/partner-locations`, () => {
23679
23769
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23770
+ const locations = mockStore.partnerLocations || platformData.reputationData?.partnerLocations || [];
23680
23771
  return HttpResponse.json({
23681
23772
  success: true,
23682
23773
  message: "Success (Sandbox)",
23683
23774
  data: {
23684
- locations: platformData.reputationData?.partnerLocations || []
23775
+ locations
23685
23776
  }
23686
23777
  });
23687
23778
  }),
23779
+ // Update Partner Location (for Google Business Profile mapping)
23780
+ http.put(
23781
+ `${HOSTNAME}/api/partner-locations/:id`,
23782
+ async ({ request, params }) => {
23783
+ const locationId = params.id;
23784
+ const body = await request.json();
23785
+ const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23786
+ if (!mockStore.partnerLocations) {
23787
+ mockStore.partnerLocations = [
23788
+ ...platformData.reputationData?.partnerLocations || []
23789
+ ];
23790
+ }
23791
+ const locationIndex = mockStore.partnerLocations.findIndex(
23792
+ (loc) => loc.externalId === locationId
23793
+ );
23794
+ if (locationIndex === -1) {
23795
+ return HttpResponse.json(
23796
+ {
23797
+ success: false,
23798
+ message: `Partner location with ID ${locationId} not found (Sandbox)`
23799
+ },
23800
+ { status: 404 }
23801
+ );
23802
+ }
23803
+ mockStore.partnerLocations[locationIndex] = {
23804
+ ...mockStore.partnerLocations[locationIndex],
23805
+ googleBusinessProfilePlaceId: body.googleBusinessProfilePlaceId || null,
23806
+ updatedAt: /* @__PURE__ */ new Date()
23807
+ };
23808
+ return HttpResponse.json({
23809
+ success: true,
23810
+ message: "Partner location updated successfully (Sandbox)",
23811
+ data: mockStore.partnerLocations[locationIndex]
23812
+ });
23813
+ }
23814
+ ),
23688
23815
  // Channel Senders
23689
23816
  http.get(`${HOSTNAME}/api/channel/senders`, () => {
23690
23817
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
@@ -23710,10 +23837,19 @@ const reputationHandlers = [
23710
23837
  // Reputation Config
23711
23838
  http.get(`${HOSTNAME}/api/reputation/config`, () => {
23712
23839
  if (mockStore.reputationConfig) {
23840
+ const currentLocations = mockStore.partnerLocations || getSandboxDataForPlatform(currentSandboxPlatformId).reputationData?.partnerLocations || [];
23841
+ const locationMappings = {};
23842
+ currentLocations.forEach((location2) => {
23843
+ locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
23844
+ });
23713
23845
  const response2 = {
23714
23846
  success: true,
23715
23847
  message: "Success (Sandbox)",
23716
- data: mockStore.reputationConfig
23848
+ data: {
23849
+ ...mockStore.reputationConfig,
23850
+ locationMappings
23851
+ // Always return current mappings from partner locations
23852
+ }
23717
23853
  };
23718
23854
  return HttpResponse.json(response2);
23719
23855
  }
@@ -23733,7 +23869,7 @@ const reputationHandlers = [
23733
23869
  if (currentConfig) {
23734
23870
  const updatedConfig = {
23735
23871
  ...currentConfig,
23736
- locationMappings: body.locationMappings || currentConfig.locationMappings,
23872
+ // Note: locationMappings are now stored in partner_locations table, not here
23737
23873
  emailChannelSenderId: body.emailChannelSenderId,
23738
23874
  smsChannelSenderId: body.smsChannelSenderId,
23739
23875
  // Preserve existing completedOnboardingAt if not provided in update
@@ -23742,10 +23878,19 @@ const reputationHandlers = [
23742
23878
  };
23743
23879
  mockStore.reputationConfig = updatedConfig;
23744
23880
  }
23881
+ const currentLocations = mockStore.partnerLocations || getSandboxDataForPlatform(currentSandboxPlatformId).reputationData?.partnerLocations || [];
23882
+ const locationMappings = {};
23883
+ currentLocations.forEach((location2) => {
23884
+ locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
23885
+ });
23745
23886
  const response = {
23746
23887
  success: true,
23747
23888
  message: "Configuration updated successfully (Sandbox)",
23748
- data: mockStore.reputationConfig
23889
+ data: {
23890
+ ...mockStore.reputationConfig,
23891
+ locationMappings
23892
+ // Include current mappings from partner locations
23893
+ }
23749
23894
  };
23750
23895
  return HttpResponse.json(response);
23751
23896
  }),
@@ -23895,13 +24040,11 @@ const reputationHandlers = [
23895
24040
  const startDate = url.searchParams.get("start_date");
23896
24041
  const endDate = url.searchParams.get("end_date");
23897
24042
  const locationIds = url.searchParams.get("location_ids");
23898
- const includeComparison = url.searchParams.get("include_comparison") === "true";
23899
24043
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23900
24044
  const queryParams = {
23901
24045
  start_date: startDate || void 0,
23902
24046
  end_date: endDate || void 0,
23903
- location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0,
23904
- include_comparison: includeComparison
24047
+ location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0
23905
24048
  };
23906
24049
  const analyticsData = generateReviewAnalyticsData({
23907
24050
  platformData,
@@ -23949,11 +24092,8 @@ const reputationHandlers = [
23949
24092
  const url = new URL(request.url);
23950
24093
  const startDate = url.searchParams.get("start_date");
23951
24094
  const endDate = url.searchParams.get("end_date");
23952
- const includeComparison = url.searchParams.get("include_comparison") === "true";
23953
24095
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23954
- const queryParams = {
23955
- include_comparison: includeComparison
23956
- };
24096
+ const queryParams = {};
23957
24097
  if (startDate) {
23958
24098
  queryParams.start_date = startDate;
23959
24099
  }
@@ -24023,6 +24163,198 @@ const reputationHandlers = [
24023
24163
  message: "Success (Sandbox)",
24024
24164
  data: auditLogResponse
24025
24165
  });
24166
+ }),
24167
+ // Reviews Data Availability
24168
+ http.get(`${HOSTNAME}/api/reviews/data-availability`, () => {
24169
+ const now = /* @__PURE__ */ new Date();
24170
+ const earliestDate = new Date(now);
24171
+ earliestDate.setDate(earliestDate.getDate() - 100);
24172
+ return HttpResponse.json({
24173
+ success: true,
24174
+ message: "Success (Sandbox)",
24175
+ data: {
24176
+ dataAvailableFrom: earliestDate.toISOString()
24177
+ }
24178
+ });
24179
+ }),
24180
+ // Feedback Data Availability
24181
+ http.get(`${HOSTNAME}/api/feedback/data-availability`, () => {
24182
+ const now = /* @__PURE__ */ new Date();
24183
+ const earliestDate = new Date(now);
24184
+ earliestDate.setDate(earliestDate.getDate() - 100);
24185
+ return HttpResponse.json({
24186
+ success: true,
24187
+ message: "Success (Sandbox)",
24188
+ data: {
24189
+ dataAvailableFrom: earliestDate.toISOString()
24190
+ }
24191
+ });
24192
+ }),
24193
+ // Reputation Data Availability
24194
+ http.get(`${HOSTNAME}/api/reputation/data-availability`, () => {
24195
+ const now = /* @__PURE__ */ new Date();
24196
+ const earliestDate = new Date(now);
24197
+ earliestDate.setDate(earliestDate.getDate() - 100);
24198
+ return HttpResponse.json({
24199
+ success: true,
24200
+ message: "Success (Sandbox)",
24201
+ data: {
24202
+ dataAvailableFrom: earliestDate.toISOString()
24203
+ }
24204
+ });
24205
+ }),
24206
+ // Reputation Automation Template
24207
+ http.get(`${HOSTNAME}/api/reputation/reputation-template`, () => {
24208
+ const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
24209
+ const businessName = platformData.businessesMe?.name || "Our Business";
24210
+ const primaryColor = platformData.businessesMe?.branding?.primaryColor || "#2563eb";
24211
+ const feedbackHtml = `<!DOCTYPE html>
24212
+ <html>
24213
+ <head>
24214
+ <meta charset="UTF-8">
24215
+ <title>Feedback Request</title>
24216
+ <style>
24217
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
24218
+ .header { background: ${primaryColor}; color: white; padding: 30px 20px; text-align: center; border-radius: 8px 8px 0 0; }
24219
+ .content { background: white; padding: 30px 20px; border-radius: 0 0 8px 8px; }
24220
+ .button { background: ${primaryColor}; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; font-weight: 600; }
24221
+ .rating-scale { display: flex; gap: 8px; justify-content: center; margin: 20px 0; }
24222
+ .rating-box { border: 2px solid #ddd; padding: 12px 16px; border-radius: 4px; font-weight: bold; }
24223
+ </style>
24224
+ </head>
24225
+ <body>
24226
+ <div class="header">
24227
+ <h1>💬 How was your experience?</h1>
24228
+ </div>
24229
+ <div class="content">
24230
+ <p>Hi there! 👋</p>
24231
+ <p>Thank you for choosing <strong>${businessName}</strong>! We hope you had a great experience with us.</p>
24232
+ <p>We'd love to hear your honest feedback. On a scale of 1-10, how would you rate your experience?</p>
24233
+ <div class="rating-scale">
24234
+ <div class="rating-box">1</div>
24235
+ <div class="rating-box">...</div>
24236
+ <div class="rating-box">10</div>
24237
+ </div>
24238
+ <a href="{{feedback_url}}" class="button">Share Your Feedback</a>
24239
+ <p>Your response helps us improve and serve you better.</p>
24240
+ <p>Thank you for your time!</p>
24241
+ <p>Best regards,<br>The ${businessName} Team</p>
24242
+ </div>
24243
+ </body>
24244
+ </html>`;
24245
+ const googleReviewHtml = `<!DOCTYPE html>
24246
+ <html>
24247
+ <head>
24248
+ <meta charset="UTF-8">
24249
+ <title>Review Request</title>
24250
+ <style>
24251
+ body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
24252
+ .header { background: ${primaryColor}; color: white; padding: 30px 20px; text-align: center; border-radius: 8px 8px 0 0; }
24253
+ .content { background: white; padding: 30px 20px; border-radius: 0 0 8px 8px; }
24254
+ .button { background: ${primaryColor}; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; font-weight: 600; }
24255
+ .stars { color: #fbbf24; font-size: 24px; }
24256
+ </style>
24257
+ </head>
24258
+ <body>
24259
+ <div class="header">
24260
+ <h1>⭐ We'd love your Google review!</h1>
24261
+ </div>
24262
+ <div class="content">
24263
+ <p>Hi there! 👋</p>
24264
+ <p>Thank you so much for your positive feedback about <strong>${businessName}</strong>!</p>
24265
+ <p>Since you had a great experience, we'd be incredibly grateful if you could share your thoughts on Google. It only takes a moment and helps other customers discover us.</p>
24266
+ <div class="stars">⭐ ⭐ ⭐ ⭐ ⭐</div>
24267
+ <a href="{{external_review_url}}" class="button">Leave a Google Review</a>
24268
+ <p>Your support means the world to us and helps our business grow!</p>
24269
+ <p>Thank you so much!</p>
24270
+ <p>Warmly,<br>The ${businessName} Team</p>
24271
+ </div>
24272
+ </body>
24273
+ </html>`;
24274
+ const templates = [
24275
+ {
24276
+ id: "0199110d-eac1-7211-aeb3-1d9d9b60e77b",
24277
+ name: "Private Feedback Request",
24278
+ version: "1.0.0",
24279
+ status: "active",
24280
+ feature: "reputation",
24281
+ visibility: "visible",
24282
+ contents: {
24283
+ name: "reputation-feedback-request",
24284
+ description: "Request private direct feedback from customers",
24285
+ feature: "reputation",
24286
+ triggerMetadata: {
24287
+ cooldownSeconds: 86400,
24288
+ // 1 day after purchase
24289
+ eventFilter: {
24290
+ segment_id: "all_customers"
24291
+ }
24292
+ },
24293
+ actionData: [
24294
+ {
24295
+ type: "send_communication",
24296
+ index: 0,
24297
+ subject: `How was your experience with ${businessName}?`,
24298
+ previewText: "We'd love your honest feedback",
24299
+ compiledHtml: feedbackHtml
24300
+ }
24301
+ ]
24302
+ }
24303
+ },
24304
+ {
24305
+ id: "0199110d-eac1-7211-aeb3-1d9d9b60e77c",
24306
+ name: "Google Review Request",
24307
+ version: "1.0.0",
24308
+ status: "active",
24309
+ feature: "reputation",
24310
+ visibility: "visible",
24311
+ contents: {
24312
+ name: "reputation-google-review-request",
24313
+ description: "Request Google review from satisfied customers (9-10 rating)",
24314
+ feature: "reputation",
24315
+ triggerMetadata: {
24316
+ cooldownSeconds: 0,
24317
+ // Sent immediately after positive feedback
24318
+ eventFilter: {
24319
+ segment_id: "satisfied_customers"
24320
+ }
24321
+ },
24322
+ actionData: [
24323
+ {
24324
+ type: "send_communication",
24325
+ index: 0,
24326
+ subject: `Would you leave us a Google review? - ${businessName}`,
24327
+ previewText: "Share your positive experience with others",
24328
+ compiledHtml: googleReviewHtml
24329
+ }
24330
+ ]
24331
+ }
24332
+ }
24333
+ ];
24334
+ const sortedTemplates = templates.sort(
24335
+ (a2, b2) => a2.name.localeCompare(b2.name, void 0, { sensitivity: "base" })
24336
+ );
24337
+ return HttpResponse.json({
24338
+ success: true,
24339
+ message: "Success (Sandbox)",
24340
+ data: sortedTemplates
24341
+ });
24342
+ }),
24343
+ // Activate Automations
24344
+ http.post(`${HOSTNAME}/api/reputation/activate-automations`, () => {
24345
+ return HttpResponse.json({
24346
+ success: true,
24347
+ message: "Success (Sandbox)",
24348
+ data: {}
24349
+ });
24350
+ }),
24351
+ // Sync Google Reviews
24352
+ http.post(`${HOSTNAME}/api/reputation/sync-google-reviews`, () => {
24353
+ return HttpResponse.json({
24354
+ success: true,
24355
+ message: "Success (Sandbox)",
24356
+ data: {}
24357
+ });
24026
24358
  })
24027
24359
  ];
24028
24360
  const getHandlersByFeatures = (features) => {