From 9f7a22587561e81b877e9dd892380403bb3b692b Mon Sep 17 00:00:00 2001 From: Denis Mosolov Date: Sat, 20 Jun 2020 16:42:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=B8=D0=BC=D0=B8=D1=82=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 + composer.lock | 226 +++++++++----- phpunit.xml | 2 + src/Application.php | 2 + src/InvalidPriceException.php | 9 + src/Price.php | 71 +++++ src/Reply/LimitOrderBuyStock.php | 318 ++++++++++++++++++++ src/Reply/Repeat.php | 5 + tests/PriceTest.php | 67 +++++ tests/Reply/LimitOrderBuyStockTest.php | 398 +++++++++++++++++++++++++ 10 files changed, 1028 insertions(+), 80 deletions(-) create mode 100644 src/InvalidPriceException.php create mode 100644 src/Price.php create mode 100644 src/Reply/LimitOrderBuyStock.php create mode 100644 tests/PriceTest.php create mode 100644 tests/Reply/LimitOrderBuyStockTest.php diff --git a/README.md b/README.md index fd9aef3..ee818aa 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,16 @@ ID: my.stocks Если вы подтвердите намерение, то услышите: «заявка на продажу создана» либо «заявка исполнена». +### Лимитная заявка на покупку акций + +Чтобы отправить лимитную заявку на покупку акций по заданной цене скажите: «купи 10 лотов НЛМК по цене 120 рублей 30 копеек». + +После этого Оливер попросит подтвердить заявку: «заявка на покупку 10 лотов НЛМК, тикер NLMK, по цене 120 рублей 30 копеек за акцию, сумма сделки 1203 рубля плюс комиссия брокера, для подтверждения скажите подтверждаю». + +Если вы подтвержите намерение, то услышите «заявка на покупку 10 лотов НЛМК по цене 120 рублей 30 копеек создана». + +[Видео](https://youtu.be/EjlN4JI23B0) + ### Мои активные заявки Чтобы узнать информацию об активных заявках, скажите: «мои заявки». diff --git a/composer.lock b/composer.lock index 05878b8..f100eee 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/denismosolov/NumToText.git", - "reference": "477b386e60c03aab096f9dc42b1489b5139cf17a" + "reference": "5504673d33806cfc72dd9a0cf5414c7c1505ef9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/denismosolov/NumToText/zipball/477b386e60c03aab096f9dc42b1489b5139cf17a", - "reference": "477b386e60c03aab096f9dc42b1489b5139cf17a", + "url": "https://api.github.com/repos/denismosolov/NumToText/zipball/5504673d33806cfc72dd9a0cf5414c7c1505ef9f", + "reference": "5504673d33806cfc72dd9a0cf5414c7c1505ef9f", "shasum": "" }, "require": { @@ -40,6 +40,14 @@ { "name": "Aleksejs Ivanovs", "email": "ivanovs.aleksejs@gmail.com" + }, + { + "name": "Denis Mosolov", + "email": "denismosolov@gmail.com" + }, + { + "name": "Viesturs Kavacs", + "email": "kavackys@gmail.com" } ], "description": "Converts numbers to text representation in various languges.", @@ -53,7 +61,7 @@ "support": { "source": "https://github.com/denismosolov/NumToText/tree/master" }, - "time": "2020-06-13T20:56:48+00:00" + "time": "2020-06-19T08:33:21+00:00" }, { "name": "james.rus52/tinkoffinvest", @@ -543,16 +551,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "reference": "30441f2752e493c639526b215ed81d54f369d693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30441f2752e493c639526b215ed81d54f369d693", + "reference": "30441f2752e493c639526b215ed81d54f369d693", "shasum": "" }, "require": { @@ -566,7 +574,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -585,7 +593,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2020-06-19T20:22:09+00:00" }, { "name": "phpoption/phpoption", @@ -787,16 +795,16 @@ }, { "name": "phpunit/php-file-iterator", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4" + "reference": "eba15e538f2bb3fe018b7bbb47d2fe32d404bfd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/eba15e538f2bb3fe018b7bbb47d2fe32d404bfd2", + "reference": "eba15e538f2bb3fe018b7bbb47d2fe32d404bfd2", "shasum": "" }, "require": { @@ -839,20 +847,20 @@ "type": "github" } ], - "time": "2020-04-18T05:02:12+00:00" + "time": "2020-06-15T12:54:35+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a" + "reference": "62f696ad0d140e0e513e69eaafdebb674d622b4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7579d5a1ba7f3ac11c80004d205877911315ae7a", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/62f696ad0d140e0e513e69eaafdebb674d622b4c", + "reference": "62f696ad0d140e0e513e69eaafdebb674d622b4c", "shasum": "" }, "require": { @@ -892,25 +900,34 @@ "keywords": [ "process" ], - "time": "2020-02-07T06:06:11+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:10:07+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346" + "reference": "0c69cbf965d5317ba33f24a352539f354a25db09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/526dc996cc0ebdfa428cd2dfccd79b7b53fee346", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c69cbf965d5317ba33f24a352539f354a25db09", + "reference": "0c69cbf965d5317ba33f24a352539f354a25db09", "shasum": "" }, "require": { "php": "^7.3" }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, "type": "library", "extra": { "branch-alias": { @@ -938,7 +955,13 @@ "keywords": [ "template" ], - "time": "2020-02-01T07:43:44+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T12:52:43+00:00" }, { "name": "phpunit/php-timer", @@ -997,16 +1020,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c" + "reference": "e61c593e9734b47ef462340c24fca8d6a57da14e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e61c593e9734b47ef462340c24fca8d6a57da14e", + "reference": "e61c593e9734b47ef462340c24fca8d6a57da14e", "shasum": "" }, "require": { @@ -1048,7 +1071,7 @@ "type": "github" } ], - "time": "2020-05-06T09:56:31+00:00" + "time": "2020-06-16T07:00:44+00:00" }, { "name": "phpunit/phpunit", @@ -1150,16 +1173,16 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.2", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5" + "reference": "d650ef9b1fece15ed4d6eaed6e6b469b7b81183a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/d650ef9b1fece15ed4d6eaed6e6b469b7b81183a", + "reference": "d650ef9b1fece15ed4d6eaed6e6b469b7b81183a", "shasum": "" }, "require": { @@ -1198,20 +1221,20 @@ "type": "github" } ], - "time": "2020-04-30T05:58:10+00:00" + "time": "2020-06-15T13:11:26+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e" + "reference": "c771130f0e8669104a4320b7101a81c2cc2963ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5b5dbe0044085ac41df47e79d34911a15b96d82e", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c771130f0e8669104a4320b7101a81c2cc2963ef", + "reference": "c771130f0e8669104a4320b7101a81c2cc2963ef", "shasum": "" }, "require": { @@ -1243,20 +1266,26 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2020-02-07T06:20:13+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T12:56:39+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8" + "reference": "266d85ef789da8c41f06af4093c43e9798af2784" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85b3435da967696ed618ff745f32be3ff4a2b8e8", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/266d85ef789da8c41f06af4093c43e9798af2784", + "reference": "266d85ef789da8c41f06af4093c43e9798af2784", "shasum": "" }, "require": { @@ -1307,7 +1336,13 @@ "compare", "equality" ], - "time": "2020-02-07T06:08:51+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T15:04:48+00:00" }, { "name": "sebastian/diff", @@ -1373,16 +1408,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c" + "reference": "16eb0fa43e29c33d7f2117ed23072e26fc5ab34e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c753f04d68cd489b6973cf9b4e505e191af3b05c", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/16eb0fa43e29c33d7f2117ed23072e26fc5ab34e", + "reference": "16eb0fa43e29c33d7f2117ed23072e26fc5ab34e", "shasum": "" }, "require": { @@ -1428,20 +1463,20 @@ "type": "github" } ], - "time": "2020-04-14T13:36:52+00:00" + "time": "2020-06-15T13:00:01+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "80c26562e964016538f832f305b2286e1ec29566" + "reference": "d12fbca85da932d01d941b59e4b71a0d559db091" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/80c26562e964016538f832f305b2286e1ec29566", - "reference": "80c26562e964016538f832f305b2286e1ec29566", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d12fbca85da932d01d941b59e4b71a0d559db091", + "reference": "d12fbca85da932d01d941b59e4b71a0d559db091", "shasum": "" }, "require": { @@ -1495,7 +1530,13 @@ "export", "exporter" ], - "time": "2020-02-07T06:10:52+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:12:44+00:00" }, { "name": "sebastian/global-state", @@ -1553,16 +1594,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67516b175550abad905dc952f43285957ef4363" + "reference": "15f319d67c49fc55ebcdbffb3377433125588455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67516b175550abad905dc952f43285957ef4363", - "reference": "e67516b175550abad905dc952f43285957ef4363", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/15f319d67c49fc55ebcdbffb3377433125588455", + "reference": "15f319d67c49fc55ebcdbffb3377433125588455", "shasum": "" }, "require": { @@ -1596,20 +1637,26 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2020-02-07T06:12:23+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:15:25+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7" + "reference": "14e04b3c25b821cc0702d4837803fe497680b062" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/14e04b3c25b821cc0702d4837803fe497680b062", + "reference": "14e04b3c25b821cc0702d4837803fe497680b062", "shasum": "" }, "require": { @@ -1641,20 +1688,26 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2020-02-07T06:19:40+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:08:02+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579" + "reference": "a32789e5f0157c10cf216ce6c5136db12a12b847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cdd86616411fc3062368b720b0425de10bd3d579", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/a32789e5f0157c10cf216ce6c5136db12a12b847", + "reference": "a32789e5f0157c10cf216ce6c5136db12a12b847", "shasum": "" }, "require": { @@ -1694,20 +1747,26 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2020-02-07T06:18:20+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:06:44+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98" + "reference": "71421c1745788de4facae1b79af923650bd3ec15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/71421c1745788de4facae1b79af923650bd3ec15", + "reference": "71421c1745788de4facae1b79af923650bd3ec15", "shasum": "" }, "require": { @@ -1739,7 +1798,13 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2020-02-07T06:13:02+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-15T13:17:14+00:00" }, { "name": "sebastian/type", @@ -2226,16 +2291,16 @@ }, { "name": "webmozart/assert", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" + "reference": "9dc4f203e36f2b486149058bade43c851dd97451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", + "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", + "reference": "9dc4f203e36f2b486149058bade43c851dd97451", "shasum": "" }, "require": { @@ -2243,6 +2308,7 @@ "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "phpstan/phpstan": "<0.12.20", "vimeo/psalm": "<3.9.1" }, "require-dev": { @@ -2270,7 +2336,7 @@ "check", "validate" ], - "time": "2020-04-18T12:12:48+00:00" + "time": "2020-06-16T10:16:42+00:00" } ], "aliases": [], diff --git a/phpunit.xml b/phpunit.xml index c0e5159..30d04f6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,7 @@ tests/Reply/IntroductionTest.php tests/Reply/RepeatTest.php + tests/Reply/LimitOrderBuyStockTest.php tests/Reply/OrdersTest.php tests/Reply/StocksTest.php tests/Reply/MarketOrderBuyStockTest.php @@ -11,6 +12,7 @@ tests/DeclensionTest.php + tests/PriceTest.php \ No newline at end of file diff --git a/src/Application.php b/src/Application.php index c96aa60..c6f6917 100644 --- a/src/Application.php +++ b/src/Application.php @@ -8,6 +8,7 @@ use jamesRUS52\TinkoffInvest\TIClient; use Oliver\Reply\ICanDo; use Oliver\Reply\Introduction; +use Oliver\Reply\LimitOrderBuyStock; use Oliver\Reply\Orders; use Oliver\Reply\Repeat; use Oliver\Reply\MarketOrderBuyStock; @@ -72,6 +73,7 @@ public function run(): array new Orders($this->client), new MarketOrderBuyStock($this->client), new MarketOrderSellStock($this->client), + new LimitOrderBuyStock($this->client), ]; foreach ($replies as $reply) { $response = $reply->handle($this->event); diff --git a/src/InvalidPriceException.php b/src/InvalidPriceException.php new file mode 100644 index 0000000..b969256 --- /dev/null +++ b/src/InvalidPriceException.php @@ -0,0 +1,9 @@ +currency = $currency; + } + + /** + * Переводит рубли и копейки в float. + * В случае некорректных данных выбрасывает исключение, например: + * 120 рублей 50 копеек - 120.5 + * 120 рублей - 120.0 + * 50 копеек - 0.5 + * 0.5 копейки - 0.05 + * 120 рублей 120 копеек - исключение + * 120 рублей 10 центов - исключение + * 50 копеек 120 рублей - исключение + * 120 лей - исключение + * + * @return float + * @throws Oliver\TIException + */ + public function concat(float $price1, string $currency1, float $price2 = 0.0, string $currency2 = '') + { + if ($price2 === 0.0 && $currency2 === '') { + // ожидаются рубли или копейки + if (in_array($currency1, ['рубль', 'рубля', 'рублей'])) { + // считать допустимым 120.5 рублей + if ($price1 > 0) { + return $price1; + } else { + throw new InvalidPriceException('не понимаю, похоже на отрицательную цену, используйте рубли и копейки, например сто рублей десять копеек.'); + } + } elseif (in_array($currency1, ['копейка', 'копейки', 'копеек'])) { + // не может быть больше 99 копеек + if ($price1 > 0 && $price1 < 100.0) { + return $price1 * 0.01; + } else { + throw new InvalidPriceException('не понимаю цену, не доложно быть больше девяносто девяти копеек, например, сто рублей десять копеек.'); + } + } else { + // @todo: rework + throw new InvalidPriceException('неправильная валюта, используйте рубли и копейки, например сто рублей десять копеек.'); + } + } else { + if ( + in_array($currency1, ['рубль', 'рубля', 'рублей']) && + in_array($currency2, ['копейка', 'копейки', 'копеек']) + ) { + if ($price2 > 0 && $price2 < 100.0) { + return intval($price1) + $price2 * 0.01; + } else { + throw new InvalidPriceException('не понимаю цену, не доложно быть больше девяносто девяти копеек, например, сто рублей десять копеек.'); + } + } else { + throw new InvalidPriceException('не понимаю цену, сначала рубли затем копейки, например, сто рублей десять копеек.'); + } + } + throw new InvalidPriceException('не понимаю цену, пример, сто рублей десять копеек.'); + } +} diff --git a/src/Reply/LimitOrderBuyStock.php b/src/Reply/LimitOrderBuyStock.php new file mode 100644 index 0000000..2aaf36e --- /dev/null +++ b/src/Reply/LimitOrderBuyStock.php @@ -0,0 +1,318 @@ +client = $client; + } + + public function handle(array $event): array + { + $confirmed = isset($event['request']['nlu']['intents']['YANDEX.CONFIRM']) && + $event['request']['nlu']['intents']['YANDEX.CONFIRM']; + $rejected = isset($event['request']['nlu']['intents']['YANDEX.REJECT']) && + $event['request']['nlu']['intents']['YANDEX.REJECT']; + + if ($this->userPlacesOrder($event)) { + return $this->askConfirmation($event); + } elseif ($confirmed && $this->userReplies($event)) { + return $this->createLimitOrder($event); + } elseif ($rejected && $this->userReplies($event)) { + return $this->rejected(); + } elseif (! $confirmed && ! $rejected && $this->userReplies($event)) { + return $this->askConfirmation($event); + } else { + // @todo: hint + return []; + } + } + + /** + * Создаёт лимитную заявку на покупку акций и сообщает результат пользователю + */ + private function createLimitOrder(array $event): array + { + $text = ''; + try { + // TIOrder + $order = $this->client->sendOrder( + $event['state']['session']['order_details']['figi'], + $event['state']['session']['order_details']['requestedLots'], + TIOperationEnum::BUY, + $event['state']['session']['order_details']['price'], + ); + switch ($order->getStatus()) { + // [ New, PartiallyFill, Fill, Cancelled, Replaced, PendingCancel, Rejected, PendingReplace, PendingNew ] + case 'New': + $text = 'лимитная заявка на покупку создана,'; + break; + case 'PendingNew': + $text = 'лимитная заявка на покупку отправлена,'; + break; + case 'Rejected': + $text = 'лимитная заявка на покупку отклонена системой,'; + // ОШИБКА: (579) Для выбранного финансового инструмента цена должна быть не меньше 126.02 + print $order->getRejectReason() . "\n"; + print $order->getMessage() . "\n"; + if ( + $order->getRejectReason() === 'Unknown' && + preg_match('/ОШИБКА:\s+\(\d+\)/', $order->getMessage()) + ) { + $parts = false; + $parts = preg_split('/ОШИБКА:\s+\(\d+\)/', $order->getMessage()); + if (is_array($parts)) { + $text .= end($parts); + } else { + // @todo: ???? + } + } + // @todo: Specified security is not found [...] + break; + default: + // @todo: add test case + print $order->getStatus() . "\n"; + $text = 'произошло что-то непонятное, проверьте свои заявки и акции,'; + break; + } + } catch (TIException $te) { + print $te->getMessage() . "\n"; + // Недостаточно активов для сделки [OrderNotAvailable] + if (preg_match('/\[OrderNotAvailable\]/', $te->getMessage())) { + $text = preg_replace('/\[OrderNotAvailable\]/', '', $te->getMessage()); + if (is_null($text)) { + // @todo: ???? + } + } elseif (preg_match('/\[VALIDATION_ERROR\]/', $te->getMessage())) { + if (preg_match('/has invalid scale/', $te->getMessage())) { + $text .= 'недопустимый шаг цены, узнайте минимальный шаг цены для этого инструмента на бирже,'; + } + } else { + $text = 'ошибка при взаимодействии с биржей, попробуйте создать лимитную заявку позже,'; + } + } + + return [ + 'session_state' => [ + 'text' => $text, + 'context' => [], + ], + 'response' => [ + 'text' => $text, + 'tts' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } + + /** + * Сообщает пользователю детали заявки, которые удалось узнать + * из запроса пользователя, затем предлагает подтвердить + * или отменить заявку. + */ + private function askConfirmation(array $event): array + { + $price1 = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['price1']['value'] ?? 0.0; + $currency1 = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['currency1']['value'] ?? ''; + $price2 = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['price2']['value'] ?? 0.0; + $currency2 = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['currency2']['value'] ?? ''; + $figi = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['figi']['value'] ?? ''; + $lots = $event['request']['nlu']['intents']['limit.order.buy.stock']['slots']['requestedLots']['value'] ?? 0; + + $stockPrice = new StockPrice('RUB'); + try { + $price = $stockPrice->concat($price1, $currency1, $price2, $currency2); + } catch (InvalidPriceException $iv) { + return [ + 'session_state' => [ + 'text' => $iv->getMessage(), // @todo: do not use getMessage + 'context' => [ + 'limit_order_buy_stock', + ], + 'order_details' => [ + 'price' => $price, + 'figi' => $figi, + 'requestedLots' => $lots, + ], + ], + 'response' => [ + 'text' => $iv->getMessage(), + 'tts' => $iv->getMessage(), + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } + + $valid = $lots > 0; + if ($valid) { + $instrument = $this->client->getInstrumentByFigi($figi); + // @todo: check minPriceIncrement + $text = sprintf('лимитная заявка на покупку %s,', $instrument->getName()); + $text .= sprintf('тикер: %s,', $instrument->getTicker()); + $text .= sprintf('цена за акцию: %s,', Price::toText($price, [['рублей', 'рубль', 'рубля'], ['копеек', 'копейка', 'копейки']], 'RU')); + $text .= sprintf('количество лотов: %s,', Num::toText($lots, 'RU')); + // @todo: сколько акций в одном лоте + $text .= sprintf('сумма заявки: %s плюс комиссия брокера,', Price::toText($price * $lots * $instrument->getLot(), [['рублей', 'рубль', 'рубля'], ['копеек', 'копейка', 'копейки']], 'RU')); + $text .= 'для подтверждения заявки скажите да, для отмены скажите нет,'; + return [ + 'session_state' => [ + 'text' => $text, + 'context' => [ + 'limit_order_buy_stock', + ], + 'order_details' => [ + 'price' => $price, + 'figi' => $figi, + 'requestedLots' => $lots, + ], + ], + 'response' => [ + 'text' => $text, + 'tts' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } else { + // price 120.5 + // currency копеек копеек + // lot -2, 10.5 + // @todo: ??????????????????? + $text = ''; + return [ + 'session_state' => [ + 'text' => $text, + 'context' => [ + 'limit_order_buy_stock', + ], + 'order_details' => [], + ], + 'response' => [ + 'text' => $text, + 'tts' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } + } + + private function rejected(): array + { + $text = 'заявка отменена'; + return [ + 'session_state' => [ + 'text' => $text, + 'context' => [], + 'order_details' => [], + ], + 'response' => [ + 'text' => $text, + 'tts' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } + + /** + * Пользователь ответил что-то невнятное + * Просим подтвердить заявку, либо отменить + */ + private function hint(array $event): array + { + $text = 'чтобы создать заявку скажите подтверждаю или скажите отмена'; + $context = []; + if (isset($event['state']['session']['context'])) { + $context = $event['state']['session']['context']; + } + $details = []; + if (isset($event['state']['session']['order_details'])) { + $details = $event['state']['session']['order_details']; + } + return [ + 'session_state' => [ + 'text' => $text, + 'context' => $context, + 'order_details' => $details, + ], + 'response' => [ + 'text' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + return [ + 'session_state' => [ + 'text' => $text, + 'context' => [], + 'order_details' => [], + ], + 'response' => [ + 'text' => $text, + 'tts' => $text, + 'end_session' => false, + ], + 'version' => '1.0', + ]; + } + + /** + * Пользователь намеревается создать лимитную заявку + */ + private function userPlacesOrder(array $event): bool + { + return isset($event['request']['nlu']['intents']['limit.order.buy.stock']); + } + + /** + * Пользователь ответил на предложение подтвердить или отменить лимитную заявку + * @throws \Exception + */ + private function userReplies(array $event): bool + { + $context = isset($event['state']['session']['context']) && + is_array($event['state']['session']['context']) && + in_array('limit_order_buy_stock', $event['state']['session']['context']); + // $valid = isset($event['state']['session']['order_details']['figi']) && + // isset($event['state']['session']['order_details']['requestedLots']) && + // isset($event['state']['session']['order_details']['price']); + // if (! $valid) { + // // @todo: залогируй session + // print_r($event['state']['session']); + // throw new \Exception('Невалидные данные в order_details.'); + // } + return $context; + } +} diff --git a/src/Reply/Repeat.php b/src/Reply/Repeat.php index ee849f9..315a90a 100644 --- a/src/Reply/Repeat.php +++ b/src/Reply/Repeat.php @@ -19,10 +19,15 @@ public function handle(array $event): array if (isset($event['state']['session']['context'])) { $context = $event['state']['session']['context']; } + $details = []; + if (isset($event['state']['session']['order_details'])) { + $details = $event['state']['session']['order_details']; + } return [ 'session_state' => [ 'text' => $text, 'context' => $context, + 'order_details' => $details, ], 'response' => [ 'text' => $text, diff --git a/tests/PriceTest.php b/tests/PriceTest.php new file mode 100644 index 0000000..5f753d0 --- /dev/null +++ b/tests/PriceTest.php @@ -0,0 +1,67 @@ +expectException(InvalidPriceException::class); + $this->expectExceptionMessage( + 'неправильная валюта, используйте рубли и копейки, например сто рублей десять копеек.' + ); + $instance = new Price('RUB'); + $instance->concat(1, 'акция'); + } + + public function testInvalidPriceException2(): void + { + $this->expectException(InvalidPriceException::class); + $this->expectExceptionMessage( + 'не понимаю цену, не доложно быть больше девяносто девяти копеек, например, сто рублей десять копеек.' + ); + $instance = new Price('RUB'); + $instance->concat(120.0, 'рублей', 120.0, 'копеек'); + } + + public function testCurrencyOrder(): void + { + $this->expectException(InvalidPriceException::class); + $this->expectExceptionMessage( + 'не понимаю цену, сначала рубли затем копейки, например, сто рублей десять копеек.' + ); + $instance = new Price('RUB'); + $instance->concat(50.0, 'копеек', 120.0, 'рублей'); + } + + public function testCurrency1(): void + { + $instance = new Price('RUB'); + $this->assertEquals( + 120.0, + $instance->concat(120.0, 'рубль') + ); + $this->assertEquals( + 0.01, + $instance->concat(1.0, 'копейка') + ); + $this->assertEquals( + 0.001, + $instance->concat(0.1, 'копейка') + ); + } + + public function testCurrency2(): void + { + $instance = new Price('RUB'); + $this->assertEquals( + 120.5, + $instance->concat(120.0, 'рублей', 50.0, 'копеек') + ); + } +} diff --git a/tests/Reply/LimitOrderBuyStockTest.php b/tests/Reply/LimitOrderBuyStockTest.php new file mode 100644 index 0000000..cc657f1 --- /dev/null +++ b/tests/Reply/LimitOrderBuyStockTest.php @@ -0,0 +1,398 @@ + [ + 'command' => 'подтверждаю', + 'original_utterance' => 'подтверждаю', + 'nlu' => [ + 'tokens' => [ + 'подтверждаю' + ], + 'entities' => [], + 'intents' => [ + 'YANDEX.CONFIRM' => [ + 'slots' => [] + ] + ] + ], + 'markup' => [ + 'dangerous_context' => false + ], + 'type' => 'SimpleUtterance' + ], + 'state' => [ + 'session' => [ + 'text' => 'заявка на покупку 10 лотов НЛМК, тикер NLMK, по цене сто двадцать рублей пятьдесят копеек за акцию. сумма сделки одна тысяча двести пять рублей плюс комиссия брокера. для подтверждения скажите подтверждаю.', + 'context' => [ + 'limit_order_buy_stock', + ], + 'order_details' => [ + 'price' => 120.5, + 'figi' => 'BBG004S681B4', + 'requestedLots' => 10, + 'name' => 'НЛМК', + ] + ], + 'user' => [] + ] + ]; + $order = $this->createStub(TIOrder::class); + $order->method('getStatus') + ->willReturn('New'); + $client = $this->createMock(TIClient::class); + $client->expects($this->once()) + ->method('sendOrder') + ->with( + $this->equalTo('BBG004S681B4'), + $this->equalTo(10), + $this->equalTo(TIOperationEnum::BUY) + )->willReturn($order); + + $limitOrder = new LimitOrderBuyStock($client); + $result = $limitOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('лимитная заявка на покупку создана', $result['response']['text']); + // @todo: lot, figi and price not in session + + + $order = $this->createStub(TIOrder::class); + $order->method('getStatus') + ->willReturn('PendingNew'); + $client = $this->createMock(TIClient::class); + $client->expects($this->once()) + ->method('sendOrder') + ->with( + $this->equalTo('BBG004S681B4'), + $this->equalTo(10), + $this->equalTo(TIOperationEnum::BUY) + )->willReturn($order); + + $limitOrder = new LimitOrderBuyStock($client); + $result = $limitOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('лимитная заявка на покупку отправлена', $result['response']['text']); + + + $order = $this->createStub(TIOrder::class); + $order->method('getStatus') + ->willReturn('Rejected'); + $order->method('getRejectReason') + ->willReturn('Unknown'); + $order->method('getMessage') + ->willReturn('ОШИБКА: (579) Для выбранного финансового инструмента цена должна быть не меньше 126.02'); + $client = $this->createMock(TIClient::class); + $client->expects($this->once()) + ->method('sendOrder') + ->with( + $this->equalTo('BBG004S681B4'), + $this->equalTo(10), + $this->equalTo(TIOperationEnum::BUY) + )->willReturn($order); + + $limitOrder = new LimitOrderBuyStock($client); + $result = $limitOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('лимитная заявка на покупку отклонена системой', $result['response']['text']); + $this->assertStringContainsStringIgnoringCase('для выбранного финансового инструмента цена должна быть не меньше', $result['response']['text']); + $this->assertStringNotContainsStringIgnoringCase('(', $result['response']['text']); + + /* does not work becase TIException extends Exception which is final + $exception = $this->createStub(TIException::class); + $exception->method('getMessage') + ->willReturn('Недостаточно активов для сделки [OrderNotAvailable]'); + $client = $this->createMock(TIClient::class); + $client->expects($this->once()) + ->method('sendOrder') + ->with( + $this->equalTo('BBG004S681B4'), + $this->equalTo(10), + $this->equalTo(TIOperationEnum::BUY) + )->willThrowException($exception); + + $limitOrder = new LimitOrderBuyStock($client); + $result = $limitOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('недостаточно активов для сделки', $result['response']['text']); + $this->assertStringNotContainsStringIgnoringCase('OrderNotAvailable', $result['response']['text']); + + + $exception = $this->createStub(TIException::class); + $exception->method('getMessage') + ->willReturn('[price]: 129.99 has invalid scale, minPriceIncrement=0.02 [VALIDATION_ERROR]'); + $client = $this->createMock(TIClient::class); + $client->expects($this->once()) + ->method('sendOrder') + ->with( + $this->equalTo('BBG004S681B4'), + $this->equalTo(10), + $this->equalTo(TIOperationEnum::BUY) + )->willThrowException($exception); + + $limitOrder = new LimitOrderBuyStock($client); + $result = $limitOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('недопустимый шаг цены', $result['response']['text']); + $this->assertStringNotContainsStringIgnoringCase('has invalid scale', $result['response']['text']); + */ + } + + public function testLimitOrderBuyStockReject(): void + { + $event = [ + 'request' => [ + 'command' => 'нет', + 'original_utterance' => 'нет', + 'nlu' => [ + 'tokens' => [ + 'нет' + ], + 'entities' => [], + 'intents' => [ + 'YANDEX.REJECT' => [ + 'slots' => [] + ] + ] + ], + 'markup' => [ + 'dangerous_context' => false + ], + 'type' => 'SimpleUtterance' + ], + 'state' => [ + 'session' => [ + 'text' => 'заявка на покупку 10 лотов НЛМК, тикер NLMK, по цене сто двадцать рублей пятьдесят копеек за акцию. сумма сделки одна тысяча двести пять рублей плюс комиссия брокера. для подтверждения скажите подтверждаю.', + 'context' => [ + 'limit_order_buy_stock', + ], + 'order_details' => [ + 'price' => 120.5, + 'figi' => 'BBG004S681B4', + 'requestedLots' => 10, + 'name' => 'НЛМК', + ] + ], + 'user' => [] + ] + ]; + $client = $this->createMock(TIClient::class); + $client->expects($this->never()) + ->method('sendOrder'); + + $newOrder = new LimitOrderBuyStock($client); + $result = $newOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertNotContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertStringContainsStringIgnoringCase('заявка отменена', $result['response']['text']); + // @todo: lot, figi and price not in session + } + + // @todo: + // в ApplicationTest проверить, что этот класс вообще вызывается + public function testLimitOrderBuyStock(): void + { + $event = [ + 'request' => [ + 'command' => 'купи 10 лотов нлмк по цене десять рублей пятьдесят копеек', + 'original_utterance' => 'купи 10 лотов нлмк по цене десять рублей пятьдесят копеек', + 'nlu' => [ + 'tokens' => [ + 'купи', + '10', + 'лотов', + 'нлмк', + 'по', + 'цене', + '100', + 'р', + '50', + 'к' + ], + 'entities' => [ + [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 1, + 'end' => 2 + ], + 'value' => 10 + ], + [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 6, + 'end' => 7 + ], + 'value' => 100 + ], + [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 8, + 'end' => 9 + ], + 'value' => 50 + ], + [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 11, + 'end' => 12 + ], + 'value' => 1 + ] + ], + 'intents' => [ + 'limit.order.buy.stock' => [ + 'slots' => [ + 'currency1' => [ + 'type' => 'YANDEX.STRING', + 'tokens' => [ + 'start' => 7, + 'end' => 8 + ], + 'value' => 'рублей' + ], + 'currency2' => [ + 'type' => 'YANDEX.STRING', + 'tokens' => [ + 'start' => 9, + 'end' => 10 + ], + 'value' => 'копеек' + ], + 'figi' => [ + 'type' => 'FIGI', + 'tokens' => [ + 'start' => 3, + 'end' => 4 + ], + 'value' => 'BBG004S681B4' + ], + 'requestedLots' => [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 1, + 'end' => 2 + ], + 'value' => 10 + ], + 'price1' => [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 6, + 'end' => 7 + ], + 'value' => 100 + ], + 'price2' => [ + 'type' => 'YANDEX.NUMBER', + 'tokens' => [ + 'start' => 8, + 'end' => 9 + ], + 'value' => 50 + ], + ], + ], + ], + ], + ], + ]; + + $client = $this->createStub(TIClient::class); + $client->method('getInstrumentByFigi') + ->willReturn(new TIInstrument( + 'BBG004S681B4', + 'NLMK', + null, + null, + 10, + TICurrencyEnum::RUB, + 'НЛМК', + null, + )); + + $newOrder = new LimitOrderBuyStock($client); + $result = $newOrder->handle($event); + $this->assertArrayHasKey('version', $result); + $this->assertArrayHasKey('response', $result); + $this->assertArrayHasKey('text', $result['response']); + $this->assertArrayHasKey('session_state', $result); + $this->assertArrayHasKey('text', $result['session_state']); + $this->assertArrayHasKey('context', $result['session_state']); + $this->assertContains('limit_order_buy_stock', $result['session_state']['context']); + $this->assertArrayHasKey('order_details', $result['session_state']); + $this->assertArrayHasKey('price', $result['session_state']['order_details']); + $this->assertEquals(100.5, $result['session_state']['order_details']['price']); + $this->assertArrayHasKey('figi', $result['session_state']['order_details']); + $this->assertEquals('BBG004S681B4', $result['session_state']['order_details']['figi']); + $this->assertArrayHasKey('requestedLots', $result['session_state']['order_details']); + $this->assertEquals(10, $result['session_state']['order_details']['requestedLots']); + $this->assertStringContainsStringIgnoringCase('количество лотов: десять', $result['response']['text']); + $this->assertStringContainsStringIgnoringCase( + 'цена за акцию: сто рублей пятьдесят копеек', + $result['response']['text'] + ); + $this->assertStringContainsStringIgnoringCase('тикер: NLMK', $result['response']['text']); + $this->assertStringContainsStringIgnoringCase('сумма заявки: десять тысяч пятьдесят рублей', $result['response']['text']); + $this->assertStringContainsStringIgnoringCase('для подтверждения', $result['response']['text']); + $this->assertStringContainsStringIgnoringCase('для отмены', $result['response']['text']); + } +}