SumOfUs/Champaign

View on GitHub
app/javascript/state/fundraiser/reducer.js

Summary

Maintainability
C
1 day
Test Coverage
import { isEmpty, keys, pick, reduce } from 'lodash';
import ee from '../../shared/pub_sub';

import { isDirectDebitSupported } from '../../util/directDebitDecider';

const getLocalPaymentTypes = ({ country, recurring, currency }) => {
  const supportedList = [];

  if (currency !== 'EUR' || recurring) return supportedList;

  // IDEAL
  if (country === 'NL') supportedList.push('ideal');

  // GIROPAY
  if (country === 'DE') supportedList.push('giropay');

  return supportedList;
};

export const initialState = {
  currency: 'USD',
  currentPaymentType: 'ideal',
  currentStep: 0,
  showDirectDebit: false,
  showIdeal: false,
  directDebitOnly: false,
  disableSavedPayments: false,
  donationAmount: undefined,
  selectedAmountButton: null,
  isCustomAmount: false,
  donationBands: {
    USD: [2, 5, 10, 25, 50],
    GBP: [2, 5, 10, 25, 50],
    EUR: [2, 5, 10, 25, 50],
    CHF: [2, 5, 10, 25, 50],
    CAD: [2, 5, 10, 25, 50],
    AUD: [2, 5, 10, 25, 50],
    NZD: [2, 5, 10, 25, 50],
    MXN: [2, 5, 10, 25, 50],
    ARS: [2, 5, 10, 25, 50],
    BRL: [2, 5, 10, 25, 50],
  },
  fields: [],
  form: {},
  formId: '',
  formValues: {},
  oneClick: false,
  outstandingFields: [],
  paymentMethods: [],
  paymentTypes: ['card', 'paypal'],
  localPaymentTypes: [],
  preselectAmount: false,
  recurring: false,
  recurringDefault: 'one_off',
  storeInVault: false,
  submitting: false,
  oneClickError: false,
  title: '',
  supportedLocalCurrency: true,
  merchantAccounts: {
    USD: 'sumofus',
    GBP: 'sumofus2_GBP',
    EUR: 'sumofus2_EUR',
    CHF: 'SumOfUs_CHF',
    CAD: 'SumOfUs_CAD',
    AUD: 'SumOfUs_AUD',
    NZD: 'SumOfUs_NZD',
    MXN: 'SumOfUs_MXN',
    ARS: 'SumOfUs_ARS',
    BRL: 'SumOfUs_BRL',
  },
  merchantAccountId: 'sumofus',
};

