File size: 8,246 Bytes
5743bc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
export type BookingManipulation = {
  type: "payment_timer" | "fake_viewers" | "seat_pressure" | "price_surge" | "dynamic_pricing";
  severity: "critical" | "high" | "medium" | "low";
  reality: "REAL" | "FAKE" | "UNCERTAIN";
  confidence: number;
  evidence: string[];
  reasoning: string;
  userAdvice: string;
};

type TimerData = {
  hasTimer?: boolean;
  timerText?: string | null;
  timerValue?: string | null;
  timerResetsOnRefresh?: boolean | null;
};

type PriceData = {
  hasPrice?: boolean;
  currentPrice?: number | null;
  priceChanges?: number | null;
  priceHistory?: Array<{ price?: number | null; timestamp?: number }>;
};

type HistoryData = {
  viewerCountChanges?: boolean | null;
  previousPrice?: number | null;
  priceChanges?: number | null;
};

export function detectPaymentTimer(pageText: string, timerData?: TimerData | null, pageType?: string): BookingManipulation | null {
  const source = `${pageText || ""} ${timerData?.timerText || ""}`;
  const hasTimer = timerData?.hasTimer === true || /(\d+:\d+)|expires?\s*in|time.*left|hurry.*\d+\s*min/i.test(source);
  if (!hasTimer) return null;

  const evidence: string[] = [];
  let reality: BookingManipulation["reality"] = "UNCERTAIN";
  let confidence = 0.5;

  if (timerData?.timerResetsOnRefresh === true) {
    reality = "FAKE";
    confidence = 0.95;
    evidence.push("Timer resets to the same value on refresh.");
  }

  const timerMatch = source.match(/(\d+):(\d+)/);
  if (timerMatch) {
    const minutes = Number(timerMatch[1]);
    if (minutes < 5 && reality !== "FAKE") {
      reality = "FAKE";
      confidence = 0.8;
      evidence.push(`Timer is unusually short at ${timerMatch[0]}.`);
    } else if (minutes >= 10 && minutes <= 20 && reality === "UNCERTAIN") {
      reality = "REAL";
      confidence = 0.7;
      evidence.push("Timer length looks consistent with a real payment session timeout.");
    }
  }

  if (pageType && pageType !== "payment" && reality !== "FAKE") {
    reality = "FAKE";
    confidence = Math.max(confidence, 0.85);
    evidence.push("Timer appears before the payment stage, which is typical of manipulative pressure.");
  }

  return {
    type: "payment_timer",
    severity: reality === "FAKE" ? "high" : "medium",
    reality,
    confidence,
    evidence,
    reasoning:
      reality === "FAKE"
        ? "This timer looks like artificial urgency rather than a genuine payment timeout."
        : reality === "REAL"
          ? "This timer may be tied to a legitimate payment or reservation hold window."
          : "Guardian could not fully verify whether this timer is real or artificial.",
    userAdvice:
      reality === "FAKE"
        ? "Ignore the timer and refresh once to confirm it resets."
        : reality === "REAL"
          ? "Treat the timer as real and finish payment only if you are ready."
          : "Refresh once and compare the timer value before trusting it.",
  };
}

export function detectFakeViewers(pageText: string, historyData?: HistoryData | null): BookingManipulation | null {
  const match =
    pageText.match(/(\d+)\s*people?\s*(viewing|looking\s*at|watching)/i) ||
    pageText.match(/(\d+)\s*others?\s*are\s*considering/i) ||
    pageText.match(/(\d+)\s*travelers?\s*booked/i);
  if (!match) return null;

  const viewerCount = Number(match[1] || 0);
  const evidence = [`Viewer pressure copy detected: ${match[0]}`];
  let reality: BookingManipulation["reality"] = "UNCERTAIN";
  let confidence = 0.5;

  if (historyData?.viewerCountChanges) {
    reality = "FAKE";
    confidence = 0.95;
    evidence.push("Viewer count changed across refreshes.");
  } else if (viewerCount > 100 || viewerCount % 5 === 0) {
    reality = "FAKE";
    confidence = 0.72;
    evidence.push("Viewer count looks suspiciously rounded or unrealistically high.");
  }

  return {
    type: "fake_viewers",
    severity: reality === "FAKE" ? "medium" : "low",
    reality,
    confidence,
    evidence,
    reasoning:
      reality === "FAKE"
        ? "This viewer count likely exists to create competition pressure rather than reflect real users."
        : "Guardian cannot verify whether the viewer count is genuinely live.",
    userAdvice: reality === "FAKE" ? "Ignore the viewer count and focus on price and availability." : "Refresh once to see whether the count changes randomly.",
  };
}

