diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5826402 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..87a0ca5 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +nexmo \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1162f43 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5774d69 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/nexmo.iml b/.idea/nexmo.iml new file mode 100644 index 0000000..6b8184f --- /dev/null +++ b/.idea/nexmo.iml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..84c5e58 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..def6a6a --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..f3445bc --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1400702043442 + 1400702043442 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aa14ee5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - composer self-update + - composer install --prefer-source --no-interaction --dev + +script: phpunit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..225f2d6 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "artistan/nexmo", + "description": "", + "authors": [ + { + "name": "Charles Peterson", + "email": "artistan@gmail.com" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "git://github.com/orchestral/phpseclib.git" + } + ], + "require": { + "php": ">=5.3.0", + "illuminate/support": "4.1.*" + }, + "require-dev":{ + "phpunit/phpunit": "3.7.*", + "orchestra/testbench": "2.1.*", + "mockery/mockery": "dev-master@dev" + }, + "autoload": { + "psr-0": { + "Artistan\\Nexmo": "src/" + } + }, + "minimum-stability": "stable" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Artistan/Nexmo/Facades/Account.php b/src/Artistan/Nexmo/Facades/Account.php new file mode 100644 index 0000000..e96539a --- /dev/null +++ b/src/Artistan/Nexmo/Facades/Account.php @@ -0,0 +1,14 @@ +package('artistan/nexmo'); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // Receipt + $this->app['nexmoreceipt'] = $this->app->share(function($app) + { + return new Account(); + }); + + $this->app->booting(function() + { + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('NexmoReceipt', 'Artistan\Nexmo\Facades\Receipt'); + }); + + // Account + $this->app['nexmoaccount'] = $this->app->share(function($app) + { + return new Account(); + }); + + $this->app->booting(function() + { + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('NexmoAccount', 'Artistan\Nexmo\Facades\Account'); + }); + + // SMS Message + $this->app['nexmosmsmessage'] = $this->app->share(function($app) + { + return new MessageSms(); + }); + + $this->app->booting(function() + { + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('NexmoSmsMessage', 'Artistan\Nexmo\Facades\Message\Sms'); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return array('nexmosmsmessage','nexmoreceipt','nexmoaccount'); + } + +} diff --git a/src/Artistan/Nexmo/Service/Account.php b/src/Artistan/Nexmo/Service/Account.php new file mode 100644 index 0000000..ea6a4b2 --- /dev/null +++ b/src/Artistan/Nexmo/Service/Account.php @@ -0,0 +1,246 @@ + array('method' => 'GET', 'url' => '/account/get-balance/{k}/{s}'), + 'get_pricing' => array('method' => 'GET', 'url' => '/account/get-pricing/outbound/{k}/{s}/{country_code}'), + 'get_own_numbers' => array('method' => 'GET', 'url' => '/account/numbers/{k}/{s}'), + 'search_numbers' => array('method' => 'GET', 'url' => '/number/search/{k}/{s}/{country_code}?pattern={pattern}'), + 'buy_number' => array('method' => 'POST', 'url' => '/number/buy/{k}/{s}/{country_code}/{msisdn}'), + 'cancel_number' => array('method' => 'POST', 'url' => '/number/cancel/{k}/{s}/{country_code}/{msisdn}') + ); + + + private $cache = array(); + + /** + * @param $nx_key Your Nexmo account key + * @param $nx_secret Your Nexmo secret + */ + public function __construct ($api_key='', $api_secret='') { + if(!empty($api_key) && !empty($api_secret)){ + $this->nx_key = $api_key; + $this->nx_secret = $api_secret; + } else { + $this->nx_key = \Config::get('nexmo::auth.api_key'); + $this->nx_secret = \Config::get('nexmo::auth.api_secret'); + } + + if(empty($this->nx_key) || empty($this->nx_secret)){ + throw new \Exception("Account Credentials Exception",5001); + } + } + + + /** + * Return your account balance in Euros + * @return float|bool + */ + public function balance () { + if (!isset($this->cache['balance'])) { + $tmp = $this->apiCall('get_balance'); + if (!$tmp['data']) return false; + + $this->cache['balance'] = $tmp['data']['value']; + } + + return (float)$this->cache['balance']; + } + + + /** + * Find out the price to send a message to a country + * @param $country_code Country code to return the SMS price for + * @return float|bool + */ + public function smsPricing ($country_code) { + $country_code = strtoupper($country_code); + + if (!isset($this->cache['country_codes'])) + $this->cache['country_codes'] = array(); + + if (!isset($this->cache['country_codes'][$country_code])) { + $tmp = $this->apiCall('get_pricing', array('country_code'=>$country_code)); + if (!$tmp['data']) return false; + + $this->cache['country_codes'][$country_code] = $tmp['data']; + } + + return (float)$this->cache['country_codes'][$country_code]['mt']; + } + + + /** + * Return a countries international dialing code + * @param $country_code Country code to return the dialing code for + * @return string|bool + */ + public function getCountryDialingCode ($country_code) { + $country_code = strtoupper($country_code); + + if (!isset($this->cache['country_codes'])) + $this->cache['country_codes'] = array(); + + if (!isset($this->cache['country_codes'][$country_code])) { + $tmp = $this->apiCall('get_pricing', array('country_code'=>$country_code)); + if (!$tmp['data']) return false; + + $this->cache['country_codes'][$country_code] = $tmp['data']; + } + + return (string)$this->cache['country_codes'][$country_code]['prefix']; + } + + + /** + * Get an array of all purchased numbers for your account + * @return array|bool + */ + public function numbersList () { + if (!isset($this->cache['own_numbers'])) { + $tmp = $this->apiCall('get_own_numbers'); + if (!$tmp['data']) return false; + + $this->cache['own_numbers'] = $tmp['data']; + } + + if (!$this->cache['own_numbers']['numbers']) { + return array(); + } + + return $this->cache['own_numbers']['numbers']; + } + + + /** + * Search available numbers to purchase for your account + * @param $country_code Country code to search available numbers in + * @param $pattern Number pattern to search for + * @return bool + */ + public function numbersSearch ($country_code, $pattern) { + $country_code = strtoupper($country_code); + + $tmp = $this->apiCall('search_numbers', array('country_code'=>$country_code, 'pattern'=>$pattern)); + if (!$tmp['data'] || !isset($tmp['data']['numbers'])) return false; + return $tmp['data']['numbers']; + } + + + /** + * Purchase an available number to your account + * @param $country_code Country code for your desired number + * @param $msisdn Full number which you wish to purchase + * @return bool + */ + public function numbersBuy ($country_code, $msisdn) { + $country_code = strtoupper($country_code); + + $tmp = $this->apiCall('buy_number', array('country_code'=>$country_code, 'msisdn'=>$msisdn)); + return ($tmp['http_code'] === 200); + } + + + /** + * Cancel an existing number on your account + * @param $country_code Country code for which the number is for + * @param $msisdn The number to cancel + * @return bool + */ + public function numbersCancel ($country_code, $msisdn) { + $country_code = strtoupper($country_code); + + $tmp = $this->apiCall('cancel_number', array('country_code'=>$country_code, 'msisdn'=>$msisdn)); + return ($tmp['http_code'] === 200); + } + + + /** + * Run a REST command on Nexmo SMS services + * @param $command + * @param array $data + * @return array|bool + */ + private function apiCall($command, $data=array()) { + if (!isset($this->rest_commands[$command])) { + return false; + } + + $cmd = $this->rest_commands[$command]; + + $url = $cmd['url']; + $url = str_replace(array('{k}', '{s}') ,array($this->nx_key, $this->nx_secret), $url); + + $parsed_data = array(); + foreach ($data as $k => $v) $parsed_data['{'.$k.'}'] = $v; + $url = str_replace(array_keys($parsed_data) ,array_values($parsed_data), $url); + + $url = trim($this->rest_base_url, '/') . $url; + $post_data = ''; + + // If available, use CURL + if (function_exists('curl_version')) { + + $to_nexmo = curl_init( $url ); + curl_setopt( $to_nexmo, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $to_nexmo, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($to_nexmo, CURLOPT_HTTPHEADER, array('Accept: application/json')); + + if ($cmd['method'] == 'POST') { + curl_setopt( $to_nexmo, CURLOPT_POST, true ); + curl_setopt( $to_nexmo, CURLOPT_POSTFIELDS, $post_data ); + } + + $from_nexmo = curl_exec( $to_nexmo ); + $curl_info = curl_getinfo($to_nexmo); + $http_response_code = $curl_info['http_code']; + curl_close ( $to_nexmo ); + + } elseif (ini_get('allow_url_fopen')) { + // No CURL available so try the awesome file_get_contents + + $opts = array('http' => + array( + 'method' => 'GET', + 'header' => 'Accept: application/json' + ) + ); + + if ($cmd['method'] == 'POST') { + $opts['http']['method'] = 'POST'; + $opts['http']['header'] .= "\r\nContent-type: application/x-www-form-urlencoded"; + $opts['http']['content'] = $post_data; + } + + $context = stream_context_create($opts); + $from_nexmo = file_get_contents($url, false, $context); + + // et the response code + preg_match('/HTTP\/[^ ]+ ([0-9]+)/i', $http_response_header[0], $m); + $http_response_code = $m[1]; + + } else { + // No way of sending a HTTP post :( + return false; + } + + $data = json_decode($from_nexmo, true); + return array( + 'data' => $data, + 'http_code' => (int)$http_response_code + ); + + } +} \ No newline at end of file diff --git a/src/Artistan/Nexmo/Service/Message/Sms.php b/src/Artistan/Nexmo/Service/Message/Sms.php new file mode 100644 index 0000000..e5ffc40 --- /dev/null +++ b/src/Artistan/Nexmo/Service/Message/Sms.php @@ -0,0 +1,438 @@ +nx_key = $api_key; + $this->nx_secret = $api_secret; + } else { + $this->nx_key = \Config::get('nexmo::auth.api_key'); + $this->nx_secret = \Config::get('nexmo::auth.api_secret'); + } + + if(empty($this->nx_key) || empty($this->nx_secret)){ + throw new \Exception("Account Credentials Exception",5001); + } + } + + + + /** + * Prepare new text message. + * + * If $unicode is not provided we will try to detect the + * message type. Otherwise set to TRUE if you require + * unicode characters. + */ + function sendText ( $to, $from, $message, $unicode=null ) { + + // Making sure strings are UTF-8 encoded + if ( !is_numeric($from) && !mb_check_encoding($from, 'UTF-8') ) { + trigger_error('$from needs to be a valid UTF-8 encoded string'); + return false; + } + + if ( !mb_check_encoding($message, 'UTF-8') ) { + trigger_error('$message needs to be a valid UTF-8 encoded string'); + return false; + } + + if ($unicode === null) { + $containsUnicode = max(array_map('ord', str_split($message))) > 127; + } else { + $containsUnicode = (bool)$unicode; + } + + // Make sure $from is valid + $from = $this->validateOriginator($from); + + // URL Encode + $from = urlencode( $from ); + $message = urlencode( $message ); + + // Send away! + $post = array( + 'from' => $from, + 'to' => $to, + 'text' => $message, + 'type' => $containsUnicode ? 'unicode' : 'text' + ); + return $this->sendRequest ( $post ); + + } + + + /** + * Prepare new WAP message. + */ + function sendBinary ( $to, $from, $body, $udh ) { + + //Binary messages must be hex encoded + $body = bin2hex ( $body ); + $udh = bin2hex ( $udh ); + + // Make sure $from is valid + $from = $this->validateOriginator($from); + + // Send away! + $post = array( + 'from' => $from, + 'to' => $to, + 'type' => 'binary', + 'body' => $body, + 'udh' => $udh + ); + return $this->sendRequest ( $post ); + + } + + + /** + * Prepare new binary message. + */ + function pushWap ( $to, $from, $title, $url, $validity = 172800000 ) { + + // Making sure $title and $url are UTF-8 encoded + if ( !mb_check_encoding($title, 'UTF-8') || !mb_check_encoding($url, 'UTF-8') ) { + trigger_error('$title and $udh need to be valid UTF-8 encoded strings'); + return false; + } + + // Make sure $from is valid + $from = $this->validateOriginator($from); + + // Send away! + $post = array( + 'from' => $from, + 'to' => $to, + 'type' => 'wappush', + 'url' => $url, + 'title' => $title, + 'validity' => $validity + ); + return $this->sendRequest ( $post ); + + } + + + /** + * Prepare and send a new message. + */ + private function sendRequest ( $data ) { + // Build the post data + $data = array_merge($data, array('username' => $this->nx_key, 'password' => $this->nx_secret)); + $post = ''; + foreach($data as $k => $v){ + $post .= "&$k=$v"; + } + + // If available, use CURL + if (function_exists('curl_version')) { + + $to_nexmo = curl_init( $this->nx_uri ); + curl_setopt( $to_nexmo, CURLOPT_POST, true ); + curl_setopt( $to_nexmo, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $to_nexmo, CURLOPT_POSTFIELDS, $post ); + + if (!$this->ssl_verify) { + curl_setopt( $to_nexmo, CURLOPT_SSL_VERIFYPEER, false); + } + + $from_nexmo = curl_exec( $to_nexmo ); + curl_close ( $to_nexmo ); + + } elseif (ini_get('allow_url_fopen')) { + // No CURL available so try the awesome file_get_contents + + $opts = array('http' => + array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $post + ) + ); + $context = stream_context_create($opts); + $from_nexmo = file_get_contents($this->nx_uri, false, $context); + var_dump($from_nexmo);exit; + + } else { + // No way of sending a HTTP post :( + return false; + } + + + return $this->nexmoParse( $from_nexmo ); + + } + + + /** + * Recursively normalise any key names in an object, removing unwanted characters + */ + private function normaliseKeys ($obj) { + // Determine is working with a class or araay + if ($obj instanceof stdClass) { + $new_obj = new stdClass(); + $is_obj = true; + } else { + $new_obj = array(); + $is_obj = false; + } + + + foreach($obj as $key => $val){ + // If we come across another class/array, normalise it + if ($val instanceof stdClass || is_array($val)) { + $val = $this->normaliseKeys($val); + } + + // Replace any unwanted characters in they key name + if ($is_obj) { + $new_obj->{str_replace('-', '', $key)} = $val; + } else { + $new_obj[str_replace('-', '', $key)] = $val; + } + } + + return $new_obj; + } + + + /** + * Parse server response. + */ + private function nexmoParse ( $from_nexmo ) { + $response = json_decode($from_nexmo); + + // Copy the response data into an object, removing any '-' characters from the key + $response_obj = $this->normaliseKeys($response); + + if ($response_obj) { + $this->nexmo_response = $response_obj; + + // Find the total cost of this message + $response_obj->cost = $total_cost = 0; + if (is_array($response_obj->messages)) { + foreach ($response_obj->messages as $msg) { + if (property_exists($msg, "messageprice")) { + $total_cost = $total_cost + (float)$msg->messageprice; + } + } + + $response_obj->cost = $total_cost; + } + + return $response_obj; + + } else { + // A malformed response + $this->nexmo_response = array(); + return false; + } + + } + + + /** + * Validate an originator string + * + * If the originator ('from' field) is invalid, some networks may reject the network + * whilst stinging you with the financial cost! While this cannot correct them, it + * will try its best to correctly format them. + */ + private function validateOriginator($inp){ + // Remove any invalid characters + $ret = preg_replace('/[^a-zA-Z0-9]/', '', (string)$inp); + + if(preg_match('/[a-zA-Z]/', $inp)){ + + // Alphanumeric format so make sure it's < 11 chars + $ret = substr($ret, 0, 11); + + } else { + + // Numerical, remove any prepending '00' + if(substr($ret, 0, 2) == '00'){ + $ret = substr($ret, 2); + $ret = substr($ret, 0, 15); + } + } + + return (string)$ret; + } + + + + /** + * Display a brief overview of a sent message. + * Useful for debugging and quick-start purposes. + */ + public function displayOverview( $nexmo_response=null ){ + $info = (!$nexmo_response) ? $this->nexmo_response : $nexmo_response; + + if (!$nexmo_response ) return 'Cannot display an overview of this response'; + + // How many messages were sent? + if ( $info->messagecount > 1 ) { + + $status = 'Your message was sent in ' . $info->messagecount . ' parts'; + + } elseif ( $info->messagecount == 1) { + + $status = 'Your message was sent'; + + } else { + + return 'There was an error sending your message'; + } + + // Build an array of each message status and ID + if (!is_array($info->messages)) $info->messages = array(); + $message_status = array(); + foreach ( $info->messages as $message ) { + $tmp = array('id'=>'', 'status'=>0); + + if ( $message->status != 0) { + $tmp['status'] = $message->errortext; + } else { + $tmp['status'] = 'OK'; + $tmp['id'] = $message->messageid; + } + + $message_status[] = $tmp; + } + + + // Build the output + if (isset($_SERVER['HTTP_HOST'])) { + // HTML output + $ret = ''; + $ret .= ''; + foreach ($message_status as $mstat) { + $ret .= ''; + } + $ret .= '
'.$status.'
StatusMessage ID
'.$mstat['status'].''.$mstat['id'].'
'; + + } else { + + // CLI output + $ret = "$status:\n"; + + // Get the sizes for the table + $out_sizes = array('id'=>strlen('Message ID'), 'status'=>strlen('Status')); + foreach ($message_status as $mstat) { + if ($out_sizes['id'] < strlen($mstat['id'])) { + $out_sizes['id'] = strlen($mstat['id']); + } + if ($out_sizes['status'] < strlen($mstat['status'])) { + $out_sizes['status'] = strlen($mstat['status']); + } + } + + $ret .= ' '.str_pad('Status', $out_sizes['status'], ' ').' '; + $ret .= str_pad('Message ID', $out_sizes['id'], ' ')."\n"; + foreach ($message_status as $mstat) { + $ret .= ' '.str_pad($mstat['status'], $out_sizes['status'], ' ').' '; + $ret .= str_pad($mstat['id'], $out_sizes['id'], ' ')."\n"; + } + } + + return $ret; + } + + + + + + + + /** + * Inbound text methods + */ + + + /** + * Check for any inbound messages, using $_GET by default. + * + * This will set the current message to the inbound + * message allowing for a future reply() call. + */ + public function inboundText( $data=null ){ + if(!$data) $data = $_GET; + + if(!isset($data['text'], $data['msisdn'], $data['to'])) return false; + + // Get the relevant data + $this->to = $data['to']; + $this->from = $data['msisdn']; + $this->text = $data['text']; + $this->network = (isset($data['network-code'])) ? $data['network-code'] : ''; + $this->message_id = $data['messageId']; + + // Flag that we have an inbound message + $this->inbound_message = true; + + return true; + } + + + /** + * Reply the current message if one is set. + */ + public function reply ($message) { + // Make sure we actually have a text to reply to + if (!$this->inbound_message) { + return false; + } + + return $this->sendText($this->from, $this->to, $message); + } + +} \ No newline at end of file diff --git a/src/Artistan/Nexmo/Service/Receipt.php b/src/Artistan/Nexmo/Service/Receipt.php new file mode 100644 index 0000000..0204614 --- /dev/null +++ b/src/Artistan/Nexmo/Service/Receipt.php @@ -0,0 +1,64 @@ +found = true; + + // Get the relevant data + $this->to = $data['msisdn']; + $this->from = $data['to']; + $this->network = $data['network-code']; + $this->message_id = $data['messageId']; + $this->status = strtoupper($data['status']); + + // Format the date into timestamp + $dp = date_parse_from_format('ymdGi', $data['scts']); + $this->received_time = mktime($dp['hour'], $dp['minute'], $dp['second'], $dp['month'], $dp['day'], $dp['year']); + + // TODO add event with this object attached. + } + + + /** + * Returns true if a valid receipt is found + */ + public function exists () { + return $this->found; + } +} \ No newline at end of file diff --git a/src/Artistan/Nexmo/original/Nexmo-PHP-lib b/src/Artistan/Nexmo/original/Nexmo-PHP-lib new file mode 160000 index 0000000..fceadaf --- /dev/null +++ b/src/Artistan/Nexmo/original/Nexmo-PHP-lib @@ -0,0 +1 @@ +Subproject commit fceadafe642636a224f442401db012f395832aa4 diff --git a/src/config/auth.php b/src/config/auth.php new file mode 100644 index 0000000..43a4e29 --- /dev/null +++ b/src/config/auth.php @@ -0,0 +1,8 @@ + 'api_key', + 'api_secret' => 'api_secret' +); \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/AccountTest.php b/tests/AccountTest.php new file mode 100644 index 0000000..8888523 --- /dev/null +++ b/tests/AccountTest.php @@ -0,0 +1,22 @@ +assertTrue(true); + } +} + \ No newline at end of file diff --git a/tests/MessageSmsTest.php b/tests/MessageSmsTest.php new file mode 100644 index 0000000..b0e32b0 --- /dev/null +++ b/tests/MessageSmsTest.php @@ -0,0 +1,22 @@ +assertTrue(true); + } +} + \ No newline at end of file diff --git a/tests/ReceiptTest.php b/tests/ReceiptTest.php new file mode 100644 index 0000000..3d9e357 --- /dev/null +++ b/tests/ReceiptTest.php @@ -0,0 +1,22 @@ +assertTrue(true); + } +} + \ No newline at end of file