Яндекс Касса на ASP NET MVC 5

25 Июля 2016

Предисловие

Когда начал подключать Яндекс Кассу (можно Яндекс Деньги) к сайту  столкнулся с отсутствием примеров реализаций. На оффициальном сайте есть примеры подключения к различным CMS, есть примеры для PHP, для ASP NET дельной информации никакой. В интеренете нашел только один гайд, который пару раз разкопировали по интернету. Ништяк - подумал я, Но не тут-то было. Версия Яндекс Кассы апнулась и пример перестал быть актуальным. Намучался я подключать Кассу, основываясь только на принципе работы, поэтому и пишу этот пример, дабы облегчить кому-то жизнь.

Принци работы Кассы

Напишу еще как работает внутрення логика на примере банковской карточки. В общих чертах.

  1. Человек нажимает кнопку купить у товара или услуги
  2. Пользователь попадает на страничку оплаты на сайте Яндекс Денег
  3. Вводит данные своей карточки и нажимает оплатить
  4. Яндекс Касса отправляет на сайт запрос по Url, указанному в настройках (для CheckOrder и  PaymentAviso). Подробнее об этом ниже.
  5. Если вернулся положительный результат от запросов, снимаются деньги у клиента. Подчеркну, деньги снимаются именно в этот момент, когда результат от сайта был положительный
  6. Пользователя перекидывает на указанный Url или выводится сообщение об успешной транзакции на сайте Яндекса, в зависимости от настроек в личном кабинете

CheckOrder и  PaymentAviso. Вся соль

В целом все просто. Вся сложность заключается в получении этих запросов от Яндекса и возврате определенного ответа.

Запросы представляют из себя объекты с параметрами, по объекту на запрос. Приходит не в Xml, это важно.

CheckOrder - запрос, как понятно из названия, сверки заказа. Чтобы пользователь не поменял вручную стоимость заказа и не купил за рубль машину. Вы должны проверить правильность заказа и вернуть: "верно" или "не верно".

PaymentAviso - запрос проверяющий получил ли пользователь свой товар или услугу.

Вернуть ответ на оба запроса нужно объектом, в формате Xml. Именно Xml!

В идеале нужно создать Api контроллер который будет возвращать объект. Но Api возвращает в том формате в котором получает. Тоесть он вернет Xml только если к нему обратится с Xml. Можно в конфиге сделать чтобы Api всегда возвращал Xml. Но у меня в проекте есть другие Api контроллеры, которые должны возвращать не Xml. Если у Вас нет такой необходимости - можете использовать Api. В моем примере я буду возвращать Xml через обычный контроллер.

Сам пример

Форма товара или услуги:


 

Контроллер для проверки CheckOrder и PaymentAviso

[HttpPost]
public async Task Check(orderPost model)
{
    model.action = Request["action"]; //иначе возьмет название Action. В данном случае "Check", а нам нужно взять значение из поста.
    if(model.action == "checkOrder"){

    var stringBegin = model.action + ";" + model.orderSumAmount.ToString() + ";" + model.orderSumCurrencyPaycash.ToString() + ";" + model.orderSumBankPaycash.ToString() +  ";"
        + model.shopId.ToString() + ";" + model.invoiceId.ToString() + ";" + model.customerNumber.ToString() + ";Secred__Code";
    // Стринга для сравнения md5 присланного и сгенерированного
    // Secred__Code в настройках Яндекс Кассы

    var result = new checkOrderResponse
    {
        performedDatetime = DateTime.Now,
        invoiceId = model.invoiceId,
        shopId = model.shopId
    };
    // Подготовка ответа

    if (CalculateMD5Hash(stringBegin) == model.md5)
    {

        var product = db.Product.FirstOrDefault(a => a.Id == user.FormProduct);
        // Сравнить цену товара или услуги со значением из БД

        if (product .Price != model.orderSumAmount)
        {
            var resultErrorTemp = new checkOrderResponse
            {
                performedDatetime = DateTime.Now,
                code = 100,
                invoiceId = model.invoiceId,
                shopId = model.shopId,
                message = "action is wrong"
            };
            // Отказ от оплаты. Цена не совпадает

            return Content(resultErrorTemp.SerializeToXml(), "application/xml");
        }

        var orderNew = new Order
        {
            // Запись в БД заказа
        };
        db.Orders.Add(orderNew);

        await db.SaveChangesAsync();

        result.code = 0;
        // Успешно
    }
    else
    {
        result.code = 1;
        result.message = "Hash error";
        result.techMessage = "Hash error";
        // md5 не совпало
    }

    return Content(result.SerializeToXml(), "application/xml");
    }
    else if (model.action == "paymentAviso")
    {
        var result = new paymentAvisoResponse
        {
            performedDatetime = DateTime.Now,
            code = 0,
            invoiceId = model.invoiceId,
            shopId = model.shopId
        };
        // Подготовка объекта для ответа

        var user = db.Users.FirstOrDefault(a => a.Email == model.customerNumber);
        var order = db.Orders.FirstOrDefault(a => a.UserId == user.Id);

        if (order == null)
        {
            result.code = 100;
            // Нет записи в БД. Товар или услуга не проданы
        }


        return Content(result.SerializeToXml(), "application/xml");
    }

    var resultError = new checkOrderResponse
    {
        performedDatetime = DateTime.Now,
        code = 100,
        invoiceId = model.invoiceId,
        shopId = model.shopId,
        message = "action is wrong"
    };

    // Если левый запрос
    return Content(resultError.SerializeToXml(), "application/xml");
}

Генерирование md5

public string CalculateMD5Hash(string input)
{

    MD5 md5 = System.Security.Cryptography.MD5.Create();
    byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
    byte[] hash = md5.ComputeHash(inputBytes);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < hash.Length; i++)
    {
        sb.Append(hash[i].ToString("X2"));
    }
    return sb.ToString();
}

В принципе и все. Если будут вопросы - пишите!