@embedreach/components 0.3.32 → 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
  };
@@ -21434,11 +21431,7 @@ const createWebPresenceContentFactory = (platformConfig, businessConfig) => {
21434
21431
  const createSandboxMeasureAndAcquireData = (platformId, businessId) => {
21435
21432
  const platformConfigKey = getPlatformConfigKey(platformId);
21436
21433
  const platformConfig = {
21437
- id: platformConfigKey,
21438
- name: platformConfigKey === "goose" ? "ACME Pet Spa" : platformConfigKey === "renterra" ? "Tomers Rentals" : "Default",
21439
- brandColors: [],
21440
- locationNames: [],
21441
- locationAreas: []
21434
+ id: platformConfigKey
21442
21435
  };
21443
21436
  const businessConfig = getBusinessConfig(platformId);
21444
21437
  const businessesMe = createBusinessDataFactory(
@@ -21705,7 +21698,7 @@ const native = {
21705
21698
  randomUUID
21706
21699
  };
21707
21700
  function v4(options, buf, offset) {
21708
- if (native.randomUUID && !buf && !options) {
21701
+ if (native.randomUUID && true && !options) {
21709
21702
  return native.randomUUID();
21710
21703
  }
21711
21704
  options = options || {};
@@ -22582,9 +22575,18 @@ const generateReputationResponsesData = ({
22582
22575
  }
22583
22576
  if (searchTerm && searchTerm.length >= 3) {
22584
22577
  const searchLower = searchTerm.toLowerCase();
22585
- filteredResponses = filteredResponses.filter(
22586
- (response) => response.content.toLowerCase().includes(searchLower) || response.userName.toLowerCase().includes(searchLower) || response.userEmail.toLowerCase().includes(searchLower)
22587
- );
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
+ });
22588
22590
  }
22589
22591
  filteredResponses.sort(
22590
22592
  (a2, b2) => new Date(b2.createdAt).getTime() - new Date(a2.createdAt).getTime()
@@ -22641,41 +22643,79 @@ function createGoogleReview(platformConfig, partnerLocations, startDate, endDate
22641
22643
  ]);
22642
22644
  const reviewContent = generateReviewContent(platformConfig, rating);
22643
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";
22644
22669
  return {
22645
22670
  id: f.string.uuid(),
22646
22671
  source: "google",
22672
+ reviewSource: "google_business_profile",
22647
22673
  rating,
22648
- userName: f.person.fullName(),
22649
- userEmail: f.internet.email(),
22650
- // 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
22651
22676
  content: reviewContent,
22652
22677
  createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
22653
- hasReply: f.helpers.weightedArrayElement([
22654
- { weight: 30, value: true },
22655
- // 30% have business replies
22656
- { weight: 70, value: false }
22657
- ]),
22678
+ hasReply,
22658
22679
  isResponded: false,
22659
22680
  // Not applicable for Google reviews
22660
- actionUrl: `https://business.google.com/reviews/l/${f.string.alphanumeric(21)}`,
22661
- 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)
22662
22685
  };
22663
22686
  }
22664
22687
  function createInternalFeedback(platformConfig, partnerLocations, startDate, endDate) {
22665
22688
  const rating = f.helpers.weightedArrayElement([
22666
- { weight: 80, value: 5 },
22667
- // Thumbs up (positive feedback) - 80%
22668
- { weight: 20, value: 1 }
22669
- // 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)
22670
22709
  ]);
22671
22710
  const feedbackContent = generateFeedbackContent(platformConfig, rating);
22672
22711
  const location2 = partnerLocations.length > 0 ? f.helpers.arrayElement(partnerLocations) : null;
22673
22712
  return {
22674
22713
  id: f.string.uuid(),
22675
22714
  source: "internal",
22715
+ reviewSource: "internal_feedback",
22676
22716
  rating,
22677
22717
  userName: f.person.fullName(),
22678
- userEmail: f.internet.email(),
22718
+ // userEmail: faker.internet.email(),
22679
22719
  content: feedbackContent,
22680
22720
  createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
22681
22721
  hasReply: false,
@@ -22685,8 +22725,6 @@ function createInternalFeedback(platformConfig, partnerLocations, startDate, end
22685
22725
  // 40% have customer responses
22686
22726
  { weight: 60, value: false }
22687
22727
  ]),
22688
- actionUrl: void 0,
22689
- // Internal feedback doesn't have external action URLs
22690
22728
  locationId: location2?.externalId || void 0
22691
22729
  };
22692
22730
  }
@@ -22759,42 +22797,78 @@ function getReviewTemplates(platformId, rating) {
22759
22797
  if (rating <= 2) return commonNegative;
22760
22798
  return commonNeutral;
22761
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
+ }
22762
22823
  function getFeedbackTemplates(platformId, rating) {
22763
- const commonPositive = [
22764
- "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.",
22765
- "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.",
22766
- "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."
22767
22829
  ];
22768
- 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 = [
22769
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.",
22770
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.",
22771
- "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."
22772
- ];
22773
- const commonNeutral = [
22774
- "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.",
22775
- "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."
22776
22840
  ];
22777
22841
  if (platformId === "goose") {
22778
- const petSpaDetailed = rating >= 4 ? [
22779
- "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."
22780
- ] : [
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 = [
22781
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."
22782
22850
  ];
22783
- if (rating >= 4) return [...commonPositive, ...petSpaDetailed];
22784
- if (rating <= 2) return [...commonNegative, ...petSpaDetailed];
22851
+ if (rating >= 9) return [...commonPromoters, ...petSpaPromoters];
22852
+ if (rating >= 7) return [...commonPassives, ...petSpaPassives];
22853
+ return [...commonDetractors, ...petSpaDetractors];
22785
22854
  }
22786
22855
  if (platformId === "renterra") {
22787
- const rentalDetailed = rating >= 4 ? [
22788
- "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."
22789
- ] : [
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 = [
22790
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."
22791
22864
  ];
22792
- if (rating >= 4) return [...commonPositive, ...rentalDetailed];
22793
- if (rating <= 2) return [...commonNegative, ...rentalDetailed];
22865
+ if (rating >= 9) return [...commonPromoters, ...rentalPromoters];
22866
+ if (rating >= 7) return [...commonPassives, ...rentalPassives];
22867
+ return [...commonDetractors, ...rentalDetractors];
22794
22868
  }
22795
- if (rating >= 4) return commonPositive;
22796
- if (rating <= 2) return commonNegative;
22797
- return commonNeutral;
22869
+ if (rating >= 9) return commonPromoters;
22870
+ if (rating >= 7) return commonPassives;
22871
+ return commonDetractors;
22798
22872
  }
22799
22873
  const generateReviewAnalyticsData = ({
22800
22874
  platformData,
@@ -22820,36 +22894,55 @@ const generateReviewAnalyticsData = ({
22820
22894
  false,
22821
22895
  isAllTimeQuery || false
22822
22896
  );
22823
- const includeComparison = Boolean(queryParams.include_comparison);
22824
- if (includeComparison) {
22825
- const previousPeriodData = generateReviewPeriodData(
22826
- platformConfig,
22827
- scalingFactor,
22828
- true,
22829
- isAllTimeQuery || false
22830
- );
22831
- const comparison = calculateReviewComparison(
22832
- currentPeriodData,
22833
- previousPeriodData
22834
- );
22835
- const responseWithComparison = {
22836
- reviewSource: "google_business_profile",
22837
- current: currentPeriodData,
22838
- previous: previousPeriodData,
22839
- comparison
22840
- };
22841
- return responseWithComparison;
22842
- }
22843
- 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 = {
22844
22923
  reviewSource: "google_business_profile",
22845
- totalCount: currentPeriodData.totalCount,
22846
- averageRating: currentPeriodData.averageRating,
22847
- 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
+ }
22848
22941
  };
22849
- return basicResponse;
22942
+ return fullResponse;
22850
22943
  };
22851
22944
  function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
22852
- const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics$1(platformConfig);
22945
+ const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics(platformConfig);
22853
22946
  const scaledTotalCount = isAllTime ? baseMetrics.totalCount : Math.max(1, Math.round(baseMetrics.totalCount * scalingFactor));
22854
22947
  const performanceMultiplier = isPrevious ? 0.9 : 1;
22855
22948
  const adjustedTotalCount = Math.round(
@@ -22857,7 +22950,7 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
22857
22950
  );
22858
22951
  const ratingDelta = isPrevious ? -0.2 : 0;
22859
22952
  const adjustedAverageRating = baseMetrics.averageRating + ratingDelta;
22860
- const ratingDistribution = generateRatingDistribution(
22953
+ const ratingDistribution = generateRatingDistribution$1(
22861
22954
  adjustedTotalCount,
22862
22955
  adjustedAverageRating
22863
22956
  );
@@ -22867,22 +22960,24 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
22867
22960
  ratingDistribution
22868
22961
  };
22869
22962
  }
22870
- function getBasePlatformReviewMetrics$1(platformConfig) {
22963
+ function getBasePlatformReviewMetrics(platformConfig) {
22871
22964
  const industryMultipliers = {
22872
22965
  // Pet spa industry (Goose) - high customer satisfaction
22873
- "ACME Pet Spa": {
22874
- totalCount: 8,
22875
- // Much lower - reviews are rare compared to emails
22876
- averageRating: 4.6
22966
+ "Paws Pet Hotel": {
22967
+ totalCount: 12,
22968
+ // ~12 reviews per 30 days
22969
+ averageRating: 4.5
22877
22970
  },
22878
22971
  // Equipment rental (Renterra) - moderate review volume
22879
22972
  "Tomer's Rentals": {
22880
- totalCount: 5,
22973
+ totalCount: 8,
22974
+ // ~8 reviews per 30 days
22881
22975
  averageRating: 4.2
22882
22976
  },
22883
22977
  // Default platform
22884
22978
  default: {
22885
- totalCount: 6,
22979
+ totalCount: 10,
22980
+ // ~10 reviews per 30 days
22886
22981
  averageRating: 4.3
22887
22982
  }
22888
22983
  };
@@ -22892,27 +22987,28 @@ function getBasePlatformReviewMetrics$1(platformConfig) {
22892
22987
  function getAllTimePlatformReviewMetrics(platformConfig) {
22893
22988
  const industryMultipliers = {
22894
22989
  // Pet spa industry (Goose) - high customer satisfaction
22895
- "ACME Pet Spa": {
22896
- totalCount: 124,
22897
- // Much lower - reviews are rare compared to emails
22898
- averageRating: 4.3
22899
- // 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
22900
22994
  },
22901
22995
  // Equipment rental (Renterra) - moderate review volume
22902
22996
  "Tomer's Rentals": {
22903
- totalCount: 78,
22997
+ totalCount: 27,
22998
+ // ~100 days of reviews (8 per 30 days)
22904
22999
  averageRating: 4.1
22905
23000
  },
22906
23001
  // Default platform
22907
23002
  default: {
22908
- totalCount: 95,
23003
+ totalCount: 33,
23004
+ // ~100 days of reviews (10 per 30 days)
22909
23005
  averageRating: 4.2
22910
23006
  }
22911
23007
  };
22912
23008
  const baseKey = platformConfig.name in industryMultipliers ? platformConfig.name : "default";
22913
23009
  return industryMultipliers[baseKey];
22914
23010
  }
22915
- function generateRatingDistribution(totalCount, targetAverage) {
23011
+ function generateRatingDistribution$1(totalCount, targetAverage) {
22916
23012
  const baseDistribution = {
22917
23013
  5: 0.6,
22918
23014
  // 60% 5-star
@@ -22953,28 +23049,6 @@ function adjustDistributionForAverage(baseDistribution, targetAverage) {
22953
23049
  }
22954
23050
  return adjusted;
22955
23051
  }
22956
- function calculateReviewComparison(currentPeriod, previousPeriod) {
22957
- const totalCountChange = currentPeriod.totalCount - previousPeriod.totalCount;
22958
- const totalCountChangePercent = previousPeriod.totalCount > 0 ? parseFloat(
22959
- (totalCountChange / previousPeriod.totalCount * 100).toFixed(1)
22960
- ) : null;
22961
- const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
22962
- (currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
22963
- 2
22964
- )
22965
- ) : null;
22966
- const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
22967
- (averageRatingChange / previousPeriod.averageRating * 100).toFixed(
22968
- 1
22969
- )
22970
- ) : null;
22971
- return {
22972
- totalCountChange,
22973
- totalCountChangePercent,
22974
- averageRatingChange,
22975
- averageRatingChangePercent
22976
- };
22977
- }
22978
23052
  const generateFeedbackAnalyticsData = ({
22979
23053
  platformData,
22980
23054
  queryParams
@@ -22999,38 +23073,86 @@ const generateFeedbackAnalyticsData = ({
22999
23073
  false,
23000
23074
  isAllTimeQuery
23001
23075
  );
23002
- if (queryParams.include_comparison) {
23003
- const previousPeriodData = generateFeedbackPeriodData(
23004
- platformConfig,
23005
- scalingFactor,
23006
- true,
23007
- isAllTimeQuery
23008
- );
23009
- const comparison = calculateFeedbackComparison(
23010
- currentPeriodData,
23011
- previousPeriodData
23012
- );
23013
- const responseWithComparison = {
23014
- current: currentPeriodData,
23015
- previous: previousPeriodData,
23016
- comparison
23017
- };
23018
- return responseWithComparison;
23019
- }
23020
- const basicResponse = {
23021
- totalSent: currentPeriodData.totalSent,
23022
- totalResponded: currentPeriodData.totalResponded,
23023
- averageRating: currentPeriodData.averageRating,
23024
- responseRate: currentPeriodData.responseRate,
23025
- positiveResponses: currentPeriodData.positiveResponses,
23026
- negativeResponses: currentPeriodData.negativeResponses,
23027
- passiveResponses: currentPeriodData.passiveResponses,
23028
- 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
+ }
23029
23151
  };
23030
- return basicResponse;
23152
+ return fullResponse;
23031
23153
  };
23032
23154
  function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
23033
- const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics$1(platformConfig);
23155
+ const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics(platformConfig);
23034
23156
  const scaledTotalSent = isAllTime ? baseMetrics.totalSent : Math.max(1, Math.round(baseMetrics.totalSent * scalingFactor));
23035
23157
  const performanceMultiplier = isPrevious ? 0.82 : 1;
23036
23158
  const adjustedTotalSent = Math.round(scaledTotalSent * performanceMultiplier);
@@ -23068,10 +23190,10 @@ function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious =
23068
23190
  // real NPS score
23069
23191
  };
23070
23192
  }
23071
- function getBasePlatformFeedbackMetrics$1(platformConfig) {
23193
+ function getBasePlatformFeedbackMetrics(platformConfig) {
23072
23194
  const industryMultipliers = {
23073
23195
  // Pet spa industry (Goose) - high engagement, positive feedback
23074
- "ACME Pet Spa": {
23196
+ "Paws Pet Hotel": {
23075
23197
  totalSent: 1250,
23076
23198
  // Much higher - businesses send way more emails than get reviews
23077
23199
  responseRate: 42,
@@ -23114,7 +23236,7 @@ function getBasePlatformFeedbackMetrics$1(platformConfig) {
23114
23236
  function getAllTimePlatformFeedbackMetrics(platformConfig) {
23115
23237
  const industryMultipliers = {
23116
23238
  // Pet spa industry (Goose) - high engagement, positive feedback
23117
- "ACME Pet Spa": {
23239
+ "Paws Pet Hotel": {
23118
23240
  totalSent: 18500,
23119
23241
  // WAY higher - businesses send tons of emails over time
23120
23242
  responseRate: 39,
@@ -23163,48 +23285,6 @@ function calculateAverageFromNPS(promoters, passives, detractors) {
23163
23285
  const weightedSum = promoters * promoterAverage + passives * passiveAverage + detractors * detractorAverage;
23164
23286
  return weightedSum / totalResponses;
23165
23287
  }
23166
- function calculateFeedbackComparison(currentPeriod, previousPeriod) {
23167
- const totalSentChange = currentPeriod.totalSent - previousPeriod.totalSent;
23168
- const totalSentChangePercent = previousPeriod.totalSent > 0 ? parseFloat(
23169
- (totalSentChange / previousPeriod.totalSent * 100).toFixed(1)
23170
- ) : null;
23171
- const totalRespondedChange = currentPeriod.totalResponded - previousPeriod.totalResponded;
23172
- const totalRespondedChangePercent = previousPeriod.totalResponded > 0 ? parseFloat(
23173
- (totalRespondedChange / previousPeriod.totalResponded * 100).toFixed(1)
23174
- ) : null;
23175
- const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
23176
- (currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
23177
- 2
23178
- )
23179
- ) : null;
23180
- const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
23181
- (averageRatingChange / previousPeriod.averageRating * 100).toFixed(
23182
- 1
23183
- )
23184
- ) : null;
23185
- const responseRateChange = currentPeriod.responseRate - previousPeriod.responseRate;
23186
- const responseRateChangePercent = previousPeriod.responseRate > 0 ? parseFloat(
23187
- (responseRateChange / previousPeriod.responseRate * 100).toFixed(1)
23188
- ) : null;
23189
- const npsScoreChange = currentPeriod.npsScore - previousPeriod.npsScore;
23190
- const npsScoreChangePercent = previousPeriod.npsScore !== 0 ? parseFloat(
23191
- (npsScoreChange / Math.abs(previousPeriod.npsScore) * 100).toFixed(
23192
- 1
23193
- )
23194
- ) : null;
23195
- return {
23196
- totalSentChange,
23197
- totalSentChangePercent,
23198
- totalRespondedChange,
23199
- totalRespondedChangePercent,
23200
- averageRatingChange,
23201
- averageRatingChangePercent,
23202
- responseRateChange: parseFloat(responseRateChange.toFixed(1)),
23203
- responseRateChangePercent,
23204
- npsScoreChange,
23205
- npsScoreChangePercent
23206
- };
23207
- }
23208
23288
  const generateAuditLogData = ({
23209
23289
  startDate,
23210
23290
  endDate,
@@ -23270,24 +23350,27 @@ function generateAuditLogPool(startDate, endDate, count) {
23270
23350
  const logs = [];
23271
23351
  const timeSpan = endDate.getTime() - startDate.getTime();
23272
23352
  const eventSequences = [
23273
- // Survey flow
23353
+ // Private Feedback flow
23274
23354
  {
23275
23355
  type: "SURVEY_REQUEST",
23276
23356
  channel: "EMAIL",
23277
23357
  statuses: ["SENT", "DELIVERED", "OPENED", "COMPLETED"],
23278
23358
  messages: [
23279
- "Survey request sent to customer",
23280
- "Survey request delivered to customer",
23281
- "Customer opened survey request",
23282
- "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"
23283
23363
  ]
23284
23364
  },
23285
- // Review flow
23365
+ // Google Review flow
23286
23366
  {
23287
23367
  type: "REVIEW_REQUEST",
23288
23368
  channel: "SMS",
23289
23369
  statuses: ["SENT", "DELIVERED"],
23290
- messages: ["Review request sent to", "Review request delivered to"]
23370
+ messages: [
23371
+ "Google review request sent to",
23372
+ "Google review request delivered to"
23373
+ ]
23291
23374
  },
23292
23375
  // Google review flow
23293
23376
  {
@@ -23304,13 +23387,19 @@ function generateAuditLogPool(startDate, endDate, count) {
23304
23387
  type: "SURVEY_REMINDER",
23305
23388
  channel: "EMAIL",
23306
23389
  statuses: ["SENT", "DELIVERED"],
23307
- messages: ["Survey reminder sent to", "Survey reminder delivered to"]
23390
+ messages: [
23391
+ "Private feedback reminder sent to",
23392
+ "Private feedback reminder delivered to"
23393
+ ]
23308
23394
  },
23309
23395
  {
23310
23396
  type: "REVIEW_REMINDER",
23311
23397
  channel: "SMS",
23312
23398
  statuses: ["SENT", "DELIVERED"],
23313
- messages: ["Review reminder sent to", "Review reminder delivered to"]
23399
+ messages: [
23400
+ "Google review reminder sent to",
23401
+ "Google review reminder delivered to"
23402
+ ]
23314
23403
  }
23315
23404
  ];
23316
23405
  for (let i2 = 0; i2 < count; i2++) {
@@ -23352,125 +23441,112 @@ const generateReviewTimeSeriesData = ({
23352
23441
  platformData,
23353
23442
  queryParams
23354
23443
  }) => {
23355
- const platformConfig = getPlatformConfig(
23356
- platformData.businessesMe?.platform_id
23357
- );
23444
+ const analyticsData = generateReviewAnalyticsData({
23445
+ platformData,
23446
+ queryParams
23447
+ });
23358
23448
  const { requestedDays, startDate, endDate } = parseDateRange(
23359
23449
  queryParams.start_date,
23360
23450
  queryParams.end_date,
23361
23451
  30
23362
23452
  );
23363
- const baseMetrics = getBasePlatformReviewMetrics(platformConfig);
23364
- const basePeriodDays = 30;
23365
- const scalingFactor = requestedDays / basePeriodDays;
23366
- const totalScaledReviews = Math.max(
23367
- 1,
23368
- Math.round(baseMetrics.totalReviews * scalingFactor)
23369
- );
23453
+ const isAllTimeQuery = !queryParams.start_date || !queryParams.end_date || queryParams.start_date.startsWith("2020-");
23370
23454
  let intervalDays = 7;
23371
23455
  if (requestedDays <= 30)
23372
23456
  intervalDays = 1;
23373
23457
  else if (requestedDays <= 90) intervalDays = 3;
23374
23458
  const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23375
23459
  const dataPoints = generateReviewTimeSeriesPoints({
23376
- totalScaledReviews,
23460
+ analyticsData,
23377
23461
  startDate,
23378
23462
  endDate,
23379
23463
  numDataPoints,
23380
- intervalDays
23464
+ intervalDays,
23465
+ isAllTimeQuery
23381
23466
  });
23382
23467
  return {
23383
23468
  data: dataPoints
23384
23469
  };
23385
23470
  };
23386
23471
  function generateReviewTimeSeriesPoints({
23387
- totalScaledReviews,
23472
+ analyticsData,
23388
23473
  startDate,
23389
23474
  endDate,
23390
23475
  numDataPoints,
23391
- intervalDays
23476
+ intervalDays,
23477
+ isAllTimeQuery
23392
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;
23393
23486
  const dataPoints = [];
23394
23487
  let currentDate = new Date(startDate);
23395
- let cumulativeReviews = 0;
23396
- const basePointReviews = totalScaledReviews / numDataPoints;
23397
- const targetRating = 4.2;
23398
- const startingRating = 4;
23399
- 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;
23400
23493
  for (let i2 = 0; i2 < numDataPoints; i2++) {
23401
23494
  const pointDateStr = currentDate.toISOString().split("T")[0];
23402
- let currentPointReviews;
23403
- let currentPointRating;
23404
- if (i2 < numDataPoints - 1) {
23405
- currentPointReviews = Math.round(
23406
- basePointReviews * (1 + (Math.random() - 0.5) * 0.5)
23407
- );
23408
- const progress = i2 / (numDataPoints - 1);
23409
- currentPointRating = startingRating + (endingRating - startingRating) * Math.pow(progress, 1.3);
23410
- 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;
23411
23505
  } else {
23412
- currentPointReviews = Math.max(0, totalScaledReviews - cumulativeReviews);
23413
- currentPointRating = targetRating;
23506
+ const cumulativeProgress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
23507
+ cumulativeAverageRating = parseFloat(
23508
+ (targetStartCumulative + (targetEndCumulative - targetStartCumulative) * cumulativeProgress).toFixed(2)
23509
+ );
23414
23510
  }
23415
- currentPointReviews = Math.max(0, currentPointReviews);
23416
- currentPointRating = Math.max(1, Math.min(5, currentPointRating));
23417
- cumulativeReviews += currentPointReviews;
23418
23511
  const { promoterCount, detractorCount } = calculatePromoterDetractorSplit(
23419
23512
  currentPointReviews,
23420
23513
  currentPointRating
23421
23514
  );
23515
+ const ratingDistribution = generateRatingDistribution(
23516
+ currentPointReviews,
23517
+ currentPointRating
23518
+ );
23422
23519
  dataPoints.push({
23423
23520
  date: pointDateStr,
23424
23521
  totalReviews: currentPointReviews,
23425
23522
  averageRating: currentPointReviews > 0 ? parseFloat(currentPointRating.toFixed(1)) : null,
23523
+ cumulativeAverageRating,
23426
23524
  promoterCount,
23427
- detractorCount
23525
+ detractorCount,
23526
+ ratingDistribution
23428
23527
  });
23429
23528
  currentDate.setDate(currentDate.getDate() + intervalDays);
23430
23529
  if (currentDate > endDate && i2 < numDataPoints - 1) {
23431
23530
  currentDate = new Date(endDate);
23432
- if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23433
- break;
23434
- }
23435
23531
  }
23436
23532
  }
23437
23533
  return dataPoints;
23438
23534
  }
23439
- function getBasePlatformReviewMetrics(platformConfig) {
23440
- const industryMetrics = {
23441
- // Pet spa industry (Goose) - high customer satisfaction, more reviews
23442
- "ACME Pet Spa": {
23443
- totalReviews: 52,
23444
- // Slightly higher than analytics factory
23445
- averageRating: 4.5,
23446
- promoterRate: 0.65,
23447
- // 65% 5-star
23448
- detractorRate: 0.15
23449
- // 15% 1-3 star
23450
- },
23451
- // Equipment rental (Renterra) - moderate review volume, good satisfaction
23452
- "Tomer's Rentals": {
23453
- totalReviews: 31,
23454
- averageRating: 4.2,
23455
- promoterRate: 0.55,
23456
- // 55% 5-star
23457
- detractorRate: 0.2
23458
- // 20% 1-3 star
23459
- },
23460
- // Default platform
23461
- default: {
23462
- totalReviews: 95,
23463
- // Match all-time analytics exactly
23464
- averageRating: 4.2,
23465
- // Match all-time analytics exactly
23466
- promoterRate: 0.6,
23467
- // 60% 5-star
23468
- detractorRate: 0.18
23469
- // 18% 1-3 star
23470
- }
23471
- };
23472
- const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
23473
- 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;
23474
23550
  }
23475
23551
  function calculatePromoterDetractorSplit(totalReviews, averageRating) {
23476
23552
  if (totalReviews === 0) {
@@ -23497,33 +23573,80 @@ function calculatePromoterDetractorSplit(totalReviews, averageRating) {
23497
23573
  const detractorCount = Math.round(totalReviews * detractorRate);
23498
23574
  return { promoterCount, detractorCount };
23499
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
+ }
23500
23630
  const generateFeedbackTimeSeriesData = ({
23501
23631
  platformData,
23502
23632
  queryParams
23503
23633
  }) => {
23504
- const platformConfig = getPlatformConfig(
23505
- platformData.businessesMe?.platform_id
23506
- );
23634
+ const analyticsData = generateFeedbackAnalyticsData({
23635
+ platformData,
23636
+ queryParams
23637
+ });
23507
23638
  const { requestedDays, startDate, endDate } = parseDateRange(
23508
23639
  queryParams.start_date,
23509
23640
  queryParams.end_date,
23510
23641
  30
23511
23642
  );
23512
- const baseMetrics = getBasePlatformFeedbackMetrics(platformConfig);
23513
- const basePeriodDays = 30;
23514
- const scalingFactor = requestedDays / basePeriodDays;
23515
- const totalScaledSent = Math.max(
23516
- 1,
23517
- Math.round(baseMetrics.totalSent * scalingFactor)
23518
- );
23519
23643
  let intervalDays = 7;
23520
23644
  if (requestedDays <= 30)
23521
23645
  intervalDays = 1;
23522
23646
  else if (requestedDays <= 90) intervalDays = 3;
23523
23647
  const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
23524
23648
  const dataPoints = generateFeedbackTimeSeriesPoints({
23525
- totalScaledSent,
23526
- baseMetrics,
23649
+ analyticsData,
23527
23650
  startDate,
23528
23651
  endDate,
23529
23652
  numDataPoints,
@@ -23533,52 +23656,54 @@ const generateFeedbackTimeSeriesData = ({
23533
23656
  data: dataPoints
23534
23657
  };
23535
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
+ }
23536
23675
  function generateFeedbackTimeSeriesPoints({
23537
- totalScaledSent,
23538
- baseMetrics,
23676
+ analyticsData,
23539
23677
  startDate,
23540
23678
  endDate,
23541
23679
  numDataPoints,
23542
23680
  intervalDays
23543
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;
23544
23693
  const dataPoints = [];
23545
23694
  let currentDate = new Date(startDate);
23546
- let cumulativeSent = 0;
23547
- const basePointSent = totalScaledSent / numDataPoints;
23548
- const startingResponseRate = baseMetrics.responseRate * 0.75;
23549
- const endingResponseRate = baseMetrics.responseRate * 1.15;
23550
- const startingNPS = 30;
23551
- const endingNPS = 42;
23552
23695
  for (let i2 = 0; i2 < numDataPoints; i2++) {
23553
23696
  const pointDateStr = currentDate.toISOString().split("T")[0];
23554
- let currentPointSent;
23555
- let currentPointResponseRate;
23556
- let currentPointNPS;
23557
- if (i2 < numDataPoints - 1) {
23558
- currentPointSent = Math.round(
23559
- basePointSent * (1 + (Math.random() - 0.5) * 0.5)
23560
- );
23561
- const progress = i2 / (numDataPoints - 1);
23562
- currentPointResponseRate = startingResponseRate + (endingResponseRate - startingResponseRate) * Math.pow(progress, 1.3);
23563
- currentPointNPS = startingNPS + (endingNPS - startingNPS) * Math.pow(progress, 1.2);
23564
- currentPointResponseRate *= 1 + (Math.random() - 0.5) * 0.1;
23565
- currentPointNPS *= 1 + (Math.random() - 0.5) * 0.1;
23566
- } else {
23567
- currentPointSent = Math.max(0, totalScaledSent - cumulativeSent);
23568
- currentPointResponseRate = baseMetrics.responseRate;
23569
- currentPointNPS = 42;
23570
- }
23571
- currentPointSent = Math.max(0, currentPointSent);
23572
- currentPointResponseRate = Math.max(
23573
- 5,
23574
- Math.min(80, currentPointResponseRate)
23575
- );
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;
23576
23702
  currentPointNPS = Math.max(-100, Math.min(100, currentPointNPS));
23577
- cumulativeSent += currentPointSent;
23578
- const currentPointResponded = Math.round(
23579
- currentPointSent * (currentPointResponseRate / 100)
23580
- );
23581
23703
  const { promoterCount, detractorCount, passiveCount } = calculateNPSBreakdown(currentPointResponded, currentPointNPS);
23704
+ const cumulativeNPS = parseFloat(
23705
+ (targetStartNPS + (targetEndNPS - targetStartNPS) * progress).toFixed(1)
23706
+ );
23582
23707
  dataPoints.push({
23583
23708
  date: pointDateStr,
23584
23709
  totalSent: currentPointSent,
@@ -23586,56 +23711,16 @@ function generateFeedbackTimeSeriesPoints({
23586
23711
  promoterCount,
23587
23712
  detractorCount,
23588
23713
  passiveCount,
23589
- npsScore: parseFloat(currentPointNPS.toFixed(1))
23590
- // Use the target NPS (30 42)
23714
+ npsScore: cumulativeNPS
23715
+ // Use cumulative NPS for the chart line
23591
23716
  });
23592
23717
  currentDate.setDate(currentDate.getDate() + intervalDays);
23593
23718
  if (currentDate > endDate && i2 < numDataPoints - 1) {
23594
23719
  currentDate = new Date(endDate);
23595
- if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
23596
- break;
23597
- }
23598
23720
  }
23599
23721
  }
23600
23722
  return dataPoints;
23601
23723
  }
23602
- function getBasePlatformFeedbackMetrics(platformConfig) {
23603
- const industryMetrics = {
23604
- // Pet spa industry (Goose) - high engagement, positive feedback
23605
- "ACME Pet Spa": {
23606
- totalSent: 1200,
23607
- // Much higher - realistic email volume
23608
- responseRate: 44,
23609
- // 44% response rate
23610
- targetNPS: 42,
23611
- // Match the all-time NPS score (42)
23612
- promoterRate: 0.78
23613
- // 78% promoters
23614
- },
23615
- // Equipment rental (Renterra) - moderate engagement
23616
- "Tomer's Rentals": {
23617
- totalSent: 950,
23618
- responseRate: 37,
23619
- // 37% response rate
23620
- targetNPS: 55,
23621
- // Good NPS for B2B
23622
- promoterRate: 0.72
23623
- // 72% promoters
23624
- },
23625
- // Default platform
23626
- default: {
23627
- totalSent: 1050,
23628
- responseRate: 40,
23629
- // 40% response rate
23630
- targetNPS: 60,
23631
- // Good overall NPS
23632
- promoterRate: 0.75
23633
- // 75% promoters
23634
- }
23635
- };
23636
- const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
23637
- return industryMetrics[baseKey];
23638
- }
23639
23724
  function calculateNPSBreakdown(totalResponses, targetNPS) {
23640
23725
  if (totalResponses === 0) {
23641
23726
  return { promoterCount: 0, detractorCount: 0, passiveCount: 0 };
@@ -23666,7 +23751,8 @@ function calculateNPSBreakdown(totalResponses, targetNPS) {
23666
23751
  };
23667
23752
  }
23668
23753
  const mockStore = {
23669
- reputationConfig: null
23754
+ reputationConfig: null,
23755
+ partnerLocations: null
23670
23756
  };
23671
23757
  const reputationHandlers = [
23672
23758
  // External OAuth - GBP Connection
@@ -23681,14 +23767,51 @@ const reputationHandlers = [
23681
23767
  // Partner Locations
23682
23768
  http.get(`${HOSTNAME}/api/partner-locations`, () => {
23683
23769
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23770
+ const locations = mockStore.partnerLocations || platformData.reputationData?.partnerLocations || [];
23684
23771
  return HttpResponse.json({
23685
23772
  success: true,
23686
23773
  message: "Success (Sandbox)",
23687
23774
  data: {
23688
- locations: platformData.reputationData?.partnerLocations || []
23775
+ locations
23689
23776
  }
23690
23777
  });
23691
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
+ ),
23692
23815
  // Channel Senders
23693
23816
  http.get(`${HOSTNAME}/api/channel/senders`, () => {
23694
23817
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
@@ -23714,10 +23837,19 @@ const reputationHandlers = [
23714
23837
  // Reputation Config
23715
23838
  http.get(`${HOSTNAME}/api/reputation/config`, () => {
23716
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
+ });
23717
23845
  const response2 = {
23718
23846
  success: true,
23719
23847
  message: "Success (Sandbox)",
23720
- data: mockStore.reputationConfig
23848
+ data: {
23849
+ ...mockStore.reputationConfig,
23850
+ locationMappings
23851
+ // Always return current mappings from partner locations
23852
+ }
23721
23853
  };
23722
23854
  return HttpResponse.json(response2);
23723
23855
  }
@@ -23737,7 +23869,7 @@ const reputationHandlers = [
23737
23869
  if (currentConfig) {
23738
23870
  const updatedConfig = {
23739
23871
  ...currentConfig,
23740
- locationMappings: body.locationMappings || currentConfig.locationMappings,
23872
+ // Note: locationMappings are now stored in partner_locations table, not here
23741
23873
  emailChannelSenderId: body.emailChannelSenderId,
23742
23874
  smsChannelSenderId: body.smsChannelSenderId,
23743
23875
  // Preserve existing completedOnboardingAt if not provided in update
@@ -23746,10 +23878,19 @@ const reputationHandlers = [
23746
23878
  };
23747
23879
  mockStore.reputationConfig = updatedConfig;
23748
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
+ });
23749
23886
  const response = {
23750
23887
  success: true,
23751
23888
  message: "Configuration updated successfully (Sandbox)",
23752
- data: mockStore.reputationConfig
23889
+ data: {
23890
+ ...mockStore.reputationConfig,
23891
+ locationMappings
23892
+ // Include current mappings from partner locations
23893
+ }
23753
23894
  };
23754
23895
  return HttpResponse.json(response);
23755
23896
  }),
@@ -23899,14 +24040,11 @@ const reputationHandlers = [
23899
24040
  const startDate = url.searchParams.get("start_date");
23900
24041
  const endDate = url.searchParams.get("end_date");
23901
24042
  const locationIds = url.searchParams.get("location_ids");
23902
- const includeComparison = url.searchParams.get("include_comparison") === "true";
23903
24043
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23904
24044
  const queryParams = {
23905
- platform: "google_business_profile",
23906
24045
  start_date: startDate || void 0,
23907
24046
  end_date: endDate || void 0,
23908
- location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0,
23909
- include_comparison: includeComparison
24047
+ location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0
23910
24048
  };
23911
24049
  const analyticsData = generateReviewAnalyticsData({
23912
24050
  platformData,
@@ -23954,11 +24092,8 @@ const reputationHandlers = [
23954
24092
  const url = new URL(request.url);
23955
24093
  const startDate = url.searchParams.get("start_date");
23956
24094
  const endDate = url.searchParams.get("end_date");
23957
- const includeComparison = url.searchParams.get("include_comparison") === "true";
23958
24095
  const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
23959
- const queryParams = {
23960
- include_comparison: includeComparison
23961
- };
24096
+ const queryParams = {};
23962
24097
  if (startDate) {
23963
24098
  queryParams.start_date = startDate;
23964
24099
  }
@@ -24028,6 +24163,198 @@ const reputationHandlers = [
24028
24163
  message: "Success (Sandbox)",
24029
24164
  data: auditLogResponse
24030
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
+ });
24031
24358
  })
24032
24359
  ];
24033
24360
  const getHandlersByFeatures = (features) => {