export default (state = initialState, action) => {
  switch (action.type) {
    case 'initialize_fundraiser':
      const initialData = pick(
        action.payload,
        'member',
        'pageId',
        'formValues',
        'formId',
        'outstandingFields',
        'title',
        'fields',
        'oneClick',
        'donationAmount',
        'id_mismatch'
      );
      initialData.formValues = initialData.formValues || {};
      return {
        ...state,
        ...initialData,
        paymentTypes: ['paypal', 'card'],
      };
    case 'search_string_overrides':
      return searchStringOverrides(state, action.payload);
    case 'reset_member':
      return {
        ...state,
        outstandingFields: state.fields.map(field => field.name),
        formValues: {},
      };
    case 'change_currency': {
      const { preselectAmount, donationBands, merchantAccounts } = state;
      const currency = supportedCurrency(action.payload, keys(donationBands));
      const localPaymentTypes = getLocalPaymentTypes({
        country: state.form.country || state.formValues.country,
        recurring: state.recurring,
        currency: currency,
      });
      const merchantAccountId = merchantAccounts[currency.toUpperCase()];

      return {
        ...state,
        ...featuredAmountState(preselectAmount, { donationBands, currency }),
        currency,
        localPaymentTypes,
        merchantAccountId,
      };
    }
    case 'change_amount':
      const donationAmount = action.payload || undefined;
      return { ...state, donationAmount };
    case 'set_selected_amount_button':
      return { ...state, selectedAmountButton: action.payload };
    case 'one_click_failed':
      return { ...state, disableSavedPayments: true, oneClickError: true };
    case 'change_step':
      return { ...state, currentStep: action.payload };
    case 'set_is_custom_amount':
      return { ...state, isCustomAmount: action.payload };
    case 'update_form': {
      const form = action.payload;
      const showDirectDebit = isDirectDebitSupported({
        country: form.country,
        recurring: state.recurring,
      });
      const localPaymentTypes = getLocalPaymentTypes({
        country: form.country,
        recurring: state.recurring,
        currency: state.currency,
      });

      const paymentTypes = supportedPaymentTypes({
        showDirectDebit,
        localPaymentTypes,
        directDebitOnly: state.directDebitOnly,
      });
      const currentPaymentType = safePaymentType(
        state.currentPaymentType,
        paymentTypes
      );
      return {
        ...state,
        form,
        showDirectDebit,
        localPaymentTypes,
        paymentTypes,
        currentPaymentType,
      };
    }
    case 'set_direct_debit_only': {
      const localPaymentTypes = getLocalPaymentTypes({
        country: state.form.country || state.formValues.country,
        recurring: action.payload,
        currency: state.currency,
      });

      const paymentTypes = supportedPaymentTypes({
        showDirectDebit: state.showDirectDebit,
        showIdeal: state.showIdeal,
        directDebitOnly: action.payload,
        localPaymentTypes,
      });

      const currentPaymentType = safePaymentType(
        state.currentPaymentType,
        paymentTypes
      );

      return {
        ...state,
        directDebitOnly: action.payload,
        paymentTypes,
        currentPaymentType,
      };
    }
    case 'set_donation_bands':
      const payload = action.payload;
      const donationBands = isEmpty(payload) ? state.donationBands : payload;
      const currency = supportedCurrency(state.currency, keys(donationBands));
      const preselectAmount = state.preselectAmount;
      return {
        ...state,
        ...featuredAmountState(preselectAmount, { donationBands, currency }),
        currency,
        donationBands,
      };
    case 'set_payment_type':
      const label = state.currentPaymentType
        ? `from_${state.currentPaymentType}_to_${action.payload}`
        : action.payload;
      ee.emit('set_payment_type', label);
      return {
        ...state,
        currentPaymentType: safePaymentType(action.payload, state.paymentTypes),
      };
    case 'set_recurring': {
      const showDirectDebit = isDirectDebitSupported({
        country: state.form.country || state.formValues.country,
        recurring: action.payload,
      });

      const localPaymentTypes = getLocalPaymentTypes({
        country: state.form.country || state.formValues.country,
        recurring: action.payload,
        currency: state.currency,
      });

      const paymentTypes = supportedPaymentTypes({
        showDirectDebit,
        localPaymentTypes,
        directDebitOnly: state.directDebitOnly,
      });
      const currentPaymentType = safePaymentType(
        state.currentPaymentType,
        paymentTypes
      );

      return {
        ...state,
        recurring: action.payload,
        showDirectDebit,
        localPaymentTypes,
        paymentTypes,
        currentPaymentType,
      };
    }
    case 'set_recurring_defaults': {
      const data = recurringState(action.payload);
      const showDirectDebit = isDirectDebitSupported({
        country: state.form.country || state.formValues.country,
        recurring: data.recurring,
      });

      const localPaymentTypes = getLocalPaymentTypes({
        country: state.form.country || state.formValues.country,
        recurring: data.recurring,
        currency: state.currency,
      });

      const paymentTypes = supportedPaymentTypes({
        showDirectDebit,
        localPaymentTypes,
        directDebitOnly: state.directDebitOnly,
      });

      const currentPaymentType = safePaymentType(
        state.currentPaymentType,
        paymentTypes
      );
      return {
        ...state,
        ...data,
        showDirectDebit,
        localPaymentTypes,
        paymentTypes,
        currentPaymentType,
      };
    }
    case 'set_store_in_vault':
      return { ...state, storeInVault: action.payload };
    case 'set_submitting':
      return { ...state, submitting: action.payload };
    case 'preselect_amount':
      return {
        ...state,
        ...featuredAmountState(action.payload, {
          donationBands: state.donationBands,
          currency: state.currency,
        }),
      };
    // Update our form with data from another form
    // E.g. petition was signed, so we can re-use the data from that form in
    // this form.
    case '@@chmp:action_form:updated': {
      const relevantFields = state.fields.map(field => field.name);
      const formValues = pick(action.payload, relevantFields);
      const form = formValues;
      const outstandingFields = relevantFields.filter(key => !formValues[key]);
      const showDirectDebit = isDirectDebitSupported({
        country: formValues.country,
        recurring: state.recurring,
      });
      const localPaymentTypes = getLocalPaymentTypes({
        country: formValues.country,
        recurring: state.recurring,
        currenct: state.currency,
      });

      const paymentTypes = supportedPaymentTypes({
        showDirectDebit,
        localPaymentTypes,
        directDebitOnly: state.directDebitOnly,
      });
      const currentPaymentType = safePaymentType(
        state.currentPaymentType,
        paymentTypes
      );
      return {
        ...state,
        form,
        formValues,
        outstandingFields,
        paymentTypes,
        localPaymentTypes,
        currentPaymentType,
      };
    }
    case 'set_supported_local_currency':
      return { ...state, supportedLocalCurrency: action.payload };
    default:
      return state;
  }
};

export const RECURRING_DEFAULTS = ['one_off', 'recurring', 'only_recurring'];

// `supportedCurrency` gets a currency string and compares it against our list of supported currencies.
// If our currency is not in the list of supported currencies, it will return the default currency (which,
// by default, is the first currency in our supoortedCurrencies list)
export function supportedCurrency(
  currency,
  currencies = Object.keys(initialState.donationBands)
) {
  return currencies.find(c => c === currency) || currencies[0];
}

export function recurringState(recurringValue = '') {
  const recurringDefault =
    RECURRING_DEFAULTS.find(i => i === recurringValue.toLowerCase()) ||
    'one_off';
  return {
    recurring: recurringDefault !== 'one_off',
    recurringDefault,
  };
}

export function pickMedianAmount(bands, currency) {
  const amounts = bands[currency];
  return amounts[Math.floor(amounts.length / 2)] || 0;
}

export function featuredAmountState(preselectAmount, state) {
  if (preselectAmount && state) {
    return {
      preselectAmount,
      donationFeaturedAmount: pickMedianAmount(
        state.donationBands,
        state.currency
      ),
    };
  }

  return {
    preselectAmount,
  };
}

const searchStringHandlers = {
  hide_spm: function hideSavedPayments(state, value) {
    if (value === '1') return { ...state, disableSavedPayments: true };
    else return state;
  },
};

// Overrides state values based on url query/search string (highest priority)
export function searchStringOverrides(state, search) {
  const handlers = pick(searchStringHandlers, keys(search));
  return reduce(handlers, (res, handler, k) => handler(res, search[k]), state);
}

function supportedPaymentTypes(data) {
  let list = [];
  if (data.localPaymentTypes?.length > 0) list = data.localPaymentTypes;
  if (data.showDirectDebit) list.push('gocardless');
  if (!data.directDebitOnly) list.push('paypal', 'card');
  return list;
}

function safePaymentType(pt, pts) {
  return pts.includes(pt) ? pt : pts[0];
}