package ru.yandex.autotests.directapi.steps.finance;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;

import ru.yandex.autotests.direct.utils.Parser;
import ru.yandex.autotests.direct.utils.beans.BeanPropertyPredicate;
import ru.yandex.autotests.direct.utils.config.DirectTestRunProperties;
import ru.yandex.autotests.direct.utils.money.Currency;
import ru.yandex.autotests.direct.utils.money.Money;
import ru.yandex.autotests.directapi.apiclient.RequestHeader;
import ru.yandex.autotests.directapi.apiclient.config.ConnectionConfig;
import ru.yandex.autotests.directapi.apiclient.errors.AxisError;
import ru.yandex.autotests.directapi.apiclient.methods.Action;
import ru.yandex.autotests.directapi.apiclient.methods.Method;
import ru.yandex.autotests.directapi.common.api45.AccountActionResult;
import ru.yandex.autotests.directapi.common.api45.AccountManagementResponse;
import ru.yandex.autotests.directapi.common.api45.Error;
import ru.yandex.autotests.directapi.exceptions.DirectAPIException;
import ru.yandex.autotests.directapi.matchers.axiserror.ApiResponse;
import ru.yandex.autotests.directapi.model.User;
import ru.yandex.autotests.directapi.model.balance.OrderInfo;
import ru.yandex.autotests.directapi.model.campaigns.SmsNotificationInfoMap;
import ru.yandex.autotests.directapi.model.common.SpendMode;
import ru.yandex.autotests.directapi.model.finances.CheckPaymentInfoMap;
import ru.yandex.autotests.directapi.model.finances.CreateInvoiceInfoMap;
import ru.yandex.autotests.directapi.model.finances.PayCampElementMap;
import ru.yandex.autotests.directapi.model.finances.PayCampaignsByCardInfoMap;
import ru.yandex.autotests.directapi.model.finances.PayCampaignsInfoMap;
import ru.yandex.autotests.directapi.model.finances.PayMethod;
import ru.yandex.autotests.directapi.model.finances.PaymentMap;
import ru.yandex.autotests.directapi.model.finances.TransferMoneyInfoMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.AccountDayBudgetInfoMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.AccountManagementRequestMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.AccountMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.AccountSelectionCriteriaMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.EnableSharedAccountRequestMap;
import ru.yandex.autotests.directapi.model.finances.sharedaccount.TransferMap;
import ru.yandex.autotests.directapi.steps.BaseApiSteps;
import ru.yandex.autotests.directapi.steps.UserSteps;
import ru.yandex.autotests.irt.testutils.allure.AssumptionException;
import ru.yandex.autotests.irt.testutils.allure.LogSteps;
import ru.yandex.autotests.irt.testutils.json.JsonUtils;
import ru.yandex.qatools.allure.annotations.Step;

import static ch.lambdaj.Lambda.extractProperty;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static ru.yandex.autotests.irt.testutils.allure.TestSteps.assertThat;
import static ru.yandex.autotests.irt.testutils.allure.TestSteps.assumeThat;
import static ru.yandex.autotests.irt.testutils.beandiffer2.BeanDifferMatcher.beanDiffer;


/**
 * Created with IntelliJ IDEA.
 * User: mariabye
 * Date: 24.06.13
 * Time: 18:29
 * To change this template use File | Settings | File Templates.
 */
@SuppressWarnings("unchecked")
public class FinanceSteps extends BaseApiSteps {
    private LogSteps log = LogSteps.getLogger(this.getClass());
    private static DirectTestRunProperties properties = DirectTestRunProperties.getInstance();

    private static FinanceSteps _instance;
    public static int PAYMENT_ITEMS_MAX_NUMBER = 50;
    public static final String SANDBOX_CONTRACT = "11111/00";
    public static final String SANDBOX_PAYMENT_TOKEN =
            "12345678901234.1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890" +
                    "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF123" +
                    "4567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF";

    private FinanceSteps(ConnectionConfig connectionConfig, RequestHeader requestHeader) {
        super(connectionConfig, requestHeader);
    }

    public static FinanceSteps getInstance(ConnectionConfig connectionConfig, RequestHeader requestHeader) {
        if (_instance == null) {
            _instance = new FinanceSteps(connectionConfig, requestHeader);
        } else {
            _instance.setConnectionConfig(connectionConfig);
            _instance.setRequestHeader(requestHeader);
        }
        return _instance;
    }

    //region CreateInvoice
    @Step("[CreateInvoice]")
    public String createInvoice(CreateInvoiceInfoMap createInvoiceInfoMap) {
        log.info(JsonUtils.toString(createInvoiceInfoMap.getBean()));
        requestHeader.setDirectFinanceOAUTH(Method.CREATE_INVOICE);
        try {
            return defaultClient().invokeMethod(Method.CREATE_INVOICE, createInvoiceInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[CreateInvoice]")
    public String createInvoiceWithPaymentToken(CreateInvoiceInfoMap createInvoiceInfoMap) {
        requestHeader.setYMFinanceOAUTH();
        try {
            return soapClient().invokeMethod(Method.CREATE_INVOICE, createInvoiceInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public String createInvoice(PayCampElementMap... payCampElements) {
        return createInvoice(new CreateInvoiceInfoMap(soapClient().getPackageName()).withPayments(payCampElements));
    }

    @Step("[CreateInvoice]: Sandbox")
    public String createInvoiceSandbox(PayCampElementMap... payCampElements) {
        CreateInvoiceInfoMap request = new CreateInvoiceInfoMap(soapClient().getPackageName())
                .withPayments(payCampElements);
        requestHeader.setDirectFinanceOAUTH(Method.CREATE_INVOICE.toString(), true);
        try {
            return soapClient().invokeMethod(Method.CREATE_INVOICE, request.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public String getRequestID(PayCampElementMap... payCampElements) {
        String invoiceUrl = createInvoice(
                new CreateInvoiceInfoMap(soapClient().getPackageName()).withPayments(payCampElements));
        try {
            log.info("Квитанция сформирована: " + invoiceUrl);
            return Parser.getMatchFromString(invoiceUrl, ".+(request_id%3D)(\\d+).+", 2);
        } catch (Exception e) {
            throw new DirectAPIException("Ошибка получения requestID из готовой ссылки на оплату", e);
        }
    }

    public void shouldGetErrorOnCreateInvoice(AxisError axisError, PayCampElementMap... payCampElements) {
        requestHeader.setDirectFinanceOAUTH(Method.CREATE_INVOICE);
        try {
            shouldGetErrorOn(Method.CREATE_INVOICE,
                    new CreateInvoiceInfoMap<>(soapClient().getPackageName())
                            .withPayments(payCampElements),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnCreateInvoiceSandbox(AxisError axisError, PayCampElementMap... payCampElements) {
        requestHeader.setDirectFinanceOAUTH(Method.CREATE_INVOICE.toString(), true);
        try {
            shouldGetErrorOn(Method.CREATE_INVOICE,
                    new CreateInvoiceInfoMap<>(soapClient().getPackageName())
                            .withPayments(payCampElements),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    //region GetCreditLimits
    @Step("[GetCreditLimits]")
    public <CreditLimitsInfo> CreditLimitsInfo getCreditLimits() {
        requestHeader.setDirectFinanceOAUTH(Method.GET_CREDIT_LIMITS);
        try {
            return soapClient().invokeMethod(Method.GET_CREDIT_LIMITS, null);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[GetCreditLimits]")
    public <CreditLimitsInfo> CreditLimitsInfo getCreditLimitsWithPaymentToken() {
        requestHeader.setYMFinanceOAUTH();
        try {
            return soapClient().invokeMethod(Method.GET_CREDIT_LIMITS, null);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[GetCreditLimits]: Sandbox")
    public <CreditLimitsInfo> CreditLimitsInfo getCreditLimitsSandbox() {
        requestHeader.setDirectFinanceOAUTH(Method.GET_CREDIT_LIMITS.toString(), true);
        try {
            return soapClient().invokeMethod(Method.GET_CREDIT_LIMITS, null);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public <CreditLimitsItem> CreditLimitsItem getLimits(String externalContractID) {
        Object[] limits = (Object[]) new BeanMap(getCreditLimits()).get("limits");
        Object creditLimit = CollectionUtils.find(Arrays.asList(limits),
                new BeanPropertyPredicate("contractID", externalContractID)
        );
        return (CreditLimitsItem) creditLimit;
    }

    public Money getContractSpent(String externalContractID) {
        Object creditLimitsInfo = getCreditLimits();
        Object[] limits = (Object[]) new BeanMap(getCreditLimits()).get("limits");
        Object creditLimit = CollectionUtils.find(Arrays.asList(limits),
                new BeanPropertyPredicate("contractID", externalContractID)
        );
        if (creditLimit != null) {
            float limitSpent = (float) new BeanMap(creditLimit).get("limitSpent");
            String currency = (String) new BeanMap(creditLimitsInfo).get("currency");
            return Money.valueOf(limitSpent, Currency.getFor(currency));
        }
        return null;
    }

    //endregion

    //region GetBalance
    @Step("[GetBalance]")
    public <CampaignBalanceInfo> CampaignBalanceInfo[] getBalance(int... campaignIDs) {
        return jsonClient().invokeMethod(Method.GET_BALANCE, campaignIDs);
    }

    @Step("[GetBalance]")
    public <CampaignBalanceInfo> CampaignBalanceInfo[] getBalance(long... campaignIds) {
        int[] campaignIdsInt = Arrays.stream(campaignIds).mapToInt(Ints::checkedCast).toArray();
        return jsonClient().invokeMethod(Method.GET_BALANCE, campaignIdsInt);
    }
    //endregion

    //region TransferMoney
    @Step("[TransferMoney]")
    public int transferMoney(TransferMoneyInfoMap transferMoneyInfoMap) {
        requestHeader.setDirectFinanceOAUTH(Method.TRANSFER_MONEY);
        try {
            return defaultClient().invokeMethod(Method.TRANSFER_MONEY, transferMoneyInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[TransferMoney]: Sandbox")
    public int transferMoneySandbox(TransferMoneyInfoMap transferMoneyInfoMap) {
        requestHeader.setDirectFinanceOAUTH(Method.TRANSFER_MONEY.toString(), true);
        try {
            return defaultClient().invokeMethod(Method.TRANSFER_MONEY, transferMoneyInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[TransferMoney]")
    public int transferMoneyWithPaymentToken(TransferMoneyInfoMap transferMoneyInfoMap) {
        requestHeader.setYMFinanceOAUTH();
        try {
            return jsonClient().invokeMethod(Method.TRANSFER_MONEY, transferMoneyInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public int transferMoney(long campaignIdFrom, long campaignIdTo, Float amount) {
        return transferMoney(Ints.checkedCast(campaignIdFrom), Ints.checkedCast(campaignIdTo), amount, null);
    }

    public int transferMoney(int campaignIDFrom, int campaignIDTo, Float amount) {
        return transferMoney(campaignIDFrom, campaignIDTo, amount, null);
    }

    public int transferMoney(long campaignIdFrom, long campaignIdTo, Float amount, Currency currency) {
        return transferMoney(Ints.checkedCast(campaignIdFrom), Ints.checkedCast(campaignIdTo), amount, currency);
    }

    public int transferMoney(int campaignIDFrom, int campaignIDTo, Float amount, Currency currency) {
        return transferMoney(
                new TransferMoneyInfoMap(jsonClient().getPackageName())
                        .withFromCampaigns(
                                new PayCampElementMap(jsonClient().getPackageName())
                                        .withCampaignID(campaignIDFrom)
                                        .withSum(amount)
                                        .withCurrency(currency)
                        )
                        .withToCampaigns(
                                new PayCampElementMap(jsonClient().getPackageName())
                                        .withCampaignID(campaignIDTo)
                                        .withSum(amount)
                                        .withCurrency(currency)
                        )
        );
    }

    public int transferMoney(PayCampElementMap from, PayCampElementMap to) {
        return transferMoney(
                new TransferMoneyInfoMap(jsonClient().getPackageName())
                        .withFromCampaigns(from)
                        .withToCampaigns(to)
        );
    }

    public void shouldGetErrorOnTransferMoney(long campaignIdFrom, long campaignIdTo, float amount,
            AxisError axisError)
    {
        shouldGetErrorOnTransferMoney(
                Ints.checkedCast(campaignIdFrom), Ints.checkedCast(campaignIdTo), amount, null, axisError);
    }

    public void shouldGetErrorOnTransferMoney(int campaignIDFrom, int campaignIDTo, float amount, AxisError axisError) {
        shouldGetErrorOnTransferMoney(campaignIDFrom, campaignIDTo, amount, null, axisError);
    }

    public void shouldGetErrorOnTransferMoney(long campaignIdFrom, long campaignIdTo, float amount, Currency currency,
            AxisError axisError)
    {
        shouldGetErrorOnTransferMoney(
                Ints.checkedCast(campaignIdFrom), Ints.checkedCast(campaignIdTo), amount, currency, axisError);
    }

    public void shouldGetErrorOnTransferMoney(int campaignIDFrom, int campaignIDTo, float amount, Currency currency,
            AxisError axisError)
    {
        shouldGetErrorOnTransferMoney(
                new TransferMoneyInfoMap(jsonClient().getPackageName())
                        .withFromCampaigns(
                                new PayCampElementMap(jsonClient().getPackageName())
                                        .withCampaignID(campaignIDFrom)
                                        .withSum(amount)
                                        .withCurrency(currency)
                        )
                        .withToCampaigns(
                                new PayCampElementMap(jsonClient().getPackageName())
                                        .withCampaignID(campaignIDTo)
                                        .withSum(amount)
                                        .withCurrency(currency)
                        ), axisError);
    }

    public void shouldGetErrorOnTransferMoney(TransferMoneyInfoMap transferMoneyInfoMap, AxisError axisError) {
        requestHeader.setDirectFinanceOAUTH(Method.TRANSFER_MONEY);
        try {
            shouldGetErrorOn(
                    Method.TRANSFER_MONEY,
                    transferMoneyInfoMap,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnTransferMoney(Object request, AxisError axisError) {
        requestHeader.setDirectFinanceOAUTH(Method.TRANSFER_MONEY);
        try {
            shouldGetErrorOn(
                    Method.TRANSFER_MONEY,
                    request,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    //region PayCampaigns
    @Step("[PayCampaigns]")
    public int payCampaigns(PayCampaignsInfoMap payCampaignsInfoMap, boolean withYmOAUTH) {
        if (withYmOAUTH) {
            requestHeader.setYMFinanceOAUTH();
        } else {
            requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS);
        }
        try {
            return defaultClient().invokeMethod(Method.PAY_CAMPAIGNS, payCampaignsInfoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[PayCampaigns]")
    public int payCampaigns(PayCampaignsInfoMap payCampaignsInfoMap) {
        return payCampaigns(payCampaignsInfoMap, false);
    }

    public int payCampaigns(
            String payMethod, String contractID, boolean withYmOAUTH, PayCampElementMap... payCampElementMaps)
    {
        return payCampaigns(
                new PayCampaignsInfoMap(soapClient().getPackageName())
                        .withPayMethod(payMethod)
                        .withContractID(contractID)
                        .withPayments(payCampElementMaps),
                withYmOAUTH
        );
    }

    public int payCampaigns(String payMethod, String contractID, PayCampElementMap... payCampElementMaps) {
        return payCampaigns(
                new PayCampaignsInfoMap(soapClient().getPackageName())
                        .withPayMethod(payMethod)
                        .withContractID(contractID)
                        .withPayments(payCampElementMaps),
                false
        );
    }

    public int payCampaignSandbox(Float sum, int campaignId) {
        return payCampaignSandbox(sum, null, campaignId);
    }

    public int payCampaignSandbox(Long sum, Long campaignId) {
        return payCampaignSandbox(sum, null, campaignId);
    }

    public int payCampaignSandbox(Float sum, Currency currency, int campaignId) {
        return payCampaignSandbox(new PayCampElementMap(soapClient().getPackageName())
                .withCampaignID(campaignId)
                .withCurrency(currency)
                .withSum(sum));
    }

    public int payCampaignSandbox(Long sum, Currency currency, Long campaignId) {
        return payCampaignSandbox(new PayCampElementMap(soapClient().getPackageName())
                .withCampaignID(campaignId.intValue())
                .withCurrency(currency)
                .withSum(sum / 1000000f));
    }

    public int payCampaignSandboxOverdraft(Currency currency, Float sum, int campaignId) {
        return payCampaignSandboxOverdraft(new PayCampElementMap(soapClient().getPackageName())
                .withCurrency(currency)
                .withCampaignID(campaignId)
                .withSum(sum));
    }

    public int payCampaignSandboxOverdraft(Float sum, int campaignId) {
        return payCampaignSandboxOverdraft(new PayCampElementMap(soapClient().getPackageName())
                .withCampaignID(campaignId)
                .withSum(sum));
    }

    @Step("[PayCampaigns]: Sandbox Bank")
    public int payCampaignSandbox(PayCampElementMap... payElements) {
        requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS.toString(), true);
        PayCampaignsInfoMap infoMap = new PayCampaignsInfoMap(soapClient().getPackageName())
                .withPayMethod(PayMethod.BANK)
                .withContractID(SANDBOX_CONTRACT)
                .withPayments(payElements);
        try {
            return defaultClient().invokeMethod(Method.PAY_CAMPAIGNS, infoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[PayCampaigns]: Sandbox Overdraft")
    public int payCampaignSandboxOverdraft(PayCampElementMap... payElements) {
        requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS.toString(), true);
        PayCampaignsInfoMap infoMap = new PayCampaignsInfoMap(soapClient().getPackageName())
                .withPayMethod(PayMethod.OVERDRAFT)
                .withPayments(payElements);
        try {
            return defaultClient().invokeMethod(Method.PAY_CAMPAIGNS, infoMap.getBean());
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public int payCampaignsBank(String contractID, PayCampElementMap... payCampElementMaps) {
        return payCampaigns(
                new PayCampaignsInfoMap(soapClient().getPackageName())
                        .withPayMethod(PayMethod.BANK)
                        .withContractID(contractID)
                        .withPayments(payCampElementMaps),
                false
        );
    }

    public int payCampaignsOverdraft(PayCampElementMap... payCampElementMaps) {
        return payCampaigns(
                new PayCampaignsInfoMap(soapClient().getPackageName())
                        .withPayMethod(PayMethod.OVERDRAFT)
                        .withPayments(payCampElementMaps),
                false
        );
    }

    public int payCampaignsYM(PayCampElementMap... payCampElementMaps) {
        return payCampaigns(
                new PayCampaignsInfoMap(soapClient().getPackageName())
                        .withPayMethod(PayMethod.YM)
                        .withPayments(payCampElementMaps),
                true
        );
    }

    public void shouldGetErrorOnPayCampaigns(AxisError axisError, PayCampaignsInfoMap payCampaignsInfoMaps) {
        requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS);
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payCampaignsInfoMaps,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnPayCampaignsSandbox(AxisError axisError, PayCampElementMap... payCampElements) {
        PayCampaignsInfoMap payInfoMap = new PayCampaignsInfoMap(soapClient().getPackageName())
                .withPayMethod(PayMethod.BANK)
                .withContractID(SANDBOX_CONTRACT)
                .withPayments(payCampElements);
        shouldGetErrorOnPayCampaignsSandbox(axisError, payInfoMap);
    }

    public void shouldGetErrorOnPayCampaignsSandbox(AxisError axisError, PayCampaignsInfoMap payCampaignsInfoMap) {
        requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS.toString(), true);
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payCampaignsInfoMap,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnPayCampaignsOverdraftSandbox(Float sum, int campaignId, AxisError axisError) {
        shouldGetErrorOnPayCampaignsOverdraftSandbox(sum, null, campaignId, axisError);
    }

    public void shouldGetErrorOnPayCampaignsOverdraftSandbox(Float sum, Currency currency, int campaignId,
            AxisError axisError)
    {
        PayCampaignsInfoMap payInfoMap = new PayCampaignsInfoMap(soapClient().getPackageName())
                .withPayMethod(PayMethod.OVERDRAFT)
                .withPayments(new PayCampElementMap(soapClient().getPackageName())
                        .withCampaignID(campaignId)
                        .withCurrency(currency)
                        .withSum(sum));

        requestHeader.setDirectFinanceOAUTH(Method.PAY_CAMPAIGNS.toString(), true);
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payInfoMap,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnPayCampaignsYMSandbox(AxisError axisError, PayCampaignsInfoMap payCampaignsInfoMap) {
        requestHeader.setYMFinanceOAUTH(SANDBOX_PAYMENT_TOKEN);
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payCampaignsInfoMap,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnPayCampaignsYM(AxisError axisError, PayCampaignsInfoMap payCampaignsInfoMaps) {
        requestHeader.setYMFinanceOAUTH();
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payCampaignsInfoMaps,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnPayCampaignsYM(AxisError axisError,
            PayCampaignsInfoMap payCampaignsInfoMaps,
            String paymentToken)
    {
        requestHeader.setYMFinanceOAUTH(paymentToken);
        try {
            shouldGetErrorOn(
                    Method.PAY_CAMPAIGNS,
                    payCampaignsInfoMaps,
                    axisError
            );
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    //region EnableSharedAccount
    @Step("[EnableSharedAccount]")
    public <EnableSharedAccountResponse> EnableSharedAccountResponse enableSharedAccount(
            EnableSharedAccountRequestMap enableSharedAccountRequestMap)
    {
        return jsonClient().invokeMethod(Method.ENABLE_SHARED_ACCOUNT, enableSharedAccountRequestMap.getBean());
    }

    @Step("Включение единого счета у клиента: {0}")
    public <EnableSharedAccountResponse> EnableSharedAccountResponse enableSharedAccount(String login) {
        return enableSharedAccount(
                new EnableSharedAccountRequestMap<>(jsonClient().getPackageName()).withLogins(login));
    }

    public int enableAndGetSharedAccount(String login) {
        Object enableSharedAccountResponse;
        try {
            enableSharedAccountResponse = enableSharedAccount(login);
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка включения общего счета");
        }
        Object[] errors = (Object[]) new BeanMap(enableSharedAccountResponse).get("errors");
        if (errors == null) {
            return (int) new BeanMap(enableSharedAccountResponse).get("accountID");
        }
        if (errors.length > 0 && ((int) new BeanMap(errors[0]).get("faultCode")) != 519) {
            throw new DirectAPIException(
                    "Неожиданная ошибка при подключении ОС", new AxisError(new BeanMap(errors[0]).getBean()));
        } else {
            return getAccountID(login);
        }
    }

    public void verifySharedAccount(String login) {
        int accountId;
        Object account = getAccount(login);
        if (account == null) {
            accountId = enableAndGetSharedAccount(login);
        } else {
            accountId = (int) new BeanMap(account).get("accountID");
        }
        // убеждаемся, что кошелек не пропал из баланса после переналивки
        verifyCampaignExistsInBalance(accountId);
    }

    public void verifyAccountExistsInBalance(String login) {
        int accountID = getAccountID(login);
        verifyCampaignExistsInBalance(accountID);
    }

    /**
     * Убедиться, что кампания есть в балансе.
     * Перепосылает её в баланс, если нет.
     */
    public void verifyCampaignExistsInBalance(int campaignId) {
        UserSteps userSteps = UserSteps.getInstance();
        try {
            OrderInfo ignored = userSteps.balanceSteps().getOrderInfo(campaignId);
        } catch (AssumptionException e) {
            userSteps.getDarkSideSteps().fakeAdminSteps().sendCampaignToBalance(campaignId);
        }
    }

    public void shouldGetErrorsOnEnableSharedAccount(EnableSharedAccountRequestMap request, AxisError... axisError) {
        shouldGetMultipleErrorOn(
                Method.ENABLE_SHARED_ACCOUNT,
                request.getBean(),
                "$.Errors[*]",
                axisError
        );
    }
    //endregion

    //region PayCampaignsByCard
    @Step("[PayCampaignsByCard]")
    public String payCampaignsByCard(PayCampaignsByCardInfoMap payCampaignsByCardInfoMap) {
        return defaultClient().invokeMethod(Method.PAY_CAMPAIGNS_BY_CARD, payCampaignsByCardInfoMap.getBean());
    }
    //endregion

    //region CheckPayment
    @Step("[CheckPayment]")
    public String checkPayment(CheckPaymentInfoMap checkPaymentInfoMap) {
        return defaultClient().invokeMethod(Method.CHECK_PAYMENT, checkPaymentInfoMap.getBean());
    }

    public String checkPayment(String customTransctionId) {
        return checkPayment(new CheckPaymentInfoMap(defaultClient().getPackageName())
                .withCustomTransactionId(customTransctionId));
    }

    public Callable<Boolean> checkPaymentSuccess(String customTransactionId) {
        return () -> checkPayment(customTransactionId).equals("Success");
    }
    //endregion

    //region AccountManagement
    //region Invoice
    @Step("[AccountManagement.Invoice]")
    public <AccountManagementResponse> AccountManagementResponse invoice(PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.INVOICE);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.INVOICE)
                            .withPayments(paymentMaps).getBean());
            assumeThat("ответ не содержит ошибок", JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[AccountManagement.Invoice] in SANDBOX")
    public <AccountManagementResponse> AccountManagementResponse invoiceInSandbox(PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.INVOICE, true);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.INVOICE)
                            .withPayments(paymentMaps).getBean());
            assumeThat("ответ не содержит ошибок", JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void expectErrorOnAMInvoice(AxisError error, PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.INVOICE);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.INVOICE)
                            .withPayments(paymentMaps).getBean());
            assertThat("ответ содержит правильную ошибку", JsonUtils.toString(response), ApiResponse.hasError(error));
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public <AccountManagementResponse> AccountManagementResponse invoice(
            int accountID, float amount, Currency currency)
    {
        return invoice(
                new PaymentMap(jsonClient().getPackageName())
                        .withAccountID(accountID)
                        .withAmount(amount)
                        .withCurrency(currency));
    }


    public String getInvoiceRequestID(int accountID, float amount, Currency currency) {
        return getInvoiceRequestIDs(
                new PaymentMap(jsonClient().getPackageName())
                        .withAccountID(accountID)
                        .withAmount(amount)
                        .withCurrency(currency)
        )[0];
    }

    public String[] getInvoiceRequestIDs(PaymentMap... paymentMaps) {
        try {
            Object accountManagementResponse = invoice(paymentMaps);
            assumeThat("ответ не содержит ошибок",
                    JsonUtils.toString(accountManagementResponse), ApiResponse.hasNoError());
            Object[] actionResults = (Object[]) new BeanMap(accountManagementResponse).get("actionsResult");
            List<String> urls = extractProperty(actionResults, "URL");

            List<String> result = Lists.transform(urls,
                    new Function<String, String>() {
                        public String apply(String url) {
                            return Parser.getMatchFromString(url, ".+(request_id%3D)(\\d+).+", 2);
                        }
                    });
            log.info("Квитанция: ");
            log.info(urls);
            return result.toArray(new String[0]);
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка выставления счета на оплату", e);
        }
    }

    public void shouldGetErrorOnAMInvoice(int accountID, float amount, Currency currency, AxisError error) {
        shouldGetErrorOnAMInvoice(error,
                new PaymentMap(jsonClient().getPackageName())
                        .withAccountID(accountID)
                        .withAmount(amount)
                        .withCurrency(currency));
    }

    public void shouldGetErrorOnAMInvoice(AxisError axisError, PaymentMap... payments) {
        requestHeader.setDirectFinanceOAUTH(Action.INVOICE);
        try {
            shouldGetErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(jsonClient().getPackageName())
                            .withAction(Action.INVOICE)
                            .withPayments(payments),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorsOnAMInvoice(long accountId, float amount, Currency currency, AxisError... error) {
        shouldGetErrorsOnAMInvoice(Ints.checkedCast(accountId), amount, currency, error);
    }

    public void shouldGetErrorsOnAMInvoice(int accountID, float amount, Currency currency, AxisError... error) {
        shouldGetErrorsOnAMInvoice(error, new PaymentMap(jsonClient().getPackageName())
                .withAccountID(accountID)
                .withAmount(amount)
                .withCurrency(currency));
    }

    public void shouldGetErrorsOnAMInvoice(AxisError[] axisError, PaymentMap... payments) {
        requestHeader.setDirectFinanceOAUTH(Action.INVOICE);
        try {
            shouldGetMultipleErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(jsonClient().getPackageName())
                            .withAction(Action.INVOICE)
                            .withPayments(payments).getBean(),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    //region Deposit
    @Step("[AccountManagement.Deposit]")
    public <AccountManagementResponse> AccountManagementResponse deposit(boolean isYMOAUTH, PaymentMap... paymentMaps) {
        if (isYMOAUTH) {
            requestHeader.setYMFinanceOAUTH();
        } else {
            requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        }

        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean());
            assumeThat("ответ не содержит ошибок", JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void expectErrorOnAMDeposit(AxisError error, boolean isYMOAUTH, PaymentMap... paymentMaps) {
        if (isYMOAUTH) {
            requestHeader.setYMFinanceOAUTH();
        } else {
            requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        }

        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean());
            assertThat("ответ содержит правильную ошибку", JsonUtils.toString(response), ApiResponse.hasError(error));
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[AccountManagement.Deposit] in SANDBOX")
    public <AccountManagementResponse> AccountManagementResponse depositInSandbox(PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT, true);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean());
            assumeThat("ответ не содержит ошибок", JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public <AccountManagementResponse> AccountManagementResponse deposit(PaymentMap... paymentMaps) {
        return deposit(false, paymentMaps);
    }

    public void shouldGetErrorOnAMDeposit(AxisError axisError, PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        try {
            shouldGetErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean(),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorOnAMDeposit(AxisError axisError, boolean isYMOAUTH, PaymentMap... paymentMaps) {
        if (isYMOAUTH) {
            requestHeader.setYMFinanceOAUTH();
        } else {
            requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        }
        try {
            shouldGetErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean(),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorsOnAMDeposit(PaymentMap[] paymentMaps, AxisError... error) {
        requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        try {
            shouldGetMultipleErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean(),
                    error);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorsOnAMDeposit(PaymentMap[] paymentMaps, boolean isYMOAUTH, AxisError... error) {
        if (isYMOAUTH) {
            requestHeader.setYMFinanceOAUTH();
        } else {
            requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        }
        try {
            shouldGetMultipleErrorOn(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean(),
                    error);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetErrorsOnAMDeposit(AxisError[] errors, PaymentMap... paymentMaps) {
        requestHeader.setDirectFinanceOAUTH(Action.DEPOSIT);
        try {
            shouldGetMultipleErrorOn(
                    Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.DEPOSIT)
                            .withPayments(paymentMaps).getBean(),
                    errors);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    //region DepositByCard
    @Step("[AccountManagement.DepositByCard]")
    public <AccountManagementResponse> AccountManagementResponse depositByCard(
            String payMethodId, String customTransactionId, String version, PaymentMap... payments)
    {
        return defaultClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(defaultClient().getPackageName())
                        .withAction(Action.DEPOSIT_BY_CARD)
                        .withPayMethodId(payMethodId)
                        .withVersion(version)
                        .withCustomTransactionId(customTransactionId)
                        .withPayments(payments).getBean());
    }

    public void shouldGetErrorsOnAMDepositByCard(String payMethodId, String customTransactionId,
            PaymentMap payment, AxisError... errors)
    {
        shouldGetErrorsOnAMDepositByCard(payMethodId, customTransactionId, null, payment, errors);
    }

    public void shouldGetErrorsOnAMDepositByCard(String payMethodId, String customTransactionId, String version, PaymentMap payment, AxisError... errors)
    {
        shouldGetMultipleErrorOn(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(defaultClient().getPackageName())
                        .withAction(Action.DEPOSIT_BY_CARD)
                        .withPayMethodId(payMethodId)
                        .withVersion(version)
                        .withCustomTransactionId(customTransactionId)
                        .withPayments(payment).getBean(), errors);
    }
    //endregion

    //region CheckPayment
    @Step("[AccountManagement.CheckPayment]")
    public <AccountManagementResponse> AccountManagementResponse accountManagementCheckPayment(
            String customTransctionId)
    {
        return defaultClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(defaultClient().getPackageName())
                        .withAction(Action.CHECK_PAYMENT)
                        .withCustomTransactionId(customTransctionId).getBean());
    }

    public Callable<Boolean> accountManagementCheckPaymentSuccess(String customTransactionId) {
        return () -> accountManagementCheckPayment(customTransactionId).equals("Success");
    }
    //endregion

    //region Get
    @Step("[AccountManagement.Get]")
    public <AccountManagementResponse> AccountManagementResponse get(
            AccountSelectionCriteriaMap accountSelectionCriteriaMap)
    {
        return soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.GET)
                        .withSelectionCriteria(accountSelectionCriteriaMap).getBean());
    }


    public <AccountManagementResponse> AccountManagementResponse get(String[] logins, int[] accountIds) {
        return get(new AccountSelectionCriteriaMap(jsonClient().getPackageName())
                .withLogins(logins)
                .withAccountIDs(accountIds));
    }

    public int getAccountID() {
        try {
            Object account = getAccount(null);
            return (int) new BeanMap(account).get("accountID");
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка получения номера общего счета", e);
        }
    }

    public int getAccountID(String login) {
        try {
            Object account = getAccount(login);
            if (account == null) {
                throw new DirectAPIException("Отсутствуют данные об общем счете у пользователя " + login);
            }
            return (int) new BeanMap(account).get("accountID");
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка получения номера общего счета", e);
        }
    }

    public Money getAccountAmount(long accountId) {
        AccountSelectionCriteriaMap accountSelectionCriteriaMap =
                new AccountSelectionCriteriaMap(jsonClient().getPackageName())
                        .withLogins(null)
                        .withAccountIDs(Ints.checkedCast(accountId));
        return getAccountAmount(accountSelectionCriteriaMap);
    }

    /**
     * Получить кол-во средств на общем счету
     * <p>
     * Этот метод нужен для того, чтобы получать кол-во средств на общем счёту без мапинга на бины, в которых
     * тип поля amount -- float, что приводит к потере точности, например, (float)1078957.77 = 1078957.8
     * <p>
     * Внимание! Даже если под критерии в {@code accountSelectionCriteriaMap} подходит несколько счетов, вернётся
     * кол-во средств только для первого возвращённого AccountManagement.Get счёта.
     *
     * @param accountSelectionCriteriaMap критерии отбора счёта
     * @return текущий баланс общего счёта или {@code null} если не нашёлся ни один ОС попадающий по критерии
     */
    @Step("[AccountManagement.Get] получение кол-ва средств на общем счёту")
    public Money getAccountAmount(AccountSelectionCriteriaMap accountSelectionCriteriaMap) {
        Map<String, Object> accountManagementResponse;
        try {
            accountManagementResponse = jsonClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(jsonClient().getPackageName())
                            .withAction(Action.GET)
                            .withSelectionCriteria(accountSelectionCriteriaMap).getBean(),
                    Map.class
            );
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка вызова API AccountManagement.Get", e);
        }
        List<Object> accounts;
        try {
            accounts = (List) accountManagementResponse.get("Accounts");
        } catch (ClassCastException e) {
            throw new DirectAPIException("Ошибка разбора ответа от AccountManagement.Get", e);
        }

        assumeThat("Получены данные хотябы об одном общем счёте", accounts,
                allOf(notNullValue(), not(empty())));

        if (accounts.size() > 1) {
            log.info("Внимание! По указанным критериям нашлось больше одного общего счёта.");
        }
        String amount;
        try {
            amount = (String) ((Map) accounts.get(0)).get("Amount");
        } catch (ClassCastException | NullPointerException e) {
            throw new DirectAPIException("Ошибка разбора ответа от AccountManagement.Get", e);
        }
        Currency currency;
        try {
            currency = Currency.getFor((String) ((Map) accounts.get(0)).get("Currency"));
        } catch (ClassCastException | NullPointerException e) {
            throw new DirectAPIException("Ошибка разбора ответа от AccountManagement.Get", e);
        }
        return Money.valueOf(new BigDecimal(amount), currency);
    }

    public <Account> Account getAccount(String login) {
        try {
            Object accountManagementResponse = get(new String[]{login}, null);
            Object[] accounts = ((Object[]) new BeanMap(accountManagementResponse).get("accounts"));
            if (accounts.length > 0) {
                return (Account) accounts[0];
            } else {
                return null;
            }
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка получения номера общего счета", e);
        }
    }


    public <Account> Account getAccount(int accountID) {
        try {
            Object accountManagementResponse = get(null, new int[]{accountID});
            Object[] accounts = ((Object[]) new BeanMap(accountManagementResponse).get("accounts"));
            if (accounts.length > 0) {
                return (Account) accounts[0];
            } else {
                return null;
            }
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка получения номера общего счета", e);
        }
    }

    public <Account> Callable<Float> accountAmountIs(final Account account) {
        return accountAmountIs(account, properties.isDirectUseRealNotifyOrder());
    }

    public <Account> Callable<Float> accountAmountIs(final Account account, final boolean billingNotifications) {
        return new Callable<Float>() {
            public Float call() throws Exception {
                int accountID = (int) new BeanMap(account).get("accountID");
                if (!billingNotifications) {
                    UserSteps.getInstance().balanceSteps().synchronizeWithBalance(accountID);
                }
                Float amount = (Float) new BeanMap(getAccount(accountID)).get("amount");
                log.info("Got amount = " + amount);
                return amount;
            }
        };
    }

    public <Account> Callable<Boolean> accountAmountChanged(final Account account) {
        return accountAmountChanged(account, properties.isDirectUseRealNotifyOrder());
    }

    public <Account> Callable<Boolean> accountAmountChanged(final Account account, final boolean billingNotifications) {
        return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                int accountID = (int) new BeanMap(account).get("accountID");
                Float amountBefore = (Float) new BeanMap(account).get("amount");
                if (!billingNotifications) {
                    UserSteps.getInstance().balanceSteps().synchronizeWithBalance(accountID);
                }
                Float amount = (Float) new BeanMap(getAccount(accountID)).get("amount");
                return !amountBefore.equals(amount);
            }
        };
    }

    /**
     * @deprecated Метод неточный. Нужно использовать {@link FinanceSteps#accountAmountChangedOnSum(long, Money, Money)}
     * https://st.yandex-team.ru/DIRECT-65675
     */
    @Deprecated
    public <Account> Callable<Boolean> accountAmountChangedOnSum(final Account account, final Float sum) {
        return accountAmountChangedOnSum(account, sum, properties.isDirectUseRealNotifyOrder());
    }

    /**
     * @deprecated Метод неточный. Нужно использовать {@link FinanceSteps#accountAmountChangedOnSum(long, Money, Money, boolean)}
     * https://st.yandex-team.ru/DIRECT-65675
     */
    @Deprecated
    public <Account> Callable<Boolean> accountAmountChangedOnSum(
            final Account account, final Float sum, final boolean billingNotifications)
    {
        return () -> {
            int accountID = (int) new BeanMap(account).get("accountID");
            Float amountBefore = (Float) new BeanMap(account).get("amount");
            if (!billingNotifications) {
                UserSteps.getInstance().balanceSteps().synchronizeWithBalance(accountID);
            }
            Float amountAfter = (Float) new BeanMap(getAccount(accountID)).get("amount");
            return Money.valueOf(amountAfter).add(-amountBefore)
                    .setScale(2, RoundingMode.HALF_UP).floatValue().equals(sum);
        };
    }

    /**
     * Изменилась ли сумма на общем счёту {@code accountId} на указанное значение {@code expectedDelta}
     * <p>
     * Сравнение выполняется с точностью до 2-х знаков.
     *
     * @param accountId            идентификатор общего счёта
     * @param amountBefore         начальное состояние счёта, можно получить это значение
     *                             с помощью {@link FinanceSteps#getAccountAmount(long)}
     * @param expectedDelta        ожидаемое изменение счёта
     * @param billingNotifications ожидать ли реальных нотификаций от Баланса (false -- самим ходить
     *                             узнавать в Балансе состояние счёта)
     * @return {@code true} если текущая сумма на счёту изменилась на {@code expectedDelta} по сравнению с
     * начальным состоянием {@code amountBefore} с точностью до 2-х знаков
     */
    public Callable<Boolean> accountAmountChangedOnSum(
            long accountId, Money amountBefore, Money expectedDelta, boolean billingNotifications)
    {
        assumeThat("Общий счёт должен быть в той же валюте, что и ожидаемое изменение",
                amountBefore.getCurrency(), equalTo(expectedDelta.getCurrency()));
        Money scaledExpectedDelta = expectedDelta.setScale(2, RoundingMode.HALF_UP);
        return () -> {
            Money amount = syncAndGetAccountAmountChange(accountId, amountBefore, billingNotifications);
            log.info(String.format("Текущее кол-во средств на общем счету #%d: %s", accountId, amount.toString()));
            return amount.equals(scaledExpectedDelta);
        };
    }

    public Callable<Boolean> accountAmountChangedOnSum(long accountId, Money amountBefore, Money expectedDelta) {
        return accountAmountChangedOnSum(accountId, amountBefore, expectedDelta,
                properties.isDirectUseRealNotifyOrder());
    }

    /**
     * Вернуть разницу между текущим состоянием общего счёта {@code accountId} и начальным
     * состоянием {@code amountBefore}.
     *
     * @param accountId            идентификатор общего счёта
     * @param amountBefore         начальное состояние счёта, можно получить это значение
     *                             с помощью {@link FinanceSteps#getAccountAmount(long)}
     * @param billingNotifications ожидать ли реальных нотификаций от Баланса (false -- самим ходить
     *                             узнавать в Балансе состояние счёта)
     * @return изменение ОС, с точностью до 2-х знаков после запятой
     */
    public Money syncAndGetAccountAmountChange(long accountId, Money amountBefore, boolean billingNotifications) {
        return syncAndGetAccountAmount(accountId, billingNotifications).subtract(amountBefore)
                .setScale(2, RoundingMode.HALF_UP);
    }

    public Money syncAndGetAccountAmount(long accountId, boolean billingNotifications) {
        if (!billingNotifications) {
            UserSteps.getInstance().balanceSteps().synchronizeWithBalance(accountId);
        }
        return getAccountAmount(accountId);
    }

    /**
     * Метод используется для новых кошельков, у которых не было денег
     *
     * @param accountID
     * @param <T>
     * @return
     */
    public <T> Callable<Boolean> accountSumChanged(final int accountID) {
        return accountSumChanged(accountID, properties.isDirectUseRealNotifyOrder());
    }

    public <T> Callable<Boolean> accountSumChanged(final int accountID, final boolean billingNotifications) {
        return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                if (!billingNotifications) {
                    UserSteps.getInstance().balanceSteps().synchronizeWithBalance(accountID);
                }
                log.info("Wait sum > 0");
                return !Float.valueOf(0).equals(
                        new BeanMap(getAccount(accountID)).get(AccountMap.AMOUNT));
            }
        };
    }

    public void shouldGetErrorOnAccountManagementGet(String[] logins, int[] accountIds, AxisError expectedError) {
        shouldGetErrorOn(
                Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.GET)
                        .withSelectionCriteria(new AccountSelectionCriteriaMap(soapClient().getPackageName())
                                .withLogins(logins)
                                .withAccountIDs(accountIds)).getBean(),
                expectedError
        );
    }

    public void shouldGetErrorOnAccountManagementGet(String login, AxisError expectedError) {
        shouldGetErrorOnAccountManagementGet(new String[]{login}, null, expectedError);
    }

    public void shouldGetMultipleErrorOnAccountManagementGet(String[] logins, int[] accountIds,
            AxisError... axisError)
    {
        shouldGetMultipleErrorOn(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.GET)
                        .withSelectionCriteria(new AccountSelectionCriteriaMap(soapClient().getPackageName())
                                .withLogins(logins)
                                .withAccountIDs(accountIds)).getBean(),
                axisError);
    }


    public void shouldGetErrorsOnAccountManagementGet(String login, AxisError... expectedErrors) {
        shouldGetMultipleErrorOn(
                Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.GET)
                        .withSelectionCriteria(
                                new AccountSelectionCriteriaMap(soapClient().getPackageName())
                                        .withLogins(login)).getBean(),
                expectedErrors
        );
    }

    public void shouldGetErrorsOnAccountManagementGet(String login, AxisError expectedError) {
        shouldGetErrorsOnAccountManagementGet(new AccountManagementRequestMap(soapClient().getPackageName())
                .withAction(Action.GET)
                .withSelectionCriteria(
                        new AccountSelectionCriteriaMap(jsonClient().getPackageName())
                                .withLogins(login)
                ), expectedError);
    }

    public void shouldGetErrorsOnAccountManagementGet(long accountId, AxisError expectedError) {
        shouldGetErrorsOnAccountManagementGet(Ints.checkedCast(accountId), expectedError);
    }

    public void shouldGetErrorsOnAccountManagementGet(int accountID, AxisError expectedError) {
        shouldGetErrorsOnAccountManagementGet(
                new AccountManagementRequestMap(soapClient().getPackageName())
                        .withAction(Action.GET)
                        .withSelectionCriteria(
                                new AccountSelectionCriteriaMap(jsonClient().getPackageName())
                                        .withAccountIDs(accountID)
                        ), expectedError);
    }

    public void shouldGetErrorsOnAccountManagementGet(AccountManagementRequestMap request, AxisError expectedError) {
        shouldGetMultipleErrorOn(
                Method.ACCOUNT_MANAGEMENT,
                request.getBean(),
                expectedError);
    }


    //endregion

    //region Update
    @Step("[AccountManagement.Update]")
    public <AccountManagementResponse> AccountManagementResponse update(
            AccountMap... accountMaps)
    {
        return soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.UPDATE)
                        .withAccounts(accountMaps).getBean());
    }

    public void shouldGetErrorOnUpdateAccount(AxisError expectedError, AccountMap... accountMaps) {
        shouldGetErrorOn(
                Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.UPDATE)
                        .withAccounts(accountMaps).getBean(),
                expectedError
        );
    }

    public void shouldGetElementErrorOnUpdateAccount(AccountMap account, AxisError expectedError) {
        AccountManagementResponse expectedResponse = new AccountManagementResponse();
        AccountActionResult actionResult = new AccountActionResult();
        actionResult.setErrors(new Error[]{new Error(
                expectedError.getErrorCode(),
                expectedError.getMessage(),
                expectedError.getDetails())});
        expectedResponse.setActionsResult(new AccountActionResult[]{ actionResult });

        AccountManagementResponse response = update(account);
        assertThat("ответ совпадает с ожидаемым", response, beanDiffer(expectedResponse));
    }

    public AccountManagementResponse shouldGetResultOnUpdateAccount(AccountMap[] accountMaps,
            AccountManagementResponse expectedResponse)
    {
        AccountManagementResponse response = update(accountMaps);
        assertThat("ответ совпадает с ожидаемым", response, beanDiffer(expectedResponse));
        return response;
    }

    public <Account> Account updateAccountAndReturnUpdated(AccountMap account) {
        update(account);
        return getAccount((int) account.get("accountID"));
    }

    public <Account> Account setAccountMoneyIn(String login, String moneyIn) {
        AccountMap account = new AccountMap((Account) getAccount(login))
                .withSmsNotification(
                        new SmsNotificationInfoMap<>(jsonClient().getPackageName())
                                .withMoneyInSms(moneyIn));
        update(account);
        return getAccount(login);
    }

    public <Account> Account setAccountMoneyOut(String login, String moneyOut) {
        AccountMap account = new AccountMap((Account) getAccount(login))
                .withSmsNotification(
                        new SmsNotificationInfoMap<>(jsonClient().getPackageName())
                                .withMoneyOutSms(moneyOut));
        update(account);
        return getAccount(login);
    }

    public <Account> Account setAccountSmsMoney(String login, String moneyIn, String moneyOut) {
        AccountMap account = new AccountMap((Account) getAccount(login))
                .withSmsNotification(
                        new SmsNotificationInfoMap<>(jsonClient().getPackageName())
                                .withMoneyInSms(moneyIn)
                                .withMoneyOutSms(moneyOut));
        update(account);
        return getAccount(login);
    }

    public void setAccountDayBudgetAmountEqual0(Integer accountId)
    {
        soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                new AccountManagementRequestMap<>(soapClient().getPackageName())
                        .withAction(Action.UPDATE)
                        .withAccounts(
                                new AccountMap(soapClient().getPackageName())
                                        .withAccountID(accountId)
                                        .withAccountDayBudget(new AccountDayBudgetInfoMap(soapClient().getPackageName())
                                                .withAmount(0f)
                                                .withSpendMode(SpendMode.DEFAULT.getValue()))
                        ).getBean());
    }

    //endregion

    //region TransferMoney
    @Step("[AccountManagement.TransferMoney]")
    public <AccountManagementResponse> AccountManagementResponse transfer(TransferMap... transferMap) {
        requestHeader.setDirectFinanceOAUTH(Action.TRANSFER_MONEY);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.TRANSFER_MONEY)
                            .withTransfers(transferMap).getBean());
            assumeThat("ответ не содержит ошибок",
                    JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    @Step("[AccountManagement.Transfer] in SANDBOX")
    public <AccountManagementResponse> AccountManagementResponse transferInSandbox(TransferMap... transferMap) {
        requestHeader.setDirectFinanceOAUTH(Action.TRANSFER_MONEY, true);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.TRANSFER_MONEY)
                            .withTransfers(transferMap).getBean());
            assumeThat("ответ не содержит ошибок",
                    JsonUtils.toString(response), ApiResponse.hasNoError());
            return response;
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public <AccountManagementResponse> AccountManagementResponse transfer(
            int fromAccountID, int toAccountID, float amount, String currency)
    {
        return transfer(
                new TransferMap(soapClient().getPackageName())
                        .withFromAccountID(fromAccountID)
                        .withToAccountID(toAccountID)
                        .withAmount(amount)
                        .withCurrency(currency)
        );
    }

    public int getTransferResultAccountID(int fromAccountID, int toAccountID, float amount, String currency) {
        try {
            Object accountManagementResponse = transfer(fromAccountID, toAccountID, amount, currency);
            Object[] actionResult = (Object[]) new BeanMap(accountManagementResponse).get("actionResult");

            return (int) new BeanMap(actionResult[0]).get(AccountMap.ACCOUNT_ID);
        } catch (AxisError e) {
            throw new DirectAPIException("Ошибка перевода средств");
        }
    }

    public void shouldGetErrorOnTransfer(
            int fromAccountID, int toAccountID, float amount, String currency, AxisError error)
    {
        shouldGetErrorOnTransfer(error,
                new TransferMap(soapClient().getPackageName())
                        .withFromAccountID(fromAccountID)
                        .withToAccountID(toAccountID)
                        .withAmount(amount)
                        .withCurrency(currency));
    }

    public void expectErrorOnTransfer(int fromAccountID, int toAccountID, float amount, String currency,
            AxisError error)
    {
        expectErrorOnTransfer(error,
                new TransferMap(soapClient().getPackageName())
                        .withFromAccountID(fromAccountID)
                        .withToAccountID(toAccountID)
                        .withAmount(amount)
                        .withCurrency(currency));
    }

    public void shouldGetErrorOnTransfer(AxisError axisError, TransferMap... transfers) {
        requestHeader.setDirectFinanceOAUTH(Action.TRANSFER_MONEY);
        try {
            shouldGetErrorOn(
                    Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.TRANSFER_MONEY)
                            .withTransfers(transfers),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void expectErrorOnTransfer(AxisError axisError, TransferMap... transfers) {
        requestHeader.setDirectFinanceOAUTH(Action.TRANSFER_MONEY);
        try {
            AccountManagementResponse response = soapClient().invokeMethod(Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.TRANSFER_MONEY)
                            .withTransfers(transfers).getBean());
            assertThat("ответ содержит правильную ошибку",
                    JsonUtils.toString(response), ApiResponse.hasError(axisError));
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }

    public void shouldGetMultipleErrorOnTransfer(int fromAccountID,
            int toAccountID,
            float amount,
            String currency,
            AxisError... errors)
    {
        shouldGetMultipleErrorOnTransfer(
                new TransferMap(soapClient().getPackageName())
                        .withFromAccountID(fromAccountID)
                        .withToAccountID(toAccountID)
                        .withAmount(amount)
                        .withCurrency(currency), errors);
    }

    public void shouldGetMultipleErrorOnTransfer(TransferMap transferMap, AxisError... axisError) {
        requestHeader.setDirectFinanceOAUTH(Action.TRANSFER_MONEY);
        try {
            shouldGetMultipleErrorOn(
                    Method.ACCOUNT_MANAGEMENT,
                    new AccountManagementRequestMap<>(soapClient().getPackageName())
                            .withAction(Action.TRANSFER_MONEY)
                            .withTransfers(transferMap).getBean(),
                    axisError);
        } finally {
            requestHeader.clearFinanceOAUTH();
        }
    }
    //endregion

    public void setYMFinanceOAUTH() {
        requestHeader.setYMFinanceOAUTH();
    }

    public void setDirectFinanceOAUTH(Method method) {
        requestHeader.setDirectFinanceOAUTH(method);
    }

    public void setDirectFinanceOAUTH(Action action) {
        requestHeader.setDirectFinanceOAUTH(action);
    }

    public void clearFinanceOAUTH() {
        requestHeader.clearFinanceOAUTH();
    }

}
