From 8113fefaffa9063785db965b4ca58a18b894f94c Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Wed, 23 Mar 2016 13:08:15 -0300 Subject: [PATCH] New External Orders Endpoints added 2 new endpoints * /api/v1/summits/{id}/external-orders/{external_order_id} to read external orders ( from eventbrite api) * /api/v1/summits/{id}/external-orders/{external_order_id}/external-attendees/{external_attendee_id}/confirm to confirm, and become an attendee Change-Id: I1de5cf51fffdb45a9220190f412b3a121377b023 --- .env.example | 4 +- .../summit/OAuth2SummitApiController.php | 59 ++++++- app/Http/routes.php | 6 + app/Models/Utils/BaseModelEloquent.php | 6 +- app/Models/exceptions/ValidationException.php | 6 +- app/Services/ServicesProvider.php | 7 + app/Services/apis/EventbriteAPI.php | 79 +++++++++ app/Services/apis/IEventbriteAPI.php | 28 +++ app/Services/model/ISummitService.php | 17 ++ app/Services/model/SummitService.php | 163 +++++++++++++++++- composer.json | 2 +- config/server.php | 11 +- database/seeds/ApiEndpointsSeeder.php | 44 ++++- database/seeds/ApiScopesSeeder.php | 20 +++ tests/OAuth2SummitApiTest.php | 65 +++++++ tests/ProtectedApiTest.php | 4 + 16 files changed, 500 insertions(+), 21 deletions(-) create mode 100644 app/Services/apis/EventbriteAPI.php create mode 100644 app/Services/apis/IEventbriteAPI.php diff --git a/.env.example b/.env.example index 17a3b62a..392a7d4d 100644 --- a/.env.example +++ b/.env.example @@ -53,4 +53,6 @@ API_RESPONSE_CACHE_LIFETIME=600 LOG_EMAIL_TO=smarcet@gmail.com LOG_EMAIL_FROM=smarcet@gmail.com -LOG_LEVEL=info \ No newline at end of file +LOG_LEVEL=info + +EVENTBRITE_OAUTH2_PERSONAL_TOKEN= \ No newline at end of file diff --git a/app/Http/Controllers/apis/protected/summit/OAuth2SummitApiController.php b/app/Http/Controllers/apis/protected/summit/OAuth2SummitApiController.php index 77ed1486..5156fcd4 100644 --- a/app/Http/Controllers/apis/protected/summit/OAuth2SummitApiController.php +++ b/app/Http/Controllers/apis/protected/summit/OAuth2SummitApiController.php @@ -59,10 +59,10 @@ class OAuth2SummitApiController extends OAuth2ProtectedController ) { parent::__construct($resource_server_context); - $this->repository = $summit_repository; - $this->speaker_repository = $speaker_repository; - $this->event_repository = $event_repository; - $this->service = $service; + $this->repository = $summit_repository; + $this->speaker_repository = $speaker_repository; + $this->event_repository = $event_repository; + $this->service = $service; } public function getSummits() @@ -1432,4 +1432,55 @@ class OAuth2SummitApiController extends OAuth2ProtectedController } } + + public function getExternalOrder($summit_id, $external_order_id){ + try { + $summit = SummitFinderStrategyFactory::build($this->repository)->find($summit_id); + if (is_null($summit)) return $this->error404(); + $order = $this->service->getExternalOrder($summit, $external_order_id); + return $this->ok($order); + } + catch (EntityNotFoundException $ex1) { + Log::warning($ex1); + return $this->error404(array('message' => $ex1->getMessage())); + } + catch (ValidationException $ex2) { + Log::warning($ex2); + return $this->error412($ex2->getMessages()); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + public function confirmExternalOrderAttendee($summit_id, $external_order_id, $external_attendee_id){ + try { + $summit = SummitFinderStrategyFactory::build($this->repository)->find($summit_id); + if (is_null($summit)) return $this->error404(); + $member_id = $this->resource_server_context->getCurrentUserExternalId(); + if (is_null($member_id)) { + throw new \HTTP401UnauthorizedException; + } + $attendee = $this->service->confirmExternalOrderAttendee($summit, $member_id, $external_order_id, $external_attendee_id); + return $this->ok($attendee); + } + catch (EntityNotFoundException $ex1) { + Log::warning($ex1); + return $this->error404(array('message' => $ex1->getMessage())); + } + catch (ValidationException $ex2) { + Log::warning($ex2); + return $this->error412($ex2->getMessages()); + } + catch (\HTTP401UnauthorizedException $ex3) { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 91fe3905..29025fe2 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -124,6 +124,12 @@ Route::group(array( Route::get('', 'OAuth2SummitApiController@getSummitTypes'); }); + // summit types + Route::group(array('prefix' => 'external-orders'), function () { + Route::get('{external_order_id}', 'OAuth2SummitApiController@getExternalOrder'); + Route::post('{external_order_id}/external-attendees/{external_attendee_id}/confirm', 'OAuth2SummitApiController@confirmExternalOrderAttendee'); + }); + }); }); }); \ No newline at end of file diff --git a/app/Models/Utils/BaseModelEloquent.php b/app/Models/Utils/BaseModelEloquent.php index 7f31a0ad..af33934b 100644 --- a/app/Models/Utils/BaseModelEloquent.php +++ b/app/Models/Utils/BaseModelEloquent.php @@ -73,8 +73,10 @@ class BaseModelEloquent extends Eloquent { case 'datetime_epoch': { - $datetime = new \DateTime($value); - $value = $datetime->getTimestamp(); + if(!is_null($value)) { + $datetime = new \DateTime($value); + $value = $datetime->getTimestamp(); + } } break; case 'json_string': diff --git a/app/Models/exceptions/ValidationException.php b/app/Models/exceptions/ValidationException.php index 7b728464..b33d2e9c 100644 --- a/app/Models/exceptions/ValidationException.php +++ b/app/Models/exceptions/ValidationException.php @@ -37,6 +37,10 @@ class ValidationException extends Exception public function getMessages() { - $this->messages; + if(is_null($this->messages)) + { + $this->messages = array($this->getMessage()); + } + return $this->messages; } } \ No newline at end of file diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php index fff75162..54830f2c 100644 --- a/app/Services/ServicesProvider.php +++ b/app/Services/ServicesProvider.php @@ -14,7 +14,9 @@ **/ use App; +use Config; use Illuminate\Support\ServiceProvider; +use services\apis\EventbriteAPI; /*** * Class ServicesProvider @@ -33,5 +35,10 @@ class ServicesProvider extends ServiceProvider App::singleton('libs\utils\ICacheService', 'services\utils\RedisCacheService'); App::singleton('libs\utils\ITransactionService', 'services\utils\EloquentTransactionService'); App::singleton('services\model\ISummitService', 'services\model\SummitService'); + App::singleton('services\apis\IEventbriteAPI', function(){ + $api = new EventbriteAPI(); + $api->setCredentials(array('token' => Config::get("server.eventbrite_oauth2_personal_token", null))); + return $api; + }); } } \ No newline at end of file diff --git a/app/Services/apis/EventbriteAPI.php b/app/Services/apis/EventbriteAPI.php new file mode 100644 index 00000000..e16a2542 --- /dev/null +++ b/app/Services/apis/EventbriteAPI.php @@ -0,0 +1,79 @@ +auth_info = $auth_info; + } + + /** + * @param string $api_url + * @param array $params + * @return mixed + * @throws Exception + */ + public function getEntity($api_url, array $params) + { + if(strstr($api_url, self::BaseUrl) === false) throw new Exception('invalid base url!'); + $client = new Client(); + + $query = array + ( + 'token' => $this->auth_info['token'] + ); + + foreach($params as $param => $value) + { + $query[$param] = $value; + } + + $response = $client->get($api_url, array + ( + 'query' => $query + ) + ); + + if($response->getStatusCode() !== 200) throw new Exception('invalid status code!'); + $content_type = $response->getHeader('content-type'); + if(empty($content_type)) throw new Exception('invalid content type!'); + if($content_type !== 'application/json') throw new Exception('invalid content type!'); + + $json = $response->getBody()->getContents(); + return json_decode($json, true); + + } + + /** + * @param string $order_id + * @return mixed + */ + public function getOrder($order_id) + { + $order_id = intval($order_id); + $url = sprintf('%s/orders/%s', self::BaseUrl, $order_id); + return $this->getEntity($url, array('expand' => 'attendees')); + } +} \ No newline at end of file diff --git a/app/Services/apis/IEventbriteAPI.php b/app/Services/apis/IEventbriteAPI.php new file mode 100644 index 00000000..dc809116 --- /dev/null +++ b/app/Services/apis/IEventbriteAPI.php @@ -0,0 +1,28 @@ +event_repository = $event_repository; + $this->eventbrite_api = $eventbrite_api; $this->tx_service = $tx_service; } @@ -907,4 +923,149 @@ final class SummitService implements ISummitService return true; }); } + + /** + * @param Summit $summit + * @param $external_order_id + * @return array + * @throws ValidationException + * @throws \Exception + */ + public function getExternalOrder(Summit $summit, $external_order_id) + { + try{ + $external_order = $this->eventbrite_api->getOrder($external_order_id); + if (isset($external_order['attendees'])) + { + $status = $external_order['status']; + $summit_external_id = $external_order['event_id']; + $summit = Summit::where('ExternalEventId', '=', $summit_external_id)->first(); + if(is_null($summit)) throw new EntityNotFoundException('summit does not exists!'); + if(intval($summit->ID) !== intval($summit->ID)) throw new ValidationException('order does not belongs to current summit!'); + if($status !== 'placed') throw new ValidationException($status); + + $attendees = array(); + foreach($external_order['attendees'] as $a) + { + + $ticket_external_id = intval($a['ticket_class_id']); + $ticket_type = SummitTicketType::where('ExternalId', '=', $ticket_external_id)->first(); + if(is_null($ticket_type)) continue; + array_push($attendees, array( + 'external_id' => intval($a['id']), + 'first_name' => $a['profile']['first_name'], + 'last_name' => $a['profile']['last_name'], + 'company' => $a['profile']['company'], + 'email' => $a['profile']['email'], + 'job_title' => $a['profile']['job_title'], + 'status' => $a['status'], + 'ticket_type' => array + ( + 'id' => intval($ticket_type->ID), + 'name' => $ticket_type->Name, + 'external_id' => $ticket_external_id, + ) + )); + } + + return array('id' => intval($external_order_id), 'attendees' => $attendees); + } + } + catch(ClientException $ex1){ + if($ex1->getCode() === 400) + throw new ValidationException('external order does not exists!'); + throw $ex1; + } + catch(\Exception $ex){ + throw $ex; + } + } + + /** + * @param Summit $summit + * @param int $me_id + * @param int $external_order_id + * @param int $external_attendee_id + * @return SummitAttendee + */ + public function confirmExternalOrderAttendee(Summit $summit, $me_id, $external_order_id, $external_attendee_id) + { + return $this->tx_service->transaction(function () use ($summit, $me_id, $external_order_id, $external_attendee_id){ + + try{ + $external_order = $this->eventbrite_api->getOrder($external_order_id); + if (isset($external_order['attendees'])) + { + $external_attendee = null; + foreach($external_order['attendees'] as $a) + { + if(intval($a['id']) === intval($external_attendee_id)) { + $external_attendee = $a; + break; + } + } + + if(is_null($external_attendee)) throw new EntityNotFoundException('Attendee not found!'); + + $ticket_external_id = intval($external_attendee['ticket_class_id']); + $ticket_type = SummitTicketType::where('ExternalId', '=', $ticket_external_id)->first(); + if(is_null($ticket_type)) throw new EntityNotFoundException('Ticket Type not found!');; + + $status = $external_order['status']; + $summit_external_id = $external_order['event_id']; + $summit = Summit::where('ExternalEventId', '=', $summit_external_id)->first(); + if(is_null($summit)) throw new EntityNotFoundException('summit does not exists!'); + if(intval($summit->ID) !== intval($summit->ID)) throw new ValidationException('order does not belongs to current summit!'); + if($status !== 'placed') throw new ValidationException($status); + + $old_attendee = SummitAttendee::where('MemberID', '=', $me_id)->where('SummitID','=', $summit->ID)->first(); + + if(!is_null($old_attendee)) + throw new ValidationException + ( + 'Attendee Already Exist for current summit!' + ); + + $old_ticket = SummitAttendeeTicket + ::where('ExternalOrderId','=', $external_order_id) + ->where('ExternalAttendeeId','=', $external_attendee_id)->first(); + + if(!is_null($old_ticket)) + throw new ValidationException + ( + sprintf + ( + 'Ticket already redeem for attendee id %s !', + $old_ticket->OwnerID + ) + ); + + $attendee = new SummitAttendee; + $attendee->MemberID = $me_id; + $attendee->SummitID = $summit->ID; + $attendee->save(); + + $ticket = new SummitAttendeeTicket; + $ticket->ExternalOrderId = intval($external_order_id); + $ticket->ExternalAttendeeId = intval($external_attendee_id); + $ticket->TicketBoughtDate = $external_attendee['created']; + $ticket->TicketChangedDate = $external_attendee['changed']; + $ticket->TicketTypeID = $ticket_type->getIdentifier(); + $ticket->OwnerID = $attendee->ID; + $ticket->save(); + + return $attendee; + } + } + catch(ClientException $ex1){ + if($ex1->getCode() === 400) + throw new ValidationException('external order does not exists!'); + throw $ex1; + } + catch(\Exception $ex){ + throw $ex; + } + + }); + } } \ No newline at end of file diff --git a/composer.json b/composer.json index 322bc526..01606c0a 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "laravel/framework": "5.0.*", "predis/predis": "1.0.1", "php": ">=5.4.0", - "guzzlehttp/guzzle": "5.2.0" + "guzzlehttp/guzzle": "5.3.0" }, "require-dev": { "phpunit/phpunit": "4.6.6", diff --git a/config/server.php b/config/server.php index 269e2a82..48e033bb 100644 --- a/config/server.php +++ b/config/server.php @@ -14,9 +14,10 @@ return array ( - 'ssl_enabled' => env('SSL_ENABLED', false), - 'db_log_enabled' => env('DB_LOG_ENABLED', false), - 'access_token_cache_lifetime' => env('ACCESS_TOKEN_CACHE_LIFETIME', 300), - 'assets_base_url' => env('ASSETS_BASE_URL', null), - 'response_cache_lifetime' => env('API_RESPONSE_CACHE_LIFETIME', 300), + 'ssl_enabled' => env('SSL_ENABLED', false), + 'db_log_enabled' => env('DB_LOG_ENABLED', false), + 'access_token_cache_lifetime' => env('ACCESS_TOKEN_CACHE_LIFETIME', 300), + 'assets_base_url' => env('ASSETS_BASE_URL', null), + 'response_cache_lifetime' => env('API_RESPONSE_CACHE_LIFETIME', 300), + 'eventbrite_oauth2_personal_token' => env('EVENTBRITE_OAUTH2_PERSONAL_TOKEN', ''), ); \ No newline at end of file diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index 05816478..88b1f97a 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -496,11 +496,35 @@ class ApiEndpointsSeeder extends Seeder ) ); - $summit_read_scope = ApiScope::where('name', '=', sprintf('%s/summits/read', $current_realm))->first(); - $summit_write_scope = ApiScope::where('name', '=', sprintf('%s/summits/write', $current_realm))->first(); - $summit_write_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/write-event', $current_realm))->first(); - $summit_publish_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/publish-event', $current_realm))->first(); - $summit_delete_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/delete-event', $current_realm))->first(); + //external orders + + ApiEndpoint::create( + array( + 'name' => 'get-external-order', + 'active' => true, + 'api_id' => $summit->id, + 'route' => '/api/v1/summits/{id}/external-orders/{external_order_id}', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'confirm-external-order', + 'active' => true, + 'api_id' => $summit->id, + 'route' => '/api/v1/summits/{id}/external-orders/{external_order_id}/external-attendees/{external_attendee_id}/confirm', + 'http_method' => 'POST' + ) + ); + + $summit_read_scope = ApiScope::where('name', '=', sprintf('%s/summits/read', $current_realm))->first(); + $summit_write_scope = ApiScope::where('name', '=', sprintf('%s/summits/write', $current_realm))->first(); + $summit_write_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/write-event', $current_realm))->first(); + $summit_publish_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/publish-event', $current_realm))->first(); + $summit_delete_event_scope = ApiScope::where('name', '=', sprintf('%s/summits/delete-event', $current_realm))->first(); + $summit_external_order_read = ApiScope::where('name', '=', sprintf('%s/summits/read-external-orders', $current_realm))->first(); + $summit_external_order_confirm = ApiScope::where('name', '=', sprintf('%s/summits/confirm-external-orders', $current_realm))->first(); // read $endpoint = ApiEndpoint::where('name', '=', 'get-summits')->first(); @@ -543,9 +567,11 @@ class ApiEndpointsSeeder extends Seeder $endpoint->scopes()->attach($summit_read_scope->id); $endpoint = ApiEndpoint::where('name', '=', 'get-summit-types')->first(); $endpoint->scopes()->attach($summit_read_scope->id); + $endpoint = ApiEndpoint::where('name', '=', 'get-external-order')->first(); + $endpoint->scopes()->attach($summit_external_order_read->id); // write - $endpoint->scopes()->attach($summit_write_scope->id); + $endpoint = ApiEndpoint::where('name', '=', 'delete-event-attendee-schedule')->first(); $endpoint->scopes()->attach($summit_write_scope->id); $endpoint = ApiEndpoint::where('name', '=', 'checking-event-attendee-schedule')->first(); @@ -554,6 +580,7 @@ class ApiEndpointsSeeder extends Seeder $endpoint->scopes()->attach($summit_write_scope->id); $endpoint = ApiEndpoint::where('name', '=', 'add-event-feedback')->first(); $endpoint->scopes()->attach($summit_write_scope->id); + // write events $endpoint = ApiEndpoint::where('name', '=', 'add-event')->first(); $endpoint->scopes()->attach($summit_write_event_scope->id); @@ -569,6 +596,11 @@ class ApiEndpointsSeeder extends Seeder $endpoint = ApiEndpoint::where('name', '=', 'delete-event')->first(); $endpoint->scopes()->attach($summit_delete_event_scope->id); + //confirm external order + + $endpoint = ApiEndpoint::where('name', '=', 'confirm-external-order')->first(); + $endpoint->scopes()->attach($summit_external_order_confirm->id); + } } \ No newline at end of file diff --git a/database/seeds/ApiScopesSeeder.php b/database/seeds/ApiScopesSeeder.php index 5b6ea77f..93683456 100644 --- a/database/seeds/ApiScopesSeeder.php +++ b/database/seeds/ApiScopesSeeder.php @@ -140,6 +140,26 @@ class ApiScopesSeeder extends Seeder 'system' => false ) ); + + ApiScope::create( + array( + 'name' => sprintf('%s/summits/read-external-orders', $current_realm), + 'short_description' => 'Allow to read External Orders', + 'description' => 'Allow to read External Orders', + 'api_id' => $summits->id, + 'system' => false + ) + ); + + ApiScope::create( + array( + 'name' => sprintf('%s/summits/confirm-external-orders', $current_realm), + 'short_description' => 'Allow to confirm External Orders', + 'description' => 'Allow to confirm External Orders', + 'api_id' => $summits->id, + 'system' => false + ) + ); } } \ No newline at end of file diff --git a/tests/OAuth2SummitApiTest.php b/tests/OAuth2SummitApiTest.php index 6239c3d5..c99bd4fa 100644 --- a/tests/OAuth2SummitApiTest.php +++ b/tests/OAuth2SummitApiTest.php @@ -1146,4 +1146,69 @@ class OAuth2SummitApiTest extends ProtectedApiTest $this->assertTrue(!is_null($locations)); } + public function testGetCurrentSummitExternalOrder() + { + $params = array + ( + 'id' => 'current', + 'external_order_id' => 484446336 + ); + + $headers = array + ( + "HTTP_Authorization" => " Bearer " .$this->access_token, + "CONTENT_TYPE" => "application/json" + ); + + $response = $this->action + ( + "GET", + "OAuth2SummitApiController@getExternalOrder", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $order = json_decode($content); + $this->assertTrue(!is_null($order)); + } + + + public function testGetCurrentSummitConfirmExternalOrder() + { + $params = array + ( + 'id' => 'current', + 'external_order_id' => 484446336, + 'external_attendee_id' => 611227262 + ); + + $headers = array + ( + "HTTP_Authorization" => " Bearer " .$this->access_token, + "CONTENT_TYPE" => "application/json" + ); + + $response = $this->action + ( + "POST", + "OAuth2SummitApiController@confirmExternalOrderAttendee", + $params, + array(), + array(), + array(), + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $attendee = json_decode($content); + $this->assertTrue(!is_null($attendee)); + } } \ No newline at end of file diff --git a/tests/ProtectedApiTest.php b/tests/ProtectedApiTest.php index e1ba9b31..cf3aa4c4 100644 --- a/tests/ProtectedApiTest.php +++ b/tests/ProtectedApiTest.php @@ -42,6 +42,8 @@ class AccessTokenServiceStub implements IAccessTokenService $url . '/summits/write-event', $url . '/summits/publish-event', $url . '/summits/delete-event', + $url . '/summits/read-external-orders', + $url . '/summits/confirm-external-orders', ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, '1','11624', 3600, 'WEB_APPLICATION', '', ''); @@ -71,6 +73,8 @@ class AccessTokenServiceStub2 implements IAccessTokenService $url . '/summits/write-event', $url . '/summits/publish-event', $url . '/summits/delete-event', + $url . '/summits/read-external-orders', + $url . '/summits/confirm-external-orders', ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, null,null, 3600, 'SERVICE', '', '');