Skip to content

Commit

Permalink
fix: [PE-796] CGN hide percentage badge if value not in range 1-100 (
Browse files Browse the repository at this point in the history
…#6454)

## Short description
This pull is removing the discount badge from cgn opportunities when
this value is not in range `1-100`

## List of changes proposed in this pull request
- Replaced the inline discount validation and normalization logic with
the `isValidDiscount` and `normalizedDiscountPercentage` utility
functions
- Added `normalizedDiscountPercentage` and `isValidDiscount` utility
functions to handle discount validation and normalization
- Added unit tests for utils functions

## How to test
- From dev-server mock some discount value equals 0 or higher than 100
- Ensure that badges for this values are not rendering

---------

Co-authored-by: Cristiano Tofani <[email protected]>
  • Loading branch information
LeleDallas and CrisTofani authored Nov 26, 2024
1 parent f9625e9 commit b151329
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 47 deletions.
16 changes: 4 additions & 12 deletions ts/features/bonus/cgn/components/merchants/CgnDiscountValueBox.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { WithinRangeInteger } from "@pagopa/ts-commons/lib/numbers";

import * as E from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
import * as React from "react";
import { View, StyleSheet } from "react-native";
import { H6, IOColors } from "@pagopa/io-app-design-system";
import * as React from "react";
import { StyleSheet, View } from "react-native";
import { normalizedDiscountPercentage } from "./utils";

type ValueBoxProps = {
value: number;
Expand Down Expand Up @@ -37,17 +34,12 @@ const PERCENTAGE_SYMBOL = "%";
const MINUS_SYMBOL = "-";

const CgnDiscountValueBox = ({ value, small }: ValueBoxProps) => {
const normalizedValue = pipe(
WithinRangeInteger(0, 100).decode(value),
E.map(v => v.toString()),
E.getOrElse(() => "-")
);
const percentage = <H6 color={"white"}>{PERCENTAGE_SYMBOL}</H6>;
return (
<View style={small ? styles.smallValueBox : styles.discountValueBox}>
<H6 color={"white"} style={styles.percentage}>
{MINUS_SYMBOL}
{normalizedValue}
{normalizedDiscountPercentage(value)}
{percentage}
</H6>
</View>
Expand Down
29 changes: 13 additions & 16 deletions ts/features/bonus/cgn/components/merchants/CgnModuleDiscount.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import {
Badge,
H6,
Expand All @@ -14,22 +12,22 @@ import {
VSpacer,
useIOExperimentalDesign
} from "@pagopa/io-app-design-system";
import * as O from "fp-ts/lib/Option";
import * as React from "react";
import { View, StyleSheet, Pressable } from "react-native";
import { pipe } from "fp-ts/lib/function";
import { WithinRangeInteger } from "@pagopa/ts-commons/lib/numbers";
import { Pressable, StyleSheet, View } from "react-native";
import Animated, {
Extrapolate,
Extrapolation,
interpolate,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring
} from "react-native-reanimated";
import { Discount } from "../../../../../../definitions/cgn/merchants/Discount";
import I18n from "../../../../../i18n";
import { ProductCategory } from "../../../../../../definitions/cgn/merchants/ProductCategory";
import I18n from "../../../../../i18n";
import { getCategorySpecs } from "../../utils/filters";
import { isValidDiscount, normalizedDiscountPercentage } from "./utils";

type Props = {
onPress: () => void;
Expand Down Expand Up @@ -89,7 +87,7 @@ export const CgnModuleDiscount = ({ onPress, discount }: Props) => {
scaleTraversed.value,
[0, 1],
[1, animationScaleValue],
Extrapolate.CLAMP
Extrapolation.CLAMP
);

return {
Expand All @@ -105,11 +103,6 @@ export const CgnModuleDiscount = ({ onPress, discount }: Props) => {
// eslint-disable-next-line functional/immutable-data
isPressed.value = 0;
}, [isPressed]);
const normalizedValue = pipe(
WithinRangeInteger(0, 100).decode(discount.discount),
E.map(v => v.toString()),
E.getOrElse(() => "-")
);

return (
<Pressable
Expand Down Expand Up @@ -146,9 +139,13 @@ export const CgnModuleDiscount = ({ onPress, discount }: Props) => {
<HSpacer size={8} />
</>
)}
{discount.discount ? (
<Badge variant="purple" outline text={`-${normalizedValue}%`} />
) : null}
{isValidDiscount(discount.discount) && (
<Badge
variant="purple"
outline
text={`-${normalizedDiscountPercentage(discount.discount)}%`}
/>
)}
</View>
<VSpacer size={8} />
<H6>{discount.name}</H6>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { fireEvent, render } from "@testing-library/react-native";
import React from "react";
import { Discount } from "../../../../../../../definitions/cgn/merchants/Discount";
import { ProductCategoryEnum } from "../../../../../../../definitions/cgn/merchants/ProductCategory";
import I18n from "../../../../../../i18n";
import { CgnModuleDiscount } from "../CgnModuleDiscount";

describe("CgnModuleDiscount", () => {
const discount: Discount = {
name: "Small Rubber Chips" as NonEmptyString,
id: "28201" as NonEmptyString,
description: undefined,
discount: 25,
discountUrl: "https://localhost",
endDate: new Date(),
isNew: true,
productCategories: [ProductCategoryEnum.cultureAndEntertainment],
startDate: new Date()
};

const onPressMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it("should render correctly", () => {
const { getByText } = render(
<CgnModuleDiscount onPress={onPressMock} discount={discount} />
);

expect(getByText(I18n.t("bonus.cgn.merchantsList.news"))).toBeTruthy();
expect(getByText("-25%")).toBeTruthy();
expect(getByText("Small Rubber Chips")).toBeTruthy();
});

it("should call onPress when pressed", () => {
const { getByRole } = render(
<CgnModuleDiscount onPress={onPressMock} discount={discount} />
);
fireEvent.press(getByRole("button"));
expect(onPressMock).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import * as E from "fp-ts/lib/Either";
import React from "react";
import { StyleSheet, View } from "react-native";
import {
Badge,
H3,
Expand All @@ -9,11 +6,12 @@ import {
VSpacer
} from "@pagopa/io-app-design-system";
import { useHeaderHeight } from "@react-navigation/elements";
import { pipe } from "fp-ts/lib/function";
import { WithinRangeInteger } from "@pagopa/ts-commons/lib/numbers";
import React from "react";
import { StyleSheet, View } from "react-native";
import { Discount } from "../../../../../../../definitions/cgn/merchants/Discount";
import { CategoryTag } from "../CgnModuleDiscount";
import I18n from "../../../../../../i18n";
import { CategoryTag } from "../CgnModuleDiscount";
import { isValidDiscount, normalizedDiscountPercentage } from "../utils";

type CgnDiscountHeaderProps = {
onLayout: (event: any) => void;
Expand All @@ -28,14 +26,10 @@ export const CgnDiscountHeader = ({
? styles.backgroundNewItem
: styles.backgroundDefault;

const normalizedDiscountValue = pipe(
WithinRangeInteger(0, 100).decode(discountDetails.discount),
E.map(v => v.toString()),
E.getOrElse(() => "-")
);

const headerHeight = useHeaderHeight();

const { isNew, discount, name, productCategories } = discountDetails;

return (
<View
onLayout={onLayout}
Expand All @@ -49,30 +43,30 @@ export const CgnDiscountHeader = ({
]}
>
<View>
{(discountDetails.isNew || discountDetails.discount) && (
{(isNew || isValidDiscount(discount)) && (
<>
<View style={[IOStyles.row, { gap: 8 }]}>
{discountDetails.isNew && (
{isNew && (
<Badge
variant="purple"
text={I18n.t("bonus.cgn.merchantsList.news")}
/>
)}
{discountDetails.discount ? (
{isValidDiscount(discount) && (
<Badge
variant="purple"
outline
text={`-${normalizedDiscountValue}%`}
text={`-${normalizedDiscountPercentage(discount)}%`}
/>
) : null}
)}
</View>
<VSpacer size={12} />
</>
)}
<H3>{discountDetails.name}</H3>
<H3>{name}</H3>
<VSpacer size={12} />
<View style={[{ flexWrap: "wrap" }, IOStyles.row]}>
{discountDetails.productCategories.map(categoryKey => (
{productCategories.map(categoryKey => (
<CategoryTag key={categoryKey} category={categoryKey} />
))}
</View>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { render } from "@testing-library/react-native";
import React from "react";
import { Discount } from "../../../../../../../../definitions/cgn/merchants/Discount";
import { ProductCategoryEnum } from "../../../../../../../../definitions/cgn/merchants/ProductCategory";
import I18n from "../../../../../../../i18n";
import { CgnDiscountHeader } from "../CgnDiscountHeader";

const mockDiscount: Discount = {
name: "Small Rubber Chips" as NonEmptyString,
id: "28201" as NonEmptyString,
description: undefined,
discount: 25,
discountUrl: "https://localhost",
endDate: new Date(),
isNew: true,
productCategories: [ProductCategoryEnum.cultureAndEntertainment],
startDate: new Date()
};

jest.mock("@react-navigation/elements", () => ({
useHeaderHeight: jest.fn().mockImplementation(() => 200)
}));

describe("CgnDiscountHeader", () => {
it("should render correctly with new discount", () => {
const { getByText } = render(
<CgnDiscountHeader onLayout={jest.fn()} discountDetails={mockDiscount} />
);

expect(getByText(I18n.t("bonus.cgn.merchantsList.news"))).toBeTruthy();
expect(getByText("-25%")).toBeTruthy();
});

it("should render correctly without new discount", () => {
const discountDetails = { ...mockDiscount, isNew: false };
const { queryByText } = render(
<CgnDiscountHeader
onLayout={jest.fn()}
discountDetails={discountDetails}
/>
);

expect(queryByText(I18n.t("bonus.cgn.merchantsList.news"))).toBeNull();
expect(queryByText("-25%")).toBeTruthy();
});

it("should render correctly without discount", () => {
const discountDetails = { ...mockDiscount, discount: 0 };
const { queryByText } = render(
<CgnDiscountHeader
onLayout={jest.fn()}
discountDetails={discountDetails}
/>
);

expect(queryByText(I18n.t("bonus.cgn.merchantsList.news"))).toBeTruthy();
expect(queryByText("-0%")).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { normalizedDiscountPercentage, isValidDiscount } from "../index";

describe("normalizedDiscountPercentage", () => {
it("should return the discount as a string if within range", () => {
expect(normalizedDiscountPercentage(50)).toBe("50");
});

it("should return '-' if the discount is out of range", () => {
expect(normalizedDiscountPercentage(150)).toBe("-");
expect(isValidDiscount(0)).toBe(false);
});

it("should return '-' if the discount is undefined", () => {
expect(normalizedDiscountPercentage()).toBe("-");
});
});

describe("isValidDiscount", () => {
it("should return true if the discount is within range", () => {
expect(isValidDiscount(50)).toBe(true);
});

it("should return false if the discount is out of range", () => {
expect(isValidDiscount(150)).toBe(false);
expect(isValidDiscount(0)).toBe(false);
});

it("should return false if the discount is undefined", () => {
expect(isValidDiscount()).toBe(false);
});
});
13 changes: 13 additions & 0 deletions ts/features/bonus/cgn/components/merchants/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { WithinRangeInteger } from "@pagopa/ts-commons/lib/numbers";
import * as E from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";

export const normalizedDiscountPercentage = (discount?: number) =>
pipe(
WithinRangeInteger(1, 100).decode(discount),
E.map(v => v.toString()),
E.getOrElse(() => "-")
);

export const isValidDiscount = (discount?: number) =>
pipe(WithinRangeInteger(1, 100).decode(discount), E.isRight);

0 comments on commit b151329

Please sign in to comment.