Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#173834010] Show warning if there's another bonus already active #2079

Merged
merged 14 commits into from
Jul 21, 2020
Merged
6 changes: 5 additions & 1 deletion locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@ bonus:
validBetween: You will be able to use the Bonus Vacanze starting from {{from}} until {{to}}
bonusRejected: Attention! This Bonus Vacanze has been rejected
redeemed: This Bonus Vacanze has been consumed on {{date}}
multipleBonus: Two bonuses requested and activated by you. Make sure you use the correct DSU to prevent the bonus user from incurring subsequent recovery actions.
usableAmount: Expendable at the accommodation facility
taxBenefit: Deductible in tax return
request: Request the
Expand Down Expand Up @@ -1483,7 +1484,7 @@ bonus:
title: Cancel
body: Do you really want to quit? To request the "Bonus Vacanze" you will have to repeat the process from the beginning.
cancel: Continue
confirm: Abort request
confirm: Abort the request
completed: Request aborted
composition:
amount: "Maximum amount"
Expand All @@ -1501,6 +1502,9 @@ bonus:
contextualHelp:
title: The Bonus Vacanze
body: Here you can see all the information related to the Bonus Vacanze, the measure established by the Decreto Rilancio to encourage tourism after the lockdown due to the Coronavirus emergency.
requestAlert:
title: Attention
content: "You have already obtained a bonus, continue the request only if your situation has changed and you have submitted a new DSU to update ISEE.\n Please note that the use of bonuses generated on the basis of incorrect DSU, determines actions to recover the benefit to the user.\n Are you sure you want to continue?"
request: Request a new bonus
requestLabel: Bonuses and discounts
requestTitle: Request
Expand Down
6 changes: 5 additions & 1 deletion locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,7 @@ bonus:
validBetween: Potrai usare questo Bonus Vacanze dal {{from}} fino al {{to}}
bonusRejected: Attenzione! Questo Bonus Vacanze è stato annullato
redeemed: Questo Bonus Vacanze è stato utilizzato il {{date}}
multipleBonus: Risultano due bonus richiesti e attivati da te. Assicurati di usare quello relativo alla DSU corretta, per evitare che chi utilizza il bonus incorra in successive azioni di recupero.
usableAmount: Spendibile presso la struttura ricettiva
taxBenefit: Detraibile in dichiarazione dei redditi
request: Richiedi il
Expand Down Expand Up @@ -1515,7 +1516,7 @@ bonus:
title: Annulla
body: Vuoi davvero uscire dal processo? Per richiedere il bonus vacanze dovrai ripetere il flusso dall’inizio.
cancel: Continua
confirm: Annulla richiesta
confirm: Annulla la richiesta
completed: Richiesta annullata
composition:
amount: "Importo massimo"
Expand All @@ -1533,6 +1534,9 @@ bonus:
contextualHelp:
title: Il bonus vacanze
body: Qui sono visualizzate tutte le informazioni relative al bonus vacanze, il provvedimento istituito dal Decreto Rilancio per incentivare il turismo dopo il lockdown dovuto all'emergenza Coronavirus.
requestAlert:
title: Attenzione
content: "Hai già ottenuto un bonus, continua la richiesta solo se la tua situazione si è modificata e hai presentato una nuova DSU per l’aggiornamento dell’ISEE.\n Ti informiamo che l’utilizzo di bonus generati in base a DSU non corrette, determina azioni di recupero dell’agevolazione verso l’utilizzatore.\n Sei sicuro di voler continuare?"
request: Richiedi un nuovo bonus
requestLabel: Bonus e sconti
requestTitle: Richiesta
Expand Down
6 changes: 5 additions & 1 deletion ts/components/wallet/card/SectionCardComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ const SectionCardComponent: React.FunctionComponent<Props> = (props: Props) => {
</Text>
{isNew && (
<Badge style={styles.badgeColor}>
<Text semibold={true} style={styles.badgeText}>
<Text
semibold={true}
style={styles.badgeText}
dark={true}
>
{I18n.t("wallet.methods.newCome")}
</Text>
</Badge>
Expand Down
18 changes: 17 additions & 1 deletion ts/features/bonusVacanze/screens/ActiveBonusScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import {
cancelLoadBonusFromIdPolling,
startLoadBonusFromIdPolling
} from "../store/actions/bonusVacanze";
import { bonusActiveDetailByIdSelector } from "../store/reducers/allActive";
import {
bonusActiveDetailByIdSelector,
ownedActiveBonus
} from "../store/reducers/allActive";
import {
availableBonusTypesSelectorFromId,
bonusVacanzeLogo
Expand All @@ -52,6 +55,7 @@ import {
isBonusActive,
validityInterval
} from "../utils/bonus";
import { ActivateBonusDiscrepancies } from "./activation/request/ActivateBonusDiscrepancies";

type QRCodeContents = {
[key: string]: string;
Expand Down Expand Up @@ -378,6 +382,16 @@ const ActiveBonusScreen: React.FunctionComponent<Props> = (props: Props) => {
<View spacer={true} extralarge={true} />
{switchInformationText()}
<View spacer={true} />
</View>
{props.hasMoreOwnedActiveBonus && (
<ActivateBonusDiscrepancies
text={I18n.t("bonus.bonusVacanze.multipleBonus")}
attention={I18n.t(
"bonus.bonusVacanze.eligibility.activateBonus.discrepancies.attention"
)}
/>
)}
<View style={[styles.paddedContentLeft, styles.paddedContentRight]}>
<ItemSeparatorComponent noPadded={true} />
<View spacer={true} />
<BonusCompositionDetails
Expand Down Expand Up @@ -456,7 +470,9 @@ const ActiveBonusScreen: React.FunctionComponent<Props> = (props: Props) => {
const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
const bonusFromNav = ownProps.navigation.getParam("bonus");
const bonus = bonusActiveDetailByIdSelector(bonusFromNav.id)(state);

return {
hasMoreOwnedActiveBonus: ownedActiveBonus(state).length > 1,
bonusInfo: availableBonusTypesSelectorFromId(ID_BONUS_VACANZE_TYPE)(state),
bonus,
isError: pot.isNone(bonus) && pot.isError(bonus), // error and no bonus data, user should retry to load
Expand Down
24 changes: 22 additions & 2 deletions ts/features/bonusVacanze/screens/BonusInformationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ import { LightModalContextInterface } from "../../../components/ui/LightModal";
import Markdown from "../../../components/ui/Markdown";
import I18n from "../../../i18n";
import { navigateBack } from "../../../store/actions/navigation";
import { GlobalState } from "../../../store/reducers/types";
import customVariables from "../../../theme/variables";
import { getLocalePrimaryWithFallback } from "../../../utils/locale";
import { maybeNotNullyString } from "../../../utils/strings";
import { actionWithAlert } from "../components/alert/ActionWithAlert";
import { bonusVacanzeStyle } from "../components/Styles";
import TosBonusComponent from "../components/TosBonusComponent";
import { checkBonusVacanzeEligibility } from "../store/actions/bonusVacanze";
import { ownedActiveBonus } from "../store/reducers/allActive";

type NavigationParams = Readonly<{
bonusItem: BonusAvailable;
Expand All @@ -35,6 +38,7 @@ type OwnProps = NavigationInjectedProps<NavigationParams>;

type Props = OwnProps &
LightModalContextInterface &
ReturnType<typeof mapStateToProps> &
ReturnType<typeof mapDispatchToProps>;

const CSS_STYLE = `
Expand Down Expand Up @@ -109,6 +113,18 @@ const BonusInformationScreen: React.FunctionComponent<Props> = props => {
const bonusTypeLocalizedContent: BonusAvailableContent =
bonusType[getLocalePrimaryWithFallback()];

// if the current profile owns other active bonus, show an alert informing about that
const handleBonusRequestOnPress = () =>
props.hasOwnedActiveBonus
? actionWithAlert({
title: I18n.t("bonus.bonusInformation.requestAlert.title"),
body: I18n.t("bonus.bonusInformation.requestAlert.content"),
confirmText: I18n.t("bonus.bonusVacanze.abort.cancel"),
cancelText: I18n.t("bonus.bonusVacanze.abort.confirm"),
onConfirmAction: props.requestBonusActivation
})
: props.requestBonusActivation();

const cancelButtonProps = {
block: true,
light: true,
Expand All @@ -119,7 +135,7 @@ const BonusInformationScreen: React.FunctionComponent<Props> = props => {
const requestButtonProps = {
block: true,
primary: true,
onPress: props.requestBonusActivation,
onPress: handleBonusRequestOnPress,
title: `${I18n.t("bonus.bonusVacanze.cta.requestBonus")} ${
bonusTypeLocalizedContent.name
}`
Expand Down Expand Up @@ -232,6 +248,10 @@ const BonusInformationScreen: React.FunctionComponent<Props> = props => {
);
};

const mapStateToProps = (state: GlobalState) => ({
hasOwnedActiveBonus: ownedActiveBonus(state).length > 0
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
requestBonusActivation: () => {
dispatch(checkBonusVacanzeEligibility.request());
Expand All @@ -241,7 +261,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({

export default withLightModalContext(
connect(
undefined,
mapStateToProps,
mapDispatchToProps
)(BonusInformationScreen)
);
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ const styles = StyleSheet.create({
},
discrepanciesBox: {
backgroundColor: themeVariables.brandHighlight
},
discrepancies: {
color: themeVariables.colorWhite
}
});

Expand All @@ -42,17 +39,10 @@ export const ActivateBonusDiscrepancies: React.FunctionComponent<
bonusVacanzeStyle.horizontalPadding
]}
>
<IconFont
name={"io-notice"}
size={iconSize}
color={themeVariables.colorWhite}
/>
<IconFont name={"io-notice"} size={iconSize} />
<View hspacer={true} />
<Text style={[styles.discrepancies, activateBonusStyle.boxText]}>
<Text
bold={true}
style={[styles.discrepancies, activateBonusStyle.boxText]}
>
<Text style={activateBonusStyle.boxText} dark={true}>
<Text bold={true} style={activateBonusStyle.boxText} dark={true}>
{`${props.attention} `}
</Text>
{props.text}
Expand Down
103 changes: 103 additions & 0 deletions ts/features/bonusVacanze/store/reducers/__tests__/allActive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as pot from "italia-ts-commons/lib/pot";
import {
EmailString,
FiscalCode,
NonEmptyString
} from "italia-ts-commons/lib/strings";
import { InitializedProfile } from "../../../../../../definitions/backend/InitializedProfile";
import { Version } from "../../../../../../definitions/backend/Version";
import { BonusActivationStatusEnum } from "../../../../../../definitions/bonus_vacanze/BonusActivationStatus";
import { BonusCode } from "../../../../../../definitions/bonus_vacanze/BonusCode";
import { mockedBonus } from "../../../mock/mockData";
import { ownedActiveBonus } from "../allActive";

const fiscalCode = "ABCDEF83A12L719R" as FiscalCode;
const profile: InitializedProfile = {
has_profile: true,
is_inbox_enabled: true,
is_webhook_enabled: true,
is_email_enabled: true,
is_email_validated: true,
email: "[email protected]" as EmailString,
spid_email: "[email protected]" as EmailString,
family_name: "Connor",
name: "John",
fiscal_code: fiscalCode,
spid_mobile_phone: "123" as NonEmptyString,
version: 1 as Version
};

const potProfile = pot.some(profile);
const bonusDifferentApplicant = pot.some({
...mockedBonus,
status: BonusActivationStatusEnum.ACTIVE,
applicant_fiscal_code: "XXXTT83A12L7XXX" as FiscalCode
});
const bonusActive = pot.some({
...mockedBonus,
status: BonusActivationStatusEnum.ACTIVE,
applicant_fiscal_code: fiscalCode
});

describe("ownedActiveBonus", () => {
it("should return an empty array", () => {
expect(ownedActiveBonus.resultFunc([], potProfile)).toStrictEqual([]);
});

it("should return an empty array", () => {
expect(
ownedActiveBonus.resultFunc(
[pot.none, pot.none, pot.noneError(new Error("some error"))],
potProfile
)
).toStrictEqual([]);
});

it("should return an empty array (different applicant)", () => {
expect(
ownedActiveBonus.resultFunc([bonusDifferentApplicant], potProfile)
).toStrictEqual([]);
});

it("should return an empty array (status !== ACTIVE)", () => {
const bonusRedeemed = pot.some({
...mockedBonus,
status: BonusActivationStatusEnum.REDEEMED,
applicant_fiscal_code: fiscalCode
});
expect(
ownedActiveBonus.resultFunc([bonusRedeemed], potProfile)
).toStrictEqual([]);
});

it("should return the active bonus", () => {
expect(
ownedActiveBonus.resultFunc(
[bonusDifferentApplicant, bonusActive],
potProfile
)
).toStrictEqual([bonusActive.value]);
});

it("should return the active bonus", () => {
expect(
ownedActiveBonus.resultFunc(
[bonusDifferentApplicant, bonusActive, pot.none],
potProfile
)
).toStrictEqual([bonusActive.value]);
});

it("should return the active bonus(2)", () => {
const anotherBonusActive = pot.some({
...bonusActive.value,
id: "XYZ" as BonusCode
});
expect(
ownedActiveBonus.resultFunc(
[bonusDifferentApplicant, bonusActive, anotherBonusActive, pot.none],
potProfile
)
).toStrictEqual([bonusActive.value, anotherBonusActive.value]);
});
});
32 changes: 32 additions & 0 deletions ts/features/bonusVacanze/store/reducers/allActive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { fromNullable } from "fp-ts/lib/Option";
import * as pot from "italia-ts-commons/lib/pot";
import { createSelector } from "reselect";
import { getType } from "typesafe-actions";
import { BonusActivationStatusEnum } from "../../../../../definitions/bonus_vacanze/BonusActivationStatus";
import { BonusActivationWithQrCode } from "../../../../../definitions/bonus_vacanze/BonusActivationWithQrCode";
import { Action } from "../../../../store/actions/types";
import {
profileSelector,
ProfileState
} from "../../../../store/reducers/profile";
import { GlobalState } from "../../../../store/reducers/types";
import {
activateBonusVacanze,
Expand Down Expand Up @@ -68,6 +73,33 @@ export const allBonusActiveSelector = createSelector<
return Object.keys(allActiveObj).map(k => allActiveObj[k]);
});

// return the list of the active bonus of which the current profile is the applicant
export const ownedActiveBonus = createSelector<
GlobalState,
ReadonlyArray<pot.Pot<BonusActivationWithQrCode, Error>>,
ProfileState,
ReadonlyArray<BonusActivationWithQrCode>
>([allBonusActiveSelector, profileSelector], (allActiveArray, profile) => {
return pot.toOption(profile).fold([], p =>
allActiveArray.reduce(
(
acc: ReadonlyArray<BonusActivationWithQrCode>,
curr: pot.Pot<BonusActivationWithQrCode, Error>
) => {
if (
pot.isSome(curr) &&
curr.value.applicant_fiscal_code === p.fiscal_code &&
curr.value.status === BonusActivationStatusEnum.ACTIVE
) {
return [...acc, curr.value];
}
return acc;
},
[]
)
);
});

// return the bonus from a given ID
export const bonusActiveDetailByIdSelector = (id: string) =>
createSelector<
Expand Down