import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useAppSelector } from '@akinon/next/redux/hooks'; import { useSetMasterpassRestBinNumberMutation, useCheckoutMasterpassInstallmentMutation, usePrepareMasterpassOrderMutation, useFinalizeMasterpassOrderMutation, useQueryMasterpassRewardsMutation, useSelectMasterpassRewardsMutation } from '../redux/api'; import { updatePaymentState, setSelectedCard, setSelectedInstallment, setInstallments, setCardType, setOrderData, setOrderCompleted, setAvailableRewards, setSelectedRewards, setLoadingRewards, setShowRewardModal, clearRewards } from '../redux/reducer'; import { useMasterpassToken } from './useMasterpassToken'; import { PaymentService } from '../services/payment'; import { createPaymentRequest, createDirectPaymentRequest, getTransactionType, setPaymentPending, clearPaymentPending } from '../utils/payment-utils'; import { handleMasterpassResponse } from '../utils/response-handler'; import type { Installment, RewardItem } from '../types/payment.types'; import type { CardModel } from '../types/account.types'; import { getCappedRewardAmounts } from '../utils/reward-utils'; export type PaymentResult = { success: boolean; requiresRedirect?: boolean; requiresOTP?: boolean; otpType?: 'OTP' | 'RTA' | 'CVV'; message?: any; }; export interface UseMasterpassPaymentOptions { enableRewards?: boolean; } export const useMasterpassPayment = ( options: UseMasterpassPaymentOptions = {} ) => { const { enableRewards = false } = options; const dispatch = useDispatch(); const { paymentState, newCardFormData, currency, terminalGroupId, bankIca, tokenData } = useAppSelector((state) => state.masterpassRest); const payableAmount = useAppSelector( (state) => state.checkout?.preOrder?.unpaid_amount ?? state.checkout?.preOrder?.total_amount_with_interest ?? null ); const { refetch: refetchToken } = useMasterpassToken({ useThreeD: paymentState.useThreeD }); const [setMasterpassRestBinNumber, { isLoading: isCheckoutLoading }] = useSetMasterpassRestBinNumberMutation(); const [checkoutMasterpassInstallment, { isLoading: isInstallmentLoading }] = useCheckoutMasterpassInstallmentMutation(); const [prepareMasterpassOrder, { isLoading: isPrepareLoading }] = usePrepareMasterpassOrderMutation(); const [finalizeMasterpassOrder, { isLoading: isFinalizeLoading }] = useFinalizeMasterpassOrderMutation(); const [queryMasterpassRewards, { isLoading: isRewardsQueryLoading }] = useQueryMasterpassRewardsMutation(); const [selectMasterpassRewards, { isLoading: isRewardsSelectLoading }] = useSelectMasterpassRewardsMutation(); const refreshTokenForPayment = useCallback(async () => { const result = await refetchToken({ skipReset: true }); if (result?.data?.token) { const decodedToken = window.Utils.decodeJwt(result.data.token); return { tokenData: decodedToken, token: result.data.token, merchantId: decodedToken?.MerchantId }; } return null; }, [refetchToken]); const updatePaymentStateAction = useCallback( (updates: Partial) => { dispatch(updatePaymentState(updates)); }, [dispatch] ); const handleInstallmentSelect = useCallback( async (installment: Installment) => { dispatch(setSelectedInstallment(installment)); try { await checkoutMasterpassInstallment({ installment: installment.pk }).unwrap(); } catch (error) { // Silently handle errors (e.g., mutation aborted due to rapid selection) } }, [checkoutMasterpassInstallment, dispatch] ); const fetchRewardsForCard = useCallback( async (card: CardModel) => { if (!card?.cardAlias) return; dispatch(setLoadingRewards(true)); try { const result = await queryMasterpassRewards({ card_alias: card.cardAlias }).unwrap(); const rewards = result.context_list?.[0]?.page_context?.rewards || []; dispatch(setAvailableRewards(rewards)); } catch (error) { dispatch(setAvailableRewards([])); } finally { dispatch(setLoadingRewards(false)); } }, [queryMasterpassRewards, dispatch] ); const handleCardSelect = useCallback( async (card: CardModel) => { dispatch(setSelectedCard(card)); dispatch(setInstallments([])); dispatch(setSelectedInstallment(null)); dispatch(setCardType(null)); if (enableRewards) { dispatch(clearRewards()); } try { const result = await setMasterpassRestBinNumber({ is_masterpass_stored_card: true, bin_number: card.cardBin }).unwrap(); if (result.context_list?.[0]?.page_context) { const pageContext = result.context_list[0].page_context; dispatch(setInstallments(pageContext.installments)); dispatch(setCardType(pageContext.card_type)); if (pageContext.installments?.length > 0) { await handleInstallmentSelect(pageContext.installments[0]); } } if (enableRewards) { void fetchRewardsForCard(card); } } catch (error) { // Silently handle errors (e.g., mutation aborted due to rapid selection) } }, [ enableRewards, setMasterpassRestBinNumber, handleInstallmentSelect, fetchRewardsForCard, dispatch ] ); const openRewardModal = useCallback(() => { dispatch(setShowRewardModal(true)); }, [dispatch]); const closeRewardModal = useCallback(() => { dispatch(setShowRewardModal(false)); }, [dispatch]); const confirmRewards = useCallback( async (selected: RewardItem[]) => { const capped = getCappedRewardAmounts(selected, payableAmount); const special = capped.special.toFixed(2); const general = capped.general.toFixed(2); try { await selectMasterpassRewards({ special, general }).unwrap(); dispatch(setSelectedRewards(selected)); dispatch(setShowRewardModal(false)); return { success: true }; } catch (error) { return { success: false, message: error instanceof Error ? error.message : 'Failed to apply rewards' }; } }, [selectMasterpassRewards, payableAmount, dispatch] ); const processPayment = useCallback(async (): Promise => { const { selectedCard, selectedInstallment, cvc, useThreeD } = paymentState; if (!selectedCard || !selectedInstallment) { return { success: false, message: 'Please complete all required steps before payment' }; } try { const freshResult = await refreshTokenForPayment(); const activeTokenData = freshResult?.tokenData || tokenData; const activeToken = freshResult?.token; const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId; if (!activeTokenData?.AccountKey) { return { success: false, message: 'Token data not available' }; } const prepareResult = await prepareMasterpassOrder({ use_three_d: useThreeD }).unwrap(); dispatch(setOrderData(prepareResult)); const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no; // From here on the order number is bound to a Masterpass transaction. // Regenerating it before that transaction settles makes the backend // reject the completion with ORDER_NO_MISMATCH. setPaymentPending(orderNo); const paymentService = new PaymentService(activeToken, activeMerchantId); const paymentRequest = createPaymentRequest({ cvc, cardAlias: selectedCard.cardAlias, accountKey: activeTokenData.AccountKey, amount: prepareResult.pre_order?.total_amount_with_interest, orderNo, currencyCode: currency, installmentCount: selectedInstallment.installment_count, authenticationMethod: activeTokenData.AuthenticationMethod, secure3DModel: activeTokenData.Secure3dType, terminalGroupId, acquirerIcaNumber: bankIca, additionalFields: prepareResult.context_list?.[0]?.page_context?.extras ?.additional_fields || {} }); const paymentResult = await paymentService.processPayment(paymentRequest); const response = await handleMasterpassResponse(paymentResult); if (response.requires3D && response.redirectUrl) { window.location.href = response.redirectUrl; return { success: false, requiresRedirect: true }; } if (response.success) { const transactionType = getTransactionType(useThreeD); const finalizeResult = await finalizeMasterpassOrder({ responseCode: paymentResult.data?.result?.responseCode, token: paymentResult.data?.result?.token, three_d_secure: useThreeD, transactionType }).unwrap(); dispatch(setOrderData(finalizeResult)); clearPaymentPending(); if (finalizeResult.errors) { return { success: false, message: finalizeResult.errors }; } dispatch(setOrderCompleted(true)); return { success: true }; } if (!response.requiresOTP) { clearPaymentPending(); } return { success: response.success, requiresOTP: response.requiresOTP, otpType: response.otpType, message: response.message }; } catch (error) { clearPaymentPending(); return { success: false, message: error instanceof Error ? error.message : 'An unexpected error occurred' }; } }, [ paymentState, tokenData, currency, bankIca, terminalGroupId, prepareMasterpassOrder, finalizeMasterpassOrder, refreshTokenForPayment, dispatch ]); const processDirectPayment = useCallback(async (): Promise => { const { selectedInstallment } = paymentState; if (!selectedInstallment) { return { success: false, message: 'Please complete all required steps before payment' }; } if ( !newCardFormData.cardNumber || !newCardFormData.expiryDate || !newCardFormData.cvv ) { return { success: false, message: 'Please complete all card details before payment' }; } try { const freshResult = await refreshTokenForPayment(); const activeTokenData = freshResult?.tokenData || tokenData; const activeToken = freshResult?.token; const activeMerchantId = freshResult?.merchantId || tokenData?.MerchantId; if (!activeTokenData?.AccountKey) { return { success: false, message: 'Token data not available' }; } const prepareResult = await prepareMasterpassOrder({ use_three_d: paymentState.useThreeD }).unwrap(); dispatch(setOrderData(prepareResult)); const orderNo = prepareResult.context_list?.[0]?.page_context?.order_no; // From here on the order number is bound to a Masterpass transaction. // Regenerating it before that transaction settles makes the backend // reject the completion with ORDER_NO_MISMATCH. setPaymentPending(orderNo); const paymentService = new PaymentService(activeToken, activeMerchantId); const directPaymentRequest = createDirectPaymentRequest({ cvc: newCardFormData.cvv, cardNumber: newCardFormData.cardNumber.replace(/\D/g, ''), cardHolderName: newCardFormData.cardholderName || '', cardAlias: newCardFormData.cardAlias || '', expiryDate: newCardFormData.expiryDate, accountKey: activeTokenData.AccountKey, amount: prepareResult.pre_order?.total_amount_with_interest, orderNo, currencyCode: currency, installmentCount: selectedInstallment.installment_count, authenticationMethod: activeTokenData.AuthenticationMethod, secure3DModel: activeTokenData.Secure3dType, terminalGroupId, acquirerIcaNumber: bankIca, additionalFields: prepareResult.context_list?.[0]?.page_context?.extras ?.additional_fields || {} }); const paymentResult = await paymentService.directPayment( directPaymentRequest ); const response = await handleMasterpassResponse(paymentResult); if (response.requires3D && response.redirectUrl) { window.location.href = response.redirectUrl; return { success: false, requiresRedirect: true }; } if (response.success) { const transactionType = getTransactionType(paymentState.useThreeD); const finalizeResult = await finalizeMasterpassOrder({ responseCode: paymentResult.data?.result?.responseCode, token: paymentResult.data?.result?.token, three_d_secure: paymentState.useThreeD, transactionType }).unwrap(); dispatch(setOrderData(finalizeResult)); clearPaymentPending(); if (finalizeResult.errors) { return { success: false, message: finalizeResult.errors }; } dispatch(setOrderCompleted(true)); return { success: true }; } if (!response.requiresOTP) { clearPaymentPending(); } return { success: response.success, requiresOTP: response.requiresOTP, otpType: response.otpType, message: response.message }; } catch (error) { clearPaymentPending(); return { success: false, message: error instanceof Error ? error.message : 'An unexpected error occurred' }; } }, [ paymentState, tokenData, newCardFormData, currency, bankIca, terminalGroupId, prepareMasterpassOrder, finalizeMasterpassOrder, refreshTokenForPayment, dispatch ]); const finalizePaymentAfterVerification = useCallback( async (verificationResponse: any): Promise => { const { useThreeD } = paymentState; try { const finalizeResult = await finalizeMasterpassOrder({ responseCode: verificationResponse?.result?.responseCode, token: verificationResponse?.result?.token, three_d_secure: useThreeD, transactionType: getTransactionType(useThreeD) }).unwrap(); dispatch(setOrderData(finalizeResult)); clearPaymentPending(); if (finalizeResult.errors) { return { success: false, message: finalizeResult.errors }; } dispatch(setOrderCompleted(true)); return { success: true }; } catch (error) { clearPaymentPending(); return { success: false, message: error instanceof Error ? error.message : 'An unexpected error occurred' }; } }, [paymentState, finalizeMasterpassOrder, dispatch] ); return { paymentState, isCheckoutLoading, isInstallmentLoading, isPrepareLoading, isFinalizeLoading, isRewardsQueryLoading, isRewardsSelectLoading, payableAmount, updatePaymentState: updatePaymentStateAction, handleCardSelect, handleInstallmentSelect, processPayment, processDirectPayment, finalizePaymentAfterVerification, fetchRewardsForCard, openRewardModal, closeRewardModal, confirmRewards }; };