export function detectSeatPressure(pageText: string, bookingType: string): BookingManipulation | null {
  const match =
    pageText.match(/only\s*(\d+)\s*(seat|room|table|spot|ticket)s?\s*left/i) ||
    pageText.match(/(\d+)\s*(seat|room|table)s?\s*remaining/i) ||
    pageText.match(/last\s*(\d+)\s*(available|left)/i) ||
    pageText.match(/almost\s*sold\s*out|high\s*demand|selling\s*fast/i);
  if (!match) return null;

  const count = match[1] ? Number(match[1]) : null;
  const evidence = [`Scarcity claim detected: ${match[0]}`];
  let reality: BookingManipulation["reality"] = count == null ? "FAKE" : "UNCERTAIN";
  let confidence = count == null ? 0.7 : 0.55;

  if (count != null) {
    reality = "REAL";
    confidence = 0.6;
    if (bookingType === "bus" && count > 20) {
      reality = "FAKE";
      confidence = 0.72;
      evidence.push("Claimed scarcity does not look meaningful for a bus-sized inventory.");
    }
  } else {
    evidence.push("The claim is vague rather than based on a specific count.");
  }

  return {
    type: "seat_pressure",
    severity: reality === "FAKE" ? "medium" : "low",
    reality,
    confidence,
    evidence,
    reasoning:
      reality === "FAKE"
        ? "The scarcity claim appears exaggerated or too vague to trust."
        : "The scarcity claim is more specific and may reflect real inventory pressure.",
    userAdvice: reality === "FAKE" ? "Do not rush solely because of this scarcity banner." : "Cross-check availability if the booking matters and the count is low.",
  };
}

export function detectDynamicPricing(currentPrice?: number | null, historyData?: HistoryData | null): BookingManipulation | null {
  if (typeof currentPrice !== "number" || typeof historyData?.previousPrice !== "number" || historyData.previousPrice <= 0) {
    return null;
  }

  const percentChange = ((currentPrice - historyData.previousPrice) / historyData.previousPrice) * 100;
  if (Math.abs(percentChange) < 2) {
    return null;
  }

  const reality: BookingManipulation["reality"] = percentChange > 0 ? "REAL" : "UNCERTAIN";
  const confidence = percentChange > 10 || (historyData.priceChanges || 0) > 2 ? 0.85 : 0.65;
  const evidence = [`Price changed by ${percentChange.toFixed(1)}% compared with the previous observed value.`];

  return {
    type: percentChange > 0 ? "price_surge" : "dynamic_pricing",
    severity: percentChange > 10 ? "high" : "medium",
    reality,
    confidence,
    evidence,
    reasoning:
      percentChange > 0
        ? "Dynamic pricing appears active, so the price may continue to move."
        : "Price movement was detected, but Guardian cannot confirm whether it is a meaningful surge.",
    userAdvice:
      percentChange > 0
        ? "Compare across platforms and decide whether the increase is worth accepting."
        : "Track one more refresh before treating this as a real trend.",
  };
}

export function analyzeBookingManipulation(input: {
  pageText: string;
  bookingType: string;
  pageType?: string;
  timer?: TimerData | null;
  viewers?: { countChanges?: boolean | null } | null;
  seats?: { hasScarcity?: boolean; text?: string | null } | null;
  price?: PriceData | null;
}) {
  const historyData: HistoryData = {
    viewerCountChanges: input.viewers?.countChanges ?? null,
    previousPrice:
      Array.isArray(input.price?.priceHistory) && input.price.priceHistory.length > 1
        ? (input.price.priceHistory[input.price.priceHistory.length - 2]?.price ?? null)
        : null,
    priceChanges: input.price?.priceChanges ?? null,
  };

  return [
    detectPaymentTimer(input.pageText, input.timer, input.pageType),
    detectFakeViewers(input.pageText, historyData),
    detectSeatPressure(`${input.pageText} ${input.seats?.text || ""}`, input.bookingType),
    detectDynamicPricing(input.price?.currentPrice ?? null, historyData),
  ].filter(Boolean) as BookingManipulation[];
}