<commit_before>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Field } from 'react-final-form';
import setFieldData from 'final-form-set-field-data';

import stripesFinalForm from '@folio/stripes/final-form';
import SafeHTMLMessage from '@folio/react-intl-safe-html';
import {
  Row,
  Col,
  Button,
  TextArea,
  Modal,
  NoValue,
  TextField,
  Checkbox,
  Select,
} from '@folio/stripes/components';

import { calculateRefundSelectedAmount } from '../accountFunctions';

import css from './PayWaive.css';

class ActionModal extends React.Component {
  static propTypes = {
    form: PropTypes.object.isRequired,
    onClose: PropTypes.func,
    handleSubmit: PropTypes.func,
    open: PropTypes.bool,
    accounts: PropTypes.arrayOf(PropTypes.object),
    feeFineActions: PropTypes.arrayOf(PropTypes.object),
    data: PropTypes.arrayOf(PropTypes.object),
    balance: PropTypes.string,
    totalPaidAmount: PropTypes.string,
    owedAmount: PropTypes.string,
    submitting: PropTypes.bool,
    pristine: PropTypes.bool,
    reset: PropTypes.func,
    commentRequired: PropTypes.bool,
    owners: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    action: PropTypes.string,
    intl: PropTypes.object.isRequired,
    checkAmount: PropTypes.string,
    okapi: PropTypes.object,
    initialValues: PropTypes.object,
  };

  static defaultProps = {
    totalPaidAmount: '',
    owedAmount: '',
  }

  constructor(props) {
    super(props);

    this.state = {
      actionAllowed: false,
      accountRemainingAmount: '0.00',
      prevValidatedAmount: null,
      prevValidationError: '',
    };

    this._isMounted = false;
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  onClose = () => {
    const { onClose, form: { reset } } = this.props;
    this._isMounted = false;

    onClose();
    reset();
  }

  renderModalLabel() {
    const {
      accounts = [],
      feeFineActions = [],
      action,
      form: { getState },
      intl: { formatMessage },
    } = this.props;

    const { values: { amount } } = getState();
    const selected = calculateRefundSelectedAmount(feeFineActions);
    const type = parseFloat(amount) < parseFloat(selected)
      ? formatMessage({ id: `ui-users.accounts.${action}.summary.partially` })
      : formatMessage({ id: `ui-users.accounts.${action}.summary.fully` });

    return (
      <SafeHTMLMessage
        id="ui-users.accounts.summary"
        values={{
          count: accounts.length,
          amount,
          type,
        }}
      />
    );
  }

  renderTransactionInfo = () => {
    const { accounts } = this.props;
    return (
      <Col xs={4}>
        {accounts.length > 1 ? <br /> : ''}
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.field.transactionInfo" />
          </Col>
        </Row>
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.transaction.placeholder">
              {placeholder => (
                <Field
                  id="transaction"
                  name="transaction"
                  component={TextField}
                  placeholder={placeholder}
                />
              )}
            </FormattedMessage>
          </Col>
        </Row>
      </Col>
    );
  }

  renderMethod = (options) => {
    const {
      action,
      intl: { formatMessage }
    } = this.props;
    return (
      <Col xs={this.isPaymentAction(action) ? 3 : 7}>
        <Row>
          <Col xs>
            <FormattedMessage id={`ui-users.accounts.${action}.method`} />
            *
          </Col>
        </Row>
        <Row>
          <Col xs id="action-selection">
            <Field
              name="method"
              component={Select}
              dataOptions={options}
              placeholder={formatMessage({ id: `ui-users.accounts.${action}.method.placeholder` })}
              validate={this.validateMethod}
            />
          </Col>
        </Row>
      </Col>
    );
  }

  isPaymentAction = (action) => {
    return action === 'payment';
  }

  isRefundAction = (action) => {
    return action === 'refund';
  }

  onChangeOwner = ({ target: { value } }) => {
    const { change } = this.props.form;

    change('ownerId', value);
    change('method', null);
  }

  singleItemCheck = (amount, accountId) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    return fetch(`${okapi.url}/accounts/${accountId}/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ amount })
      });
  };

  multipleItemsCheck = (amount, accounts) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    const accountIds = accounts.reduce((ids, account) => {
      ids.push(account.id);
      return ids;
    }, []);

    return fetch(`${okapi.url}/accounts-bulk/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ accountIds, amount })
      });
  };

  triggerCheckEndpoint = (amount, accounts) => {
    if (accounts.length === 1) {
      const { id } = _.head(accounts) || {};
      return this.singleItemCheck(amount, id);
    }

    return this.multipleItemsCheck(amount, accounts);
  };

  validateAmount = async (value) => {
    let error;

    const { accounts } = this.props;

    const {
      actionAllowed,
      accountRemainingAmount,
      prevValidatedAmount,
      prevValidationError,
    } = this.state;

    if (_.isEmpty(value)) {
      error = <FormattedMessage id="ui-users.accounts.error.field" />;
    } else if (value !== prevValidatedAmount && this._isMounted) {
      const response = await this.triggerCheckEndpoint(value, accounts);
      const {
        allowed,
        errorMessage,
        remainingAmount,
      } = await response.json();

      this.setState({ prevValidatedAmount: value });

      if (!_.isUndefined(errorMessage)) {
        this.setState({ prevValidationError: errorMessage });
        error = errorMessage;
      }

      if (actionAllowed !== allowed || accountRemainingAmount !== remainingAmount) {
        this.setState({
          actionAllowed: allowed,
          accountRemainingAmount: remainingAmount,
        });
      }
    } else {
      error = prevValidationError;
    }

    return error;
  }
<commit_msg>fix-fine-amount-validation - fixed displaying of error message when form is valid<commit_after>import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Field } from 'react-final-form';
import setFieldData from 'final-form-set-field-data';

import stripesFinalForm from '@folio/stripes/final-form';
import SafeHTMLMessage from '@folio/react-intl-safe-html';
import {
  Row,
  Col,
  Button,
  TextArea,
  Modal,
  NoValue,
  TextField,
  Checkbox,
  Select,
} from '@folio/stripes/components';

import { calculateRefundSelectedAmount } from '../accountFunctions';

import css from './PayWaive.css';

class ActionModal extends React.Component {
  static propTypes = {
    form: PropTypes.object.isRequired,
    onClose: PropTypes.func,
    handleSubmit: PropTypes.func,
    open: PropTypes.bool,
    accounts: PropTypes.arrayOf(PropTypes.object),
    feeFineActions: PropTypes.arrayOf(PropTypes.object),
    data: PropTypes.arrayOf(PropTypes.object),
    balance: PropTypes.string,
    totalPaidAmount: PropTypes.string,
    owedAmount: PropTypes.string,
    submitting: PropTypes.bool,
    pristine: PropTypes.bool,
    reset: PropTypes.func,
    commentRequired: PropTypes.bool,
    owners: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    action: PropTypes.string,
    intl: PropTypes.object.isRequired,
    checkAmount: PropTypes.string,
    okapi: PropTypes.object,
    initialValues: PropTypes.object,
  };

  static defaultProps = {
    totalPaidAmount: '',
    owedAmount: '',
  }

  constructor(props) {
    super(props);

    this.state = {
      actionAllowed: false,
      accountRemainingAmount: '0.00',
      prevValidatedAmount: null,
      prevValidationError: '',
    };

    this._isMounted = false;
  }

  componentDidMount() {
    this._isMounted = true;
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  onClose = () => {
    const { onClose, form: { reset } } = this.props;
    this._isMounted = false;

    onClose();
    reset();
  }

  renderModalLabel() {
    const {
      accounts = [],
      feeFineActions = [],
      action,
      form: { getState },
      intl: { formatMessage },
    } = this.props;

    const { values: { amount } } = getState();
    const selected = calculateRefundSelectedAmount(feeFineActions);
    const type = parseFloat(amount) < parseFloat(selected)
      ? formatMessage({ id: `ui-users.accounts.${action}.summary.partially` })
      : formatMessage({ id: `ui-users.accounts.${action}.summary.fully` });

    return (
      <SafeHTMLMessage
        id="ui-users.accounts.summary"
        values={{
          count: accounts.length,
          amount,
          type,
        }}
      />
    );
  }

  renderTransactionInfo = () => {
    const { accounts } = this.props;
    return (
      <Col xs={4}>
        {accounts.length > 1 ? <br /> : ''}
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.field.transactionInfo" />
          </Col>
        </Row>
        <Row>
          <Col xs>
            <FormattedMessage id="ui-users.accounts.payment.transaction.placeholder">
              {placeholder => (
                <Field
                  id="transaction"
                  name="transaction"
                  component={TextField}
                  placeholder={placeholder}
                />
              )}
            </FormattedMessage>
          </Col>
        </Row>
      </Col>
    );
  }

  renderMethod = (options) => {
    const {
      action,
      intl: { formatMessage }
    } = this.props;
    return (
      <Col xs={this.isPaymentAction(action) ? 3 : 7}>
        <Row>
          <Col xs>
            <FormattedMessage id={`ui-users.accounts.${action}.method`} />
            *
          </Col>
        </Row>
        <Row>
          <Col xs id="action-selection">
            <Field
              name="method"
              component={Select}
              dataOptions={options}
              placeholder={formatMessage({ id: `ui-users.accounts.${action}.method.placeholder` })}
              validate={this.validateMethod}
            />
          </Col>
        </Row>
      </Col>
    );
  }

  isPaymentAction = (action) => {
    return action === 'payment';
  }

  isRefundAction = (action) => {
    return action === 'refund';
  }

  onChangeOwner = ({ target: { value } }) => {
    const { change } = this.props.form;

    change('ownerId', value);
    change('method', null);
  }

  singleItemCheck = (amount, accountId) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    return fetch(`${okapi.url}/accounts/${accountId}/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ amount })
      });
  };

  multipleItemsCheck = (amount, accounts) => {
    const {
      checkAmount,
      okapi,
    } = this.props;

    const accountIds = accounts.reduce((ids, account) => {
      ids.push(account.id);
      return ids;
    }, []);

    return fetch(`${okapi.url}/accounts-bulk/${checkAmount}`,
      {
        method: 'POST',
        headers: {
          'X-Okapi-Tenant': okapi.tenant,
          'X-Okapi-Token': okapi.token,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ accountIds, amount })
      });
  };

  triggerCheckEndpoint = (amount, accounts) => {
    if (accounts.length === 1) {
      const { id } = _.head(accounts) || {};
      return this.singleItemCheck(amount, id);
    }

    return this.multipleItemsCheck(amount, accounts);
  };

  validateAmount = async (value) => {
    let error;

    const { accounts } = this.props;

    const {
      actionAllowed,
      accountRemainingAmount,
      prevValidatedAmount,
      prevValidationError,
    } = this.state;

    if (_.isEmpty(value)) {
      error = <FormattedMessage id="ui-users.accounts.error.field" />;
    } else if (value !== prevValidatedAmount && this._isMounted) {
      const response = await this.triggerCheckEndpoint(value, accounts);
      const {
        allowed,
        errorMessage,
        remainingAmount,
      } = await response.json();

      this.setState({ prevValidatedAmount: value });

      if (!_.isUndefined(errorMessage)) {
        this.setState({ prevValidationError: errorMessage });
        error = errorMessage;
      }

      if (actionAllowed !== allowed || accountRemainingAmount !== remainingAmount) {
        this.setState({
          actionAllowed: allowed,
          accountRemainingAmount: remainingAmount,
          prevValidationError: errorMessage,
        });
      }
    } else {
      error = prevValidationError;
    }

    return error;
  }

{'fraction_non_alphanumeric': 0.13564044284771612, 'fraction_numerical': 0.0011731065327370042, 'mean_word_length': 2.19962467745719, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 87, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5992651', 'n_tokens_mistral': 4062, 'n_tokens_neox': 4013, 'n_words': 765}