Add coupon fraud context and analytics tracking
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-02 23:31:26 +01:00
parent 75d862748b
commit 41ed682fbe
16 changed files with 461 additions and 21 deletions

View File

@@ -4,6 +4,10 @@ import { cleanup, render, screen } from '@testing-library/react';
import { CheckoutWizardProvider } from '../WizardContext';
import { PaymentStep } from '../steps/PaymentStep';
vi.mock('@/hooks/useAnalytics', () => ({
useAnalytics: () => ({ trackEvent: vi.fn() }),
}));
const basePackage = {
id: 1,
price: 49,

View File

@@ -15,6 +15,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAnalytics } from '@/hooks/useAnalytics';
type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error';
@@ -184,6 +185,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
export const PaymentStep: React.FC = () => {
const { t, i18n } = useTranslation('marketing');
const { trackEvent } = useAnalytics();
const {
selectedPackage,
nextStep,
@@ -283,6 +285,10 @@ export const PaymentStep: React.FC = () => {
if (RateLimitHelper.isLimited(trimmed)) {
setCouponError(t('coupon.errors.too_many_attempts'));
trackEvent({
category: 'marketing_coupon',
action: 'rate_limited',
});
return;
}
@@ -299,6 +305,11 @@ export const PaymentStep: React.FC = () => {
amount: preview.pricing.formatted.discount,
})
);
trackEvent({
category: 'marketing_coupon',
action: 'applied',
name: preview.coupon.code,
});
setVoucherExpiry(preview.coupon.expires_at ?? null);
setIsGiftVoucher(preview.coupon.code?.toUpperCase().startsWith('GIFT-') ?? false);
if (typeof window !== 'undefined') {
@@ -308,11 +319,15 @@ export const PaymentStep: React.FC = () => {
setCouponPreview(null);
setCouponNotice(null);
setCouponError(error instanceof Error ? error.message : t('coupon.errors.generic'));
trackEvent({
category: 'marketing_coupon',
action: 'apply_failed',
});
RateLimitHelper.bump(trimmed);
} finally {
setCouponLoading(false);
}
}, [selectedPackage, t]);
}, [RateLimitHelper, selectedPackage, t, trackEvent]);
useEffect(() => {
if (hasAutoAppliedCoupon.current) {
@@ -675,6 +690,13 @@ export const PaymentStep: React.FC = () => {
}, [applyCoupon, couponCode, selectedPackage]);
const handleRemoveCoupon = useCallback(() => {
if (couponPreview?.coupon.code) {
trackEvent({
category: 'marketing_coupon',
action: 'removed',
name: couponPreview.coupon.code,
});
}
setCouponPreview(null);
setCouponNotice(null);
setCouponError(null);
@@ -682,7 +704,7 @@ export const PaymentStep: React.FC = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('preferred_coupon_code');
}
}, []);
}, [couponPreview, trackEvent]);
const openWithdrawalModal = useCallback(async () => {
setShowWithdrawalModal(true);