Skip to content

Commit

Permalink
Chore: Refactor and migrate Personalization to app router INTER-911…
Browse files Browse the repository at this point in the history
…, INTER-459 (#165)

* chore: move personalization client to app

* chore: move personalization api to app part 1

* chore: move personalization api to app part 2

* chore: move personalization api to app part 3

* fix search terms query

* fix products query

* rough refactor done

* move files to `app`

* move files to `app`, remove deprecated code

* chore: self review fixes

* chore: self review fixes
  • Loading branch information
JuroUhlar authored Oct 4, 2024
1 parent 48566ca commit 9dd668e
Show file tree
Hide file tree
Showing 63 changed files with 552 additions and 1,113 deletions.
2 changes: 1 addition & 1 deletion e2e/coupon-fraud.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Page, test, expect } from '@playwright/test';
import { blockGoogleTagManager, resetScenarios } from './e2eTestUtils';
import { TEST_IDS } from '../src/client/testIDs';
import { COUPON_FRAUD_COPY } from '../src/server/coupon-fraud/copy';
import { COUPON_FRAUD_COPY } from '../src/app/coupon-fraud/api/claim/copy';

const insertCoupon = async (page: Page, coupon: string) => {
await page.getByTestId(TEST_IDS.couponFraud.couponCode).fill(coupon);
Expand Down
2 changes: 1 addition & 1 deletion e2e/credential-stuffing.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Page, test } from '@playwright/test';
import { blockGoogleTagManager, resetScenarios } from './e2eTestUtils';
import { TEST_IDS } from '../src/client/testIDs';
import { CREDENTIAL_STUFFING_COPY } from '../src/server/credentialStuffing/copy';
import { CREDENTIAL_STUFFING_COPY } from '../src/app/credential-stuffing/api/authenticate/copy';

const submitForm = async (page: Page) => {
// Waits for the button to be clickable out of the box
Expand Down
2 changes: 1 addition & 1 deletion e2e/e2eTestUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Page, expect } from '@playwright/test';
import { TEST_ATTRIBUTES, TEST_IDS } from '../src/client/testIDs';
import { Severity } from '../src/server/server';
import { Severity } from '../src/server/checks';

/**
*
Expand Down
49 changes: 17 additions & 32 deletions src/pages/api/admin/reset.ts → src/app/api/admin/reset/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { isValidPostRequest } from '../../../server/server';
import {
UserCartItemDbModel,
UserPreferencesDbModel,
UserSearchHistoryDbModel,
} from '../../../server/personalization/database';
import { LoanRequestDbModel } from '../../../app/loan-risk/api/request-loan/database';
import { CouponClaimDbModel } from '../../../server/coupon-fraud/database';
import { Severity, getAndValidateFingerprintResult } from '../../../server/checks';
import { NextApiRequest, NextApiResponse } from 'next';
import { LoginAttemptDbModel } from '../../../server/credentialStuffing/database';
import { ArticleViewDbModel } from '../../../app/paywall/api/database';
import { SmsVerificationDatabaseModel } from '../../../app/sms-pumping/api/database';
import { syncFirewallRuleset } from '../../../app/bot-firewall/api/block-ip/cloudflareApiHelper';
import { deleteBlockedIp } from '../../../app/bot-firewall/api/get-blocked-ips/blockedIpsDatabase';
import { PaymentAttemptDbModel } from '../../../app/payment-fraud/api/place-order/database';
import { UserCartItemDbModel, UserSearchHistoryDbModel } from '../../../personalization/api/database';
import { LoanRequestDbModel } from '../../../loan-risk/api/request-loan/database';
import { CouponClaimDbModel } from '../../../coupon-fraud/api/claim/database';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { LoginAttemptDbModel } from '../../../credential-stuffing/api/authenticate/database';
import { ArticleViewDbModel } from '../../../paywall/api/database';
import { SmsVerificationDatabaseModel } from '../../../sms-pumping/api/database';
import { syncFirewallRuleset } from '../../../bot-firewall/api/block-ip/cloudflareApiHelper';
import { deleteBlockedIp } from '../../../bot-firewall/api/get-blocked-ips/blockedIpsDatabase';
import { PaymentAttemptDbModel } from '../../../payment-fraud/api/place-order/database';
import { NextRequest, NextResponse } from 'next/server';

export type ResetResponse = {
message: string;
Expand All @@ -25,15 +20,8 @@ export type ResetRequest = {
requestId: string;
};

export default async function handler(req: NextApiRequest, res: NextApiResponse<ResetResponse>) {
// This API route accepts only POST requests.
const reqValidation = isValidPostRequest(req);
if (!reqValidation.okay) {
res.status(405).send({ severity: 'error', message: reqValidation.error });
return;
}

const { requestId } = req.body as ResetRequest;
export async function POST(req: NextRequest): Promise<NextResponse<ResetResponse>> {
const { requestId } = (await req.json()) as ResetRequest;

// Get the full Identification result from Fingerprint Server API and validate its authenticity
const fingerprintResult = await getAndValidateFingerprintResult({
Expand All @@ -42,19 +30,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
options: { minConfidenceScore: 0.3 },
});
if (!fingerprintResult.okay) {
res.status(403).send({ severity: 'error', message: fingerprintResult.error });
return;
return NextResponse.json({ severity: 'error', message: fingerprintResult.error }, { status: 403 });
}

const { visitorId, ip } = fingerprintResult.data.products?.identification?.data ?? {};
if (!visitorId) {
res.status(403).send({ severity: 'error', message: 'Visitor ID not found.' });
return;
return NextResponse.json({ severity: 'error', message: 'Visitor ID not found.' }, { status: 403 });
}

const deleteResult = await deleteVisitorData(visitorId, ip ?? '');

res.status(200).json({
return NextResponse.json({
message: 'Visitor data deleted successfully.',
severity: 'success',
result: deleteResult,
Expand All @@ -72,9 +58,8 @@ const deleteVisitorData = async (visitorId: string, ip: string) => {
deletedCouponsClaims: await tryToDestroy(() => CouponClaimDbModel.destroy(options)),
deletedPersonalizationRecords: await tryToDestroy(async () => {
const deletedCartItemsCount = await UserCartItemDbModel.destroy(options);
const deletedUserPreferencesCount = await UserPreferencesDbModel.destroy(options);
const deletedUserSearchHistoryCount = await UserSearchHistoryDbModel.destroy(options);
return deletedCartItemsCount + deletedUserPreferencesCount + deletedUserSearchHistoryCount;
return deletedCartItemsCount + deletedUserSearchHistoryCount;
}),
deletedLoanRequests: await tryToDestroy(() => LoanRequestDbModel.destroy(options)),
deletedArticleViews: await tryToDestroy(() => ArticleViewDbModel.destroy(options)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Attributes, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { sequelize } from '../../../../server/server';
import { sequelize } from '../../../../server/sequelize';
import { MAX_BLOCKED_IPS } from '../block-ip/buildFirewallRules';

interface BlockedIpAttributes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Attributes, DataTypes, FindOptions, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { sequelize } from '../../../../server/server';
import { sequelize } from '../../../../server/sequelize';
import { EventResponseBotData } from '../../../../shared/types';

interface BotVisitAttributes
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sequelize } from '../server';
import { sequelize } from '../../../../server/sequelize';
import { Attributes, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';

export const COUPON_CODES = ['Promo3000', 'BlackFriday'] as const;
Expand Down
4 changes: 2 additions & 2 deletions src/app/coupon-fraud/api/claim/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Op } from 'sequelize';
import { COUPON_CODES, CouponClaimDbModel, CouponCodeString } from '../../../../server/coupon-fraud/database';
import { COUPON_CODES, CouponClaimDbModel, CouponCodeString } from './database';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { COUPON_FRAUD_COPY } from '../../../../server/coupon-fraud/copy';
import { COUPON_FRAUD_COPY } from './copy';
import { NextResponse } from 'next/server';

export type CouponClaimPayload = {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Attributes, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { sequelize } from '../server';
import { sequelize } from '../../../../server/sequelize';

export type LoginAttemptResult =
| 'RequestIdValidationFailed'
Expand Down
6 changes: 3 additions & 3 deletions src/app/credential-stuffing/api/authenticate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Op } from 'sequelize';
import { NextResponse } from 'next/server';
import { env } from 'process';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { CREDENTIAL_STUFFING_COPY } from '../../../../server/credentialStuffing/copy';
import { LoginAttemptResult, LoginAttemptDbModel } from '../../../../server/credentialStuffing/database';
import { sequelize } from '../../../../server/server';
import { CREDENTIAL_STUFFING_COPY } from './copy';
import { LoginAttemptResult, LoginAttemptDbModel } from './database';
import { sequelize } from '../../../../server/sequelize';

export type LoginPayload = {
username: string;
Expand Down
20 changes: 15 additions & 5 deletions src/app/loan-risk/LoanRisk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import { FunctionComponent, useMemo, useState } from 'react';
import {
loanDurationValidation,
loanValueValidation,
monthlyIncomeValidation,
} from '../../client/loan-risk/validation';
import { calculateMonthInstallment } from '../../shared/loan-risk/calculate-month-installment';
import React from 'react';
import { USE_CASES } from '../../client/components/common/content';
Expand Down Expand Up @@ -66,6 +61,21 @@ const SliderField: FunctionComponent<SliderFieldProps> = ({
);
};

const loanValueValidation = {
min: 1000,
max: 1_0_000,
};

const monthlyIncomeValidation = {
min: 500,
max: 3_0_000,
};

const loanDurationValidation = {
min: 2,
max: 48,
};

export function LoanRisk() {
const { getData: getVisitorData, isLoading: isVisitorDataLoading } = useVisitorData(
{ ignoreCache: true },
Expand Down
2 changes: 1 addition & 1 deletion src/app/loan-risk/api/request-loan/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Model, InferAttributes, InferCreationAttributes, DataTypes, Attributes } from 'sequelize';
import { sequelize } from '../../../../server/server';
import { sequelize } from '../../../../server/sequelize';

interface LoanRequestAttributes
extends Model<InferAttributes<LoanRequestAttributes>, InferCreationAttributes<LoanRequestAttributes>> {
Expand Down
2 changes: 1 addition & 1 deletion src/app/payment-fraud/api/place-order/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Model, InferAttributes, InferCreationAttributes, DataTypes, Attributes } from 'sequelize';
import { sequelize } from '../../../../server/server';
import { sequelize } from '../../../../server/sequelize';

interface PaymentAttemptAttributes
extends Model<InferAttributes<PaymentAttemptAttributes>, InferCreationAttributes<PaymentAttemptAttributes>> {
Expand Down
2 changes: 1 addition & 1 deletion src/app/payment-fraud/api/place-order/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getAndValidateFingerprintResult, Severity } from '../../../../server/ch
import { PaymentAttemptData, PaymentAttemptDbModel } from './database';
import { PAYMENT_FRAUD_COPY } from './copy';
import { Op } from 'sequelize';
import { sequelize } from '../../../../server/server';
import { sequelize } from '../../../../server/sequelize';

type Card = {
number: string;
Expand Down
2 changes: 1 addition & 1 deletion src/app/paywall/api/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Attributes, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { sequelize } from '../../../server/server';
import { sequelize } from '../../../server/sequelize';

interface ArticleViewAttributes
extends Model<InferAttributes<ArticleViewAttributes>, InferCreationAttributes<ArticleViewAttributes>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
'use client';

import { useEffect, useState } from 'react';
import { useDebounce, useSessionStorage } from 'react-use';
import { useSearchHistory } from '../../client/api/personalization/use-search-history';
import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
import { useProducts } from '../../client/api/personalization/use-products';
import { usePersonalizationNotification } from '../../client/hooks/personalization/use-personalization-notification';
import { useSnackbar } from 'notistack';
import { useUserPreferences } from '../../client/api/personalization/use-user-preferences';
import { useCart } from '../../client/api/personalization/use-cart';
import { useCart } from './hooks/use-cart';
import React from 'react';
import { USE_CASES } from '../../client/components/common/content';
import { CustomPageProps } from '../_app';
import styles from './personalization.module.scss';
import Image from 'next/image';
import Button from '../../client/components/common/Button/Button';
import CartIcon from '../../client/img/cart.svg';
import { Cart, CartProduct } from '../../client/components/common/Cart/Cart';
import { Search, SearchHistory } from '../../client/components/personalization/searchComponents';
import { ProductCard } from '../../client/components/personalization/productCard';
import { Search, SearchHistory } from './components/searchComponents';
import { ProductCard } from './components/productCard';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { Spinner } from '../../client/components/common/Spinner/Spinner';
import { useSearchHistory } from './hooks/use-search-history';
import { useProducts } from './hooks/use-products';
import { usePersonalizationNotification } from './hooks/use-personalization-notification';

export default function Index({ embed }: CustomPageProps) {
export function Personalization() {
const { enqueueSnackbar } = useSnackbar();

const { isLoading: isFpDataLoading, data } = useVisitorData({ extendedResult: true });
Expand All @@ -34,7 +34,6 @@ export default function Index({ embed }: CustomPageProps) {
const searchHistoryQuery = useSearchHistory();
const { addCartItemMutation, removeCartItemMutation, cartQuery } = useCart();
const productsQuery = useProducts(searchQuery);
const { hasDarkMode } = useUserPreferences();

const isLoading = productsQuery.isLoading || isFpDataLoading;

Expand All @@ -59,7 +58,7 @@ export default function Index({ embed }: CustomPageProps) {
data?.incognito &&
data?.visitorFound &&
!userWelcomed &&
(searchHistoryQuery.data?.data?.length || hasDarkMode || cartQuery.data?.data?.length)
(searchHistoryQuery.data?.data?.length || cartQuery.data?.data?.length)
) {
enqueueSnackbar('Welcome back! We synced your cart and search terms.', {
variant: 'info',
Expand All @@ -68,9 +67,9 @@ export default function Index({ embed }: CustomPageProps) {

setUserWelcomed(true);
}
}, [cartQuery.data, data, enqueueSnackbar, hasDarkMode, searchHistoryQuery.data, userWelcomed]);
}, [cartQuery.data, data, enqueueSnackbar, searchHistoryQuery.data, userWelcomed]);

const cartItems: CartProduct[] | undefined = cartQuery.data?.data.map((item) => {
const cartItems: CartProduct[] | undefined = cartQuery.data?.data?.map((item) => {
return {
id: item.id,
name: item.product.name,
Expand Down Expand Up @@ -103,13 +102,13 @@ export default function Index({ embed }: CustomPageProps) {
<Button onClick={() => setDidAcknowledge(true)}>Okay, I understand</Button>
</DialogActions>
</Dialog>
<UseCaseWrapper useCase={USE_CASES.personalization} embed={embed}>
<UseCaseWrapper useCase={USE_CASES.personalization}>
<div className={styles.twoColumnContainer}>
<div className={styles.leftColumn}>
<div className={styles.search}>
<Search search={search} setSearch={setSearch} />
<SearchHistory
searchHistory={searchHistoryQuery.data?.data.map((searchTerm) => searchTerm.query)}
searchHistory={searchHistoryQuery.data?.data?.map((searchTerm) => searchTerm.query)}
setSearchHistory={(searchTerm) => setSearch(searchTerm)}
/>
</div>
Expand Down
75 changes: 75 additions & 0 deletions src/app/personalization/api/cart/add-item/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ProductDbModel, UserCartItemAttributes, UserCartItemDbModel } from '../../database';
import { Op } from 'sequelize';
import { NextRequest, NextResponse } from 'next/server';
import { getAndValidateFingerprintResult, Severity } from '../../../../../server/checks';

export type AddCartItemPayload = {
requestId: string;
productId: number;
};

export type AddCartItemResponse = {
severity: Severity;
message: string;
data?: UserCartItemAttributes;
};

export async function POST(req: NextRequest): Promise<NextResponse<AddCartItemResponse>> {
const { requestId, productId } = (await req.json()) as AddCartItemPayload;

// Get the full Identification result from Fingerprint Server API and validate its authenticity
const fingerprintResult = await getAndValidateFingerprintResult({
requestId,
req,
options: { minConfidenceScore: 0.3, disableFreshnessCheck: true },
});
if (!fingerprintResult.okay) {
return NextResponse.json({ severity: 'error', message: fingerprintResult.error }, { status: 403 });
}

// Get visitorId from the Server API Identification event
const visitorId = fingerprintResult.data.products?.identification?.data?.visitorId;
if (!visitorId) {
return NextResponse.json({ severity: 'error', message: 'Visitor ID not found.' }, { status: 403 });
}

const product = await ProductDbModel.findOne({
where: {
id: {
[Op.eq]: productId,
},
},
});

if (!product) {
return NextResponse.json({ severity: 'error', message: 'Product not found' }, { status: 500 });
}

const [cartItem, created] = await UserCartItemDbModel.findOrCreate({
where: {
visitorId: {
[Op.eq]: visitorId ?? '',
},
productId: {
[Op.eq]: productId,
},
},
defaults: {
visitorId: visitorId ?? '',
count: 1,
timestamp: new Date(),
productId,
},
});

if (!created) {
cartItem.count++;
await cartItem.save();
}

return NextResponse.json({
severity: 'success',
message: 'Item added',
data: cartItem,
});
}
Loading

0 comments on commit 9dd668e

Please sign in to comment.