From 9f01b9e3b32434f5aa8e92a92b3533a59810e89d Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 29 May 2019 14:32:14 -0300 Subject: [PATCH] Booking Rooms Model * model changes * Endpoint to get all avaible booking rooms per summit GET /api/v1/summits/{id}/locations/bookable-rooms query string params page ( int ) current page number per_page ( int) max amount of items per page filter ( allowed fields: name, description, cost, capacity, availability_day[epoch], attribute[string|number]) order ( allowed fields id,name,capacity) expand ( allowed relations venue,floor,attribute_type) scopes %s/bookable-rooms/read %s/summits/read/all * Endpoint to get slot availability per room GET /api/v1/summits/{id}/locations/bookable-rooms/{room_id}/availability/{day} where id is summit id (integer) room_id ( integer) and day (epoch timestamp) scopes %s/bookable-rooms/read %s/summits/read/all * endpoint create reservation POST /api/v1/summits/{id}/locations/bookable-rooms/{room_id}/reservations payload 'currency' => 'required|string|currency_iso', 'amount' => 'required|integer', 'start_datetime' => 'required|date_format:U', 'end_datetime' => 'required|date_format:U|after:start_datetime', scopes %s/bookable-rooms/my-reservations/write * endpoint to get all my reservations GET /api/v1/summits/{id}/locations/bookable-rooms/all/reservations/me query string params expand [owner, room, type] scopes %s/bookable-rooms/my-reservations/read * endpoint to cancel/ask for refund a reservation DELETE /api/v1/summits/{id}/locations/bookable-rooms/all/reservations/{reservation_id} scopes %s/bookable-rooms/my-reservations/write Change-Id: I741878c6ffc833ba23fca40f09f4664b42c8edd4 --- Libs/ModelSerializers/AbstractSerializer.php | 2 +- ...tRoomReservationValidationRulesFactory.php | 36 ++ .../Summit/OAuth2SummitApiController.php | 22 + .../OAuth2SummitLocationsApiController.php | 26 +- .../Traits/SummitBookableVenueRoomApi.php | 384 ++++++++++++++++++ app/Http/Routes/public.php | 9 + app/Http/Utils/PagingResponse.php | 6 +- app/Http/routes.php | 50 +++ ...okableVenueRoomAttributeTypeSerializer.php | 25 ++ ...kableVenueRoomAttributeValueSerializer.php | 52 +++ ...okableVenueRoomAvailableSlotSerializer.php | 28 ++ .../SummitBookableVenueRoomSerializer.php | 43 ++ .../SummitRoomReservationSerializer.php | 73 ++++ .../Locations/SummitVenueRoomSerializer.php | 3 +- app/ModelSerializers/SerializerRegistry.php | 28 +- .../Summit/SummitSerializer.php | 16 +- app/Models/Foundation/Main/Member.php | 37 ++ .../SummitRoomReservationFactory.php | 53 +++ .../Locations/SummitAbstractLocation.php | 11 +- .../Locations/SummitBookableVenueRoom.php | 280 +++++++++++++ .../SummitBookableVenueRoomAttributeType.php | 85 ++++ .../SummitBookableVenueRoomAttributeValue.php | 84 ++++ .../SummitBookableVenueRoomAvailableSlot.php | 88 ++++ .../Locations/SummitRoomReservation.php | 329 +++++++++++++++ .../Summit/Locations/SummitVenue.php | 1 - .../Summit/Repositories/ISummitRepository.php | 6 + .../ISummitRoomReservationRepository.php | 27 ++ app/Models/Foundation/Summit/Summit.php | 122 ++++++ app/Providers/AppServiceProvider.php | 19 + app/Providers/AuthServiceProvider.php | 22 +- app/Providers/RouteServiceProvider.php | 22 +- app/Repositories/RepositoriesProvider.php | 9 + .../DoctrineApiEndpointRepository.php | 2 +- .../DoctrineSummitLocationRepository.php | 66 ++- .../Summit/DoctrineSummitRepository.php | 15 + ...octrineSummitRoomReservationRepository.php | 42 ++ app/Security/SummitScopes.php | 73 ++-- app/Services/Apis/IPaymentGatewayAPI.php | 52 +++ .../Apis/PaymentGateways/StripeApi.php | 174 ++++++++ app/Services/Model/ILocationService.php | 28 ++ app/Services/Model/SummitLocationService.php | 143 ++++++- app/Services/ServicesProvider.php | 10 +- composer.json | 1 + composer.lock | 58 ++- config/stripe.php | 20 + .../model/Version20190529015655.php | 54 +++ .../model/Version20190529142913.php | 57 +++ .../model/Version20190529142927.php | 64 +++ .../model/Version20190530205326.php | 42 ++ .../model/Version20190530205344.php | 56 +++ database/seeds/ApiEndpointsSeeder.php | 88 ++++ database/seeds/ApiScopesSeeder.php | 10 + tests/MeetingRoomTest.php | 53 +++ tests/OAuth2SummitLocationsApiTest.php | 177 ++++++++ tests/ProtectedApiTest.php | 4 + 55 files changed, 3194 insertions(+), 93 deletions(-) create mode 100644 app/Http/Controllers/Apis/Protected/Summit/Factories/SummitRoomReservationValidationRulesFactory.php create mode 100644 app/Http/Controllers/Apis/Protected/Summit/Traits/SummitBookableVenueRoomApi.php create mode 100644 app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeTypeSerializer.php create mode 100644 app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeValueSerializer.php create mode 100644 app/ModelSerializers/Locations/SummitBookableVenueRoomAvailableSlotSerializer.php create mode 100644 app/ModelSerializers/Locations/SummitBookableVenueRoomSerializer.php create mode 100644 app/ModelSerializers/Locations/SummitRoomReservationSerializer.php create mode 100644 app/Models/Foundation/Summit/Factories/SummitRoomReservationFactory.php create mode 100644 app/Models/Foundation/Summit/Locations/SummitBookableVenueRoom.php create mode 100644 app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeType.php create mode 100644 app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeValue.php create mode 100644 app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAvailableSlot.php create mode 100644 app/Models/Foundation/Summit/Locations/SummitRoomReservation.php create mode 100644 app/Models/Foundation/Summit/Repositories/ISummitRoomReservationRepository.php create mode 100644 app/Repositories/Summit/DoctrineSummitRoomReservationRepository.php create mode 100644 app/Services/Apis/IPaymentGatewayAPI.php create mode 100644 app/Services/Apis/PaymentGateways/StripeApi.php create mode 100644 config/stripe.php create mode 100644 database/migrations/model/Version20190529015655.php create mode 100644 database/migrations/model/Version20190529142913.php create mode 100644 database/migrations/model/Version20190529142927.php create mode 100644 database/migrations/model/Version20190530205326.php create mode 100644 database/migrations/model/Version20190530205344.php create mode 100644 tests/MeetingRoomTest.php diff --git a/Libs/ModelSerializers/AbstractSerializer.php b/Libs/ModelSerializers/AbstractSerializer.php index 33d160c2..571d0275 100644 --- a/Libs/ModelSerializers/AbstractSerializer.php +++ b/Libs/ModelSerializers/AbstractSerializer.php @@ -133,7 +133,7 @@ abstract class AbstractSerializer implements IModelSerializer * @param array $params * @return array */ - public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array() ) + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) { $values = []; $method_prefix = ['get', 'is']; diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitRoomReservationValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitRoomReservationValidationRulesFactory.php new file mode 100644 index 00000000..bc6315b7 --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitRoomReservationValidationRulesFactory.php @@ -0,0 +1,36 @@ + 'required|string|currency_iso', + 'amount' => 'required|integer', + 'start_datetime' => 'required|date_format:U', + 'end_datetime' => 'required|date_format:U|after:start_datetime', + ]; + } +} \ 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 d361e3fc..43f60a3c 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitApiController.php @@ -211,6 +211,28 @@ final class OAuth2SummitApiController extends OAuth2ProtectedController } } + public function getAllSummitByIdOrSlug($id){ + + $expand = Request::input('expand', ''); + + try { + $summit = $this->repository->getById(intval($id)); + if(is_null($summit)) + $summit = $this->repository->getBySlug(trim($id)); + if (is_null($summit)) return $this->error404(); + $serializer_type = $this->serializer_type_selector->getSerializerType(); + return $this->ok(SerializerRegistry::getInstance()->getSerializer($summit, $serializer_type)->serialize($expand)); + } + catch(HTTP403ForbiddenException $ex1){ + Log::warning($ex1); + return $this->error403(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + /** * @return mixed */ diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php index 160fa752..e376bfa7 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php @@ -16,8 +16,10 @@ use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBannerConstants use App\Models\Foundation\Summit\Locations\SummitLocationConstants; use App\Models\Foundation\Summit\Repositories\ISummitLocationBannerRepository; use App\Models\Foundation\Summit\Repositories\ISummitLocationRepository; +use App\Services\Apis\IPaymentGatewayAPI; use App\Services\Model\ILocationService; use Exception; +use Illuminate\Http\Request as LaravelRequest; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Request; @@ -25,6 +27,7 @@ use Illuminate\Support\Facades\Validator; use libs\utils\HTMLCleaner; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\IMemberRepository; use models\oauth2\IResourceServerContext; use models\summit\IEventFeedbackRepository; use models\summit\ISpeakerRepository; @@ -45,9 +48,6 @@ use utils\FilterParserException; use utils\OrderParser; use utils\PagingInfo; use utils\PagingResponse; -use Illuminate\Http\Request as LaravelRequest; -use utils\ParseMultiPartFormDataInputStream; - /** * Class OAuth2SummitLocationsApiController * @package App\Http\Controllers @@ -79,6 +79,11 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController */ private $location_repository; + /** + * @var IMemberRepository + */ + private $member_repository; + /** * @var ILocationService */ @@ -89,6 +94,11 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController */ private $location_banners_repository; + /** + * @var IPaymentGatewayAPI + */ + private $payment_gateway; + /** * OAuth2SummitLocationsApiController constructor. * @param ISummitRepository $summit_repository @@ -97,8 +107,10 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController * @param IEventFeedbackRepository $event_feedback_repository * @param ISummitLocationRepository $location_repository * @param ISummitLocationBannerRepository $location_banners_repository + * @param IMemberRepository $member_repository * @param ISummitService $summit_service * @param ILocationService $location_service + * @param IPaymentGatewayAPI $payment_gateway * @param IResourceServerContext $resource_server_context */ public function __construct @@ -109,19 +121,23 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController IEventFeedbackRepository $event_feedback_repository, ISummitLocationRepository $location_repository, ISummitLocationBannerRepository $location_banners_repository, + IMemberRepository $member_repository, ISummitService $summit_service, ILocationService $location_service, + IPaymentGatewayAPI $payment_gateway, IResourceServerContext $resource_server_context ) { parent::__construct($resource_server_context); $this->repository = $summit_repository; $this->speaker_repository = $speaker_repository; $this->event_repository = $event_repository; + $this->member_repository = $member_repository; $this->event_feedback_repository = $event_feedback_repository; $this->location_repository = $location_repository; $this->location_banners_repository = $location_banners_repository; $this->location_service = $location_service; $this->summit_service = $summit_service; + $this->payment_gateway = $payment_gateway; } /** @@ -2211,4 +2227,8 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController } } + // bookable rooms + + use SummitBookableVenueRoomApi; + } \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/Traits/SummitBookableVenueRoomApi.php b/app/Http/Controllers/Apis/Protected/Summit/Traits/SummitBookableVenueRoomApi.php new file mode 100644 index 00000000..d6a19d3b --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Traits/SummitBookableVenueRoomApi.php @@ -0,0 +1,384 @@ + 'integer|min:1', + 'per_page' => sprintf('required_with:page|integer|min:%s|max:%s', PagingConstants::MinPageSize, PagingConstants::MaxPageSize), + ]; + + try { + + $summit = $summit_id === 'current' ? $this->repository->getCurrent() : $this->repository->getById(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $validation = Validator::make($values, $rules); + + if ($validation->fails()) { + $ex = new ValidationException(); + throw $ex->setMessages($validation->messages()->toArray()); + } + + // default values + $page = 1; + $per_page = PagingConstants::DefaultPageSize; + + if (Input::has('page')) { + $page = intval(Input::get('page')); + $per_page = intval(Input::get('per_page')); + } + + $filter = null; + + if (Input::has('filter')) { + $filter = FilterParser::parse(Input::get('filter'), [ + 'name' => ['==', '=@'], + 'description' => ['=@'], + 'capacity' => ['>', '<', '<=', '>=', '=='], + 'availability_day' => ['=='], + 'attribute' => ['=='], + ]); + } + if(is_null($filter)) $filter = new Filter(); + + $filter->validate([ + 'name' => 'sometimes|string', + 'description' => 'sometimes|string', + 'capacity' => 'sometimes|integer', + 'availability_day' => 'sometimes|date_format:U', + 'attribute' => 'sometimes|string', + ]); + + $order = null; + + if (Input::has('order')) + { + $order = OrderParser::parse(Input::get('order'), [ + 'id', + 'name', + 'capacity', + ]); + } + + $filter->addFilterCondition(FilterParser::buildFilter('class_name','==', SummitBookableVenueRoom::ClassName)); + + $data = $this->location_repository->getBySummit($summit, new PagingInfo($page, $per_page), $filter, $order); + + return $this->ok + ( + $data->toArray + ( + Request::input('expand', ''), + [], + [], + [] + ) + ); + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch (EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message' => $ex2->getMessage()]); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + public function getBookableVenueRoom($summit_id, $room_id){ + + } + + /** + * @param $summit_id + * @param $room_id + * @param $day + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getBookableVenueRoomAvailability($summit_id, $room_id, $day){ + try { + $day = intval($day); + $summit = $summit_id === 'current' ? $this->repository->getCurrent() : $this->repository->getById(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $room = $summit->getLocation($room_id); + + if(!$room instanceof SummitBookableVenueRoom) + return $this->error404(); + + $slots_definitions = $room->getAvailableSlots(new \DateTime("@$day")); + $list = []; + foreach($slots_definitions as $slot_label => $is_free){ + $dates = explode('|', $slot_label); + $list[] = new SummitBookableVenueRoomAvailableSlot + ( + $room, + $summit->convertDateFromTimeZone2UTC(new \DateTime($dates[0], $summit->getTimeZone())), + $summit->convertDateFromTimeZone2UTC(new \DateTime($dates[1], $summit->getTimeZone())), + $is_free + ); + } + + $response = new PagingResponse + ( + count($list), + count($list), + 1, + 1, + $list + ); + + return $this->ok( + $response->toArray() + ); + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch (EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message' => $ex2->getMessage()]); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $summit_id + * @param $room_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function createBookableVenueRoomReservation($summit_id, $room_id){ + try { + if(!Request::isJson()) return $this->error400(); + + $current_member_id = $this->resource_server_context->getCurrentUserExternalId(); + if (is_null($current_member_id)) + return $this->error403(); + + $summit = $summit_id === 'current' ? $this->repository->getCurrent() : $this->repository->getById(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $room = $summit->getLocation($room_id); + + if(!$room instanceof SummitBookableVenueRoom) + return $this->error404(); + + $payload = Input::json()->all(); + $payload['owner_id'] = $current_member_id; + $rules = SummitRoomReservationValidationRulesFactory::build($payload); + // Creates a Validator instance and validates the data. + $validation = Validator::make($payload, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + return $this->error412 + ( + $messages + ); + } + + $reservation = $this->location_service->addBookableRoomReservation($summit, $room_id, $payload); + + return $this->created(SerializerRegistry::getInstance()->getSerializer($reservation)->serialize()); + + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch (EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message' => $ex2->getMessage()]); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $summit_id + * @return mixed + */ + public function getMyBookableVenueRoomReservations($summit_id){ + try{ + $current_member_id = $this->resource_server_context->getCurrentUserExternalId(); + if (is_null($current_member_id)) + return $this->error403(); + + $summit = $summit_id === 'current' ? $this->repository->getCurrent() : $this->repository->getById(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $member = $this->member_repository->getById($current_member_id); + if(is_null($member)) + return $this->error403(); + + $reservations = $member->getReservations()->toArray(); + + $response = new PagingResponse + ( + count($reservations), + count($reservations), + 1, + 1, + $reservations + ); + + return $this->ok( + $response->toArray( + Request::input('expand', '') + ) + ); + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch (EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message' => $ex2->getMessage()]); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $summit_id + * @param $reservation_id + * @return mixed + */ + public function cancelMyBookableVenueRoomReservation($summit_id, $reservation_id){ + try{ + $current_member_id = $this->resource_server_context->getCurrentUserExternalId(); + if (is_null($current_member_id)) + return $this->error403(); + + $summit = $summit_id === 'current' ? $this->repository->getCurrent() : $this->repository->getById(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $member = $this->member_repository->getById($current_member_id); + if(is_null($member)) + return $this->error403(); + + $this->location_service->cancelReservation($summit, $member, $reservation_id); + + return $this->deleted(); + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch (EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message' => $ex2->getMessage()]); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param LaravelRequest $request + * @return mixed + */ + public function confirmBookableVenueRoomReservation(LaravelRequest $request){ + + if(!Request::isJson()) + return $this->error400(); + + try { + $response = $this->payment_gateway->processCallback($request); + $this->location_service->processBookableRoomPayment($response); + return $this->ok(); + } + catch (Exception $ex){ + Log::error($ex); + return $this->error400(["error" => 'payload error']); + } + return $this->error400(["error" => 'invalid event type']); + } +} \ No newline at end of file diff --git a/app/Http/Routes/public.php b/app/Http/Routes/public.php index 9f611948..784ee62d 100644 --- a/app/Http/Routes/public.php +++ b/app/Http/Routes/public.php @@ -51,6 +51,15 @@ Route::group([ Route::group(['prefix' => 'selection-plans'], function () { Route::get('current/{status}', 'OAuth2SummitSelectionPlansApiController@getCurrentSelectionPlanByStatus')->where('status', 'submission|selection|voting'); }); + + Route::group(['prefix' => 'bookable-rooms'], function () { + Route::group(['prefix' => 'all'], function () { + Route::group(['prefix' => 'reservations'], function () { + // api/public/v1/summits/all/bookable-rooms/all/reservations/confirm ( open endpoint for payment gateway callbacks) + Route::post("confirm", "OAuth2SummitLocationsApiController@confirmBookableVenueRoomReservation"); + }); + }); + }); }); Route::group(['prefix' => '{id}'], function () { diff --git a/app/Http/Utils/PagingResponse.php b/app/Http/Utils/PagingResponse.php index 02ee74b6..65c8826e 100644 --- a/app/Http/Utils/PagingResponse.php +++ b/app/Http/Utils/PagingResponse.php @@ -109,11 +109,7 @@ final class PagingResponse $items = []; foreach($this->items as $i) { - if($i instanceof IEntity) - { - $i = SerializerRegistry::getInstance()->getSerializer($i, $serializer_type)->serialize($expand, $fields, $relations, $params); - } - $items[] = $i; + $items[] = SerializerRegistry::getInstance()->getSerializer($i, $serializer_type)->serialize($expand, $fields, $relations, $params);; } return diff --git a/app/Http/routes.php b/app/Http/routes.php index bd964795..1f8f68cb 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -86,12 +86,15 @@ Route::group([ Route::group(array('prefix' => 'summits'), function () { Route::get('', [ 'middleware' => 'cache:'.Config::get('cache_api_response.get_summits_response_lifetime', 600), 'uses' => 'OAuth2SummitApiController@getSummits']); + Route::group(['prefix' => 'all'], function () { Route::get('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators|summit-room-administrators', 'uses' => 'OAuth2SummitApiController@getAllSummits']); + Route::get('{id}', 'OAuth2SummitApiController@getAllSummitByIdOrSlug'); Route::group(['prefix' => 'selection-plans'], function () { Route::get('current/{status}', ['uses' => 'OAuth2SummitSelectionPlansApiController@getCurrentSelectionPlanByStatus'])->where('status', 'submission|selection|voting'); }); }); + Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitApiController@addSummit']); Route::group(['prefix' => '{id}'], function () { @@ -312,10 +315,43 @@ Route::group([ Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@addLocation']); Route::get('metadata', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@getMetadata']); + + // bookable-rooms + Route::group(['prefix' => 'bookable-rooms'], function () { + // GET /api/v1/summits/{id}/bookable-rooms + Route::get('', 'OAuth2SummitLocationsApiController@getBookableVenueRooms'); + + Route::group(['prefix' => 'all'], function () { + Route::group(['prefix' => 'reservations'], function () { + // GET /api/v1/summits/{id}/bookable-rooms/all/reservations/me + Route::get('me', 'OAuth2SummitLocationsApiController@getMyBookableVenueRoomReservations'); + Route::group(['prefix' => '{reservation_id}'], function () { + // DELETE /api/v1/summits/{id}/bookable-rooms/all/reservations/{reservation_id} + Route::delete('', 'OAuth2SummitLocationsApiController@cancelMyBookableVenueRoomReservation'); + }); + }); + }); + + Route::group(['prefix' => '{room_id}'], function () { + // GET /api/v1/summits/{id}/locations/bookable-rooms/{room_id} + Route::get('', 'OAuth2SummitLocationsApiController@getBookableVenueRoom'); + // GET /api/v1/summits/{id}/locations/bookable-rooms/{room_id}/availability/{day} + Route::get('availability/{day}', 'OAuth2SummitLocationsApiController@getBookableVenueRoomAvailability'); + Route::group(['prefix' => 'reservations'], function () { + // POST /api/v1/summits/{id}/locations/bookable-rooms/{room_id}/reservations + Route::post('', 'OAuth2SummitLocationsApiController@createBookableVenueRoomReservation'); + }); + + }); + }); + + // venues + Route::group(['prefix' => 'venues'], function () { Route::get('', 'OAuth2SummitLocationsApiController@getVenues'); Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@addVenue']); + Route::group(['prefix' => '{venue_id}'], function () { Route::put('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@updateVenue']); @@ -328,6 +364,20 @@ Route::group([ }); }); + // bookable-rooms + Route::group(['prefix' => 'bookable-rooms'], function () { + // POST /api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms + Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@addBookableVenueRoom']); + Route::group(['prefix' => '{room_id}'], function () { + // GET /api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms/{room_id} + Route::get('', 'OAuth2SummitLocationsApiController@getBookableVenueRoom'); + // PUT /api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms/{room_id} + Route::put('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@updateBookableVenueRoom']); + // DELETE /api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms/{room_id} + Route::delete('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@deleteBookableVenueRoom']); + }); + }); + Route::group(['prefix' => 'floors'], function () { Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@addVenueFloor']); Route::group(['prefix' => '{floor_id}'], function () { diff --git a/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeTypeSerializer.php b/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeTypeSerializer.php new file mode 100644 index 00000000..af7fb253 --- /dev/null +++ b/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeTypeSerializer.php @@ -0,0 +1,25 @@ + 'type:json_string', + 'SummitId' => 'summit_id:json_int', + ]; +} \ No newline at end of file diff --git a/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeValueSerializer.php b/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeValueSerializer.php new file mode 100644 index 00000000..b4ee6edc --- /dev/null +++ b/app/ModelSerializers/Locations/SummitBookableVenueRoomAttributeValueSerializer.php @@ -0,0 +1,52 @@ + 'value:json_string', + 'TypeId' => 'type_id:json_int', + ]; + + public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array() ) + { + $attr_value = $this->object; + if(!$attr_value instanceof SummitBookableVenueRoomAttributeValue) + return []; + + $values = parent::serialize($expand, $fields, $relations, $params); + + if (!empty($expand)) { + $exp_expand = explode(',', $expand); + foreach ($exp_expand as $relation) { + switch (trim($relation)) { + case 'attribute_type': { + unset($values['type_id']); + $values['type'] = SerializerRegistry::getInstance()->getSerializer($attr_value->getType())->serialize(); + } + break; + + } + } + } + return $values; + } +} \ No newline at end of file diff --git a/app/ModelSerializers/Locations/SummitBookableVenueRoomAvailableSlotSerializer.php b/app/ModelSerializers/Locations/SummitBookableVenueRoomAvailableSlotSerializer.php new file mode 100644 index 00000000..3c7ccb5e --- /dev/null +++ b/app/ModelSerializers/Locations/SummitBookableVenueRoomAvailableSlotSerializer.php @@ -0,0 +1,28 @@ + 'start_date:datetime_epoch', + 'EndDate' => 'end_date:datetime_epoch', + 'LocalStartDate' => 'local_start_date:datetime_epoch', + 'LocalEndDate' => 'local_end_date:datetime_epoch', + 'Free' => 'is_free:json_boolean', + ]; +} \ No newline at end of file diff --git a/app/ModelSerializers/Locations/SummitBookableVenueRoomSerializer.php b/app/ModelSerializers/Locations/SummitBookableVenueRoomSerializer.php new file mode 100644 index 00000000..4b46b748 --- /dev/null +++ b/app/ModelSerializers/Locations/SummitBookableVenueRoomSerializer.php @@ -0,0 +1,43 @@ + 'time_slot_cost:json_float', + 'Currency' => 'currency:json_string', + ]; + + public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array() ) + { + $room = $this->object; + if(!$room instanceof SummitBookableVenueRoom) + return []; + + $values = parent::serialize($expand, $fields, $relations, $params); + + $attributes = []; + foreach ($room->getAttributes() as $attribute){ + $attributes[] = SerializerRegistry::getInstance()->getSerializer($attribute)->serialize($expand); + } + $values['attributes'] = $attributes; + return $values; + } +} \ No newline at end of file diff --git a/app/ModelSerializers/Locations/SummitRoomReservationSerializer.php b/app/ModelSerializers/Locations/SummitRoomReservationSerializer.php new file mode 100644 index 00000000..657d3746 --- /dev/null +++ b/app/ModelSerializers/Locations/SummitRoomReservationSerializer.php @@ -0,0 +1,73 @@ + 'room_id:json_int', + 'OwnerId' => 'owner_id:json_int', + 'Amount' => 'amount:json_int', + 'Currency' => 'currency:json_string', + 'Status' => 'status:json_string', + 'StartDatetime' => 'start_datetime:datetime_epoch', + 'EndDatetime' => 'end_datetime:datetime_epoch', + 'LocalStartDatetime' => 'local_start_datetime:datetime_epoch', + 'LocalEndDatetime' => 'local_end_datetime:datetime_epoch', + 'ApprovedPaymentDate' => 'approved_payment_date:datetime_epoch', + 'LastError' => 'last_error:json_string', + 'PaymentGatewayClientToken' => 'payment_gateway_client_token:json_string' + ]; + + /** + * @param null $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = []) + { + $reservation = $this->object; + if(!$reservation instanceof SummitRoomReservation) + return []; + + $values = parent::serialize($expand, $fields, $relations, $params); + + if (!empty($expand)) { + $exp_expand = explode(',', $expand); + foreach ($exp_expand as $relation) { + switch (trim($relation)) { + case 'room': { + unset($values['room_id']); + $values['room'] = SerializerRegistry::getInstance()->getSerializer($reservation->getRoom())->serialize($expand); + } + break; + case 'owner': { + unset($values['owner_id']); + $values['owner'] = SerializerRegistry::getInstance()->getSerializer($reservation->getOwner())->serialize($expand); + } + break; + } + } + } + + return $values; + } +} \ No newline at end of file diff --git a/app/ModelSerializers/Locations/SummitVenueRoomSerializer.php b/app/ModelSerializers/Locations/SummitVenueRoomSerializer.php index f75ac7f7..ae143455 100644 --- a/app/ModelSerializers/Locations/SummitVenueRoomSerializer.php +++ b/app/ModelSerializers/Locations/SummitVenueRoomSerializer.php @@ -12,12 +12,11 @@ * limitations under the License. **/ use ModelSerializers\SerializerRegistry; - /** * Class SummitVenueRoomSerializer * @package ModelSerializers\Locations */ -final class SummitVenueRoomSerializer extends SummitAbstractLocationSerializer +class SummitVenueRoomSerializer extends SummitAbstractLocationSerializer { protected static $array_mappings = [ 'VenueId' => 'venue_id:json_int', diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index 00547f91..ca867401 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -14,6 +14,11 @@ use App\ModelSerializers\CCLA\TeamSerializer; use App\ModelSerializers\FileSerializer; use App\ModelSerializers\LanguageSerializer; +use App\ModelSerializers\Locations\SummitBookableVenueRoomAttributeTypeSerializer; +use App\ModelSerializers\Locations\SummitBookableVenueRoomAttributeValueSerializer; +use App\ModelSerializers\Locations\SummitBookableVenueRoomAvailableSlotSerializer; +use App\ModelSerializers\Locations\SummitBookableVenueRoomSerializer; +use App\ModelSerializers\Locations\SummitRoomReservationSerializer; use App\ModelSerializers\Marketplace\CloudServiceOfferedSerializer; use App\ModelSerializers\Marketplace\ConfigurationManagementTypeSerializer; use App\ModelSerializers\Marketplace\ConsultantClientSerializer; @@ -191,15 +196,20 @@ final class SerializerRegistry $this->registry['SponsorSummitRegistrationPromoCode'] = SponsorSummitRegistrationPromoCodeSerializer::class; $this->registry['PresentationSpeakerSummitAssistanceConfirmationRequest'] = PresentationSpeakerSummitAssistanceConfirmationRequestSerializer::class; // locations - $this->registry['SummitVenue'] = SummitVenueSerializer::class; - $this->registry['SummitVenueRoom'] = SummitVenueRoomSerializer::class; - $this->registry['SummitVenueFloor'] = SummitVenueFloorSerializer::class; - $this->registry['SummitExternalLocation'] = SummitExternalLocationSerializer::class; - $this->registry['SummitHotel'] = SummitHotelSerializer::class; - $this->registry['SummitAirport'] = SummitAirportSerializer::class; - $this->registry['SummitLocationImage'] = SummitLocationImageSerializer::class; - $this->registry['SummitLocationBanner'] = SummitLocationBannerSerializer::class; - $this->registry['ScheduledSummitLocationBanner'] = ScheduledSummitLocationBannerSerializer::class; + $this->registry['SummitVenue'] = SummitVenueSerializer::class; + $this->registry['SummitVenueRoom'] = SummitVenueRoomSerializer::class; + $this->registry['SummitVenueFloor'] = SummitVenueFloorSerializer::class; + $this->registry['SummitExternalLocation'] = SummitExternalLocationSerializer::class; + $this->registry['SummitHotel'] = SummitHotelSerializer::class; + $this->registry['SummitAirport'] = SummitAirportSerializer::class; + $this->registry['SummitLocationImage'] = SummitLocationImageSerializer::class; + $this->registry['SummitLocationBanner'] = SummitLocationBannerSerializer::class; + $this->registry['ScheduledSummitLocationBanner'] = ScheduledSummitLocationBannerSerializer::class; + $this->registry['SummitBookableVenueRoom'] = SummitBookableVenueRoomSerializer::class; + $this->registry['SummitBookableVenueRoomAttributeType'] = SummitBookableVenueRoomAttributeTypeSerializer::class; + $this->registry['SummitBookableVenueRoomAttributeValue'] = SummitBookableVenueRoomAttributeValueSerializer::class; + $this->registry['SummitBookableVenueRoomAvailableSlot'] = SummitBookableVenueRoomAvailableSlotSerializer::class; + $this->registry['SummitRoomReservation'] = SummitRoomReservationSerializer::class; // track tag groups $this->registry['TrackTagGroup'] = TrackTagGroupSerializer::class; diff --git a/app/ModelSerializers/Summit/SummitSerializer.php b/app/ModelSerializers/Summit/SummitSerializer.php index 94cdc2b3..cf87a2f2 100644 --- a/app/ModelSerializers/Summit/SummitSerializer.php +++ b/app/ModelSerializers/Summit/SummitSerializer.php @@ -52,6 +52,10 @@ class SummitSerializer extends SilverStripeSerializer 'SecondaryRegistrationLink' => 'secondary_registration_link:json_string', 'SecondaryRegistrationLabel' => 'secondary_registration_label:json_string', 'RawSlug' => 'slug:json_string', + 'MeetingRoomBookingStartTime' => 'meeting_room_booking_start_time:datetime_epoch', + 'MeetingRoomBookingEndTime' => 'meeting_room_booking_end_time:datetime_epoch', + 'MeetingRoomBookingSlotLength' => 'meeting_room_booking_slot_length:json_int', + 'MeetingRoomBookingMaxAllowed' => 'meeting_room_booking_max_allowed:json_int', ]; protected static $allowed_relations = [ @@ -59,6 +63,7 @@ class SummitSerializer extends SilverStripeSerializer 'locations', 'wifi_connections', 'selection_plans', + 'meeting_booking_room_allowed_attributes', ]; /** @@ -110,7 +115,16 @@ class SummitSerializer extends SilverStripeSerializer $values['ticket_types'] = $ticket_types; } - //locations + // meeting_booking_room_allowed_attributes + if(in_array('meeting_booking_room_allowed_attributes', $relations)) { + $meeting_booking_room_allowed_attributes = []; + foreach ($summit->getMeetingBookingRoomAllowedAttributes() as $attr) { + $meeting_booking_room_allowed_attributes[] = SerializerRegistry::getInstance()->getSerializer($attr)->serialize($expand); + } + $values['meeting_booking_room_allowed_attributes'] = $meeting_booking_room_allowed_attributes; + } + + // locations if(in_array('locations', $relations)) { $locations = []; foreach ($summit->getLocations() as $location) { diff --git a/app/Models/Foundation/Main/Member.php b/app/Models/Foundation/Main/Member.php index 461b30a7..7847ef5d 100644 --- a/app/Models/Foundation/Main/Member.php +++ b/app/Models/Foundation/Main/Member.php @@ -24,6 +24,7 @@ use models\summit\RSVP; use models\summit\Summit; use models\summit\SummitEvent; use models\summit\SummitEventFeedback; +use models\summit\SummitRoomReservation; use models\utils\SilverstripeBaseModel; use Doctrine\ORM\Mapping AS ORM; /** @@ -207,6 +208,12 @@ class Member extends SilverstripeBaseModel */ private $favorites; + /** + * @ORM\OneToMany(targetEntity="models\summit\SummitRoomReservation", mappedBy="owner", cascade={"persist"}, orphanRemoval=true) + * @var ArrayCollection + */ + private $reservations; + /** * Member constructor. */ @@ -223,6 +230,7 @@ class Member extends SilverstripeBaseModel $this->rsvp = new ArrayCollection(); $this->calendars_sync = new ArrayCollection(); $this->schedule_sync_info = new ArrayCollection(); + $this->reservations = new ArrayCollection(); } /** @@ -1123,4 +1131,33 @@ SQL; } return $photoUrl; } + + /** + * @param SummitRoomReservation $reservation + * @return $this + */ + public function addReservation(SummitRoomReservation $reservation){ + if($this->reservations->contains($reservation)) return $this; + $this->reservations->add($reservation); + $reservation->setOwner($this); + return $this; + } + + /** + * @return ArrayCollection + */ + public function getReservations(){ + return $this->reservations; + } + + /** + * @param int $reservation_id + * @return SummitRoomReservation + */ + public function getReservationById(int $reservation_id): ?SummitRoomReservation { + $criteria = Criteria::create() + ->where(Criteria::expr()->eq("id", $reservation_id)); + + return $this->reservations->matching($criteria)->first(); + } } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Factories/SummitRoomReservationFactory.php b/app/Models/Foundation/Summit/Factories/SummitRoomReservationFactory.php new file mode 100644 index 00000000..1fa5d402 --- /dev/null +++ b/app/Models/Foundation/Summit/Factories/SummitRoomReservationFactory.php @@ -0,0 +1,53 @@ +setOwner($data['owner']); + if(isset($data['currency'])) + $reservation->setCurrency(trim($data['currency'])); + if(isset($data['amount'])) + $reservation->setAmount(floatval($data['amount'])); + + // dates ( they came on local time epoch , so must be converted to utc using + // summit timezone + if(isset($data['start_datetime'])) { + $val = intval($data['start_datetime']); + $val = new \DateTime("@$val", $summit->getTimeZone()); + $reservation->setStartDatetime($summit->convertDateFromTimeZone2UTC($val)); + } + + if(isset($data['end_datetime'])){ + $val = intval($data['end_datetime']); + $val = new \DateTime("@$val", $summit->getTimeZone()); + $reservation->setEndDatetime($summit->convertDateFromTimeZone2UTC($val)); + } + + return $reservation; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitAbstractLocation.php b/app/Models/Foundation/Summit/Locations/SummitAbstractLocation.php index a597f11e..5a90ec75 100644 --- a/app/Models/Foundation/Summit/Locations/SummitAbstractLocation.php +++ b/app/Models/Foundation/Summit/Locations/SummitAbstractLocation.php @@ -40,7 +40,16 @@ use Doctrine\ORM\Mapping AS ORM; * @ORM\Table(name="SummitAbstractLocation") * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="ClassName", type="string") - * @ORM\DiscriminatorMap({"SummitAbstractLocation" = "SummitAbstractLocation", "SummitGeoLocatedLocation" = "SummitGeoLocatedLocation", "SummitExternalLocation" = "SummitExternalLocation", "SummitVenue" = "SummitVenue", "SummitHotel" = "SummitHotel", "SummitAirport" = "SummitAirport", "SummitVenueRoom" = "SummitVenueRoom"}) + * @ORM\DiscriminatorMap({ + * "SummitAbstractLocation" = "SummitAbstractLocation", + * "SummitGeoLocatedLocation" = "SummitGeoLocatedLocation", + * "SummitExternalLocation" = "SummitExternalLocation", + * "SummitVenue" = "SummitVenue", + * "SummitHotel" = "SummitHotel", + * "SummitAirport" = "SummitAirport", + * "SummitVenueRoom" = "SummitVenueRoom", + * "SummitBookableVenueRoom" = "SummitBookableVenueRoom" + * }) * Class SummitAbstractLocation * @package models\summit */ diff --git a/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoom.php b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoom.php new file mode 100644 index 00000000..a33a3ac8 --- /dev/null +++ b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoom.php @@ -0,0 +1,280 @@ +reservations = new ArrayCollection(); + $this->attributes = new ArrayCollection(); + } + + /** + * @param SummitRoomReservation $reservation + * @return $this + * @throws ValidationException + */ + public function addReservation(SummitRoomReservation $reservation){ + $criteria = Criteria::create(); + + $start_date = $reservation->getStartDatetime(); + $end_date = $reservation->getEndDatetime(); + + $criteria + ->where(Criteria::expr()->eq('start_datetime', $start_date)) + ->andWhere(Criteria::expr()->eq('end_datetime',$end_date)) + ->andWhere(Criteria::expr()->notIn("status", [SummitRoomReservation::RequestedRefundStatus, SummitRoomReservation::RefundedStatus])); + + if($this->reservations->matching($criteria)->count() > 0) + throw new ValidationException(sprintf("reservation overlaps an existent reservation")); + + $criteria + ->where(Criteria::expr()->lte('start_datetime', $end_date)) + ->andWhere(Criteria::expr()->gte('end_datetime', $start_date)) + ->andWhere(Criteria::expr()->notIn("status", [SummitRoomReservation::RequestedRefundStatus, SummitRoomReservation::RefundedStatus])); + + if($this->reservations->matching($criteria)->count() > 0) + throw new ValidationException(sprintf("reservation overlaps an existent reservation")); + + $summit = $this->summit; + + $local_start_date = $summit->convertDateFromUTC2TimeZone($start_date); + $local_end_date = $summit->convertDateFromUTC2TimeZone($end_date); + $start_time = $summit->getMeetingRoomBookingStartTime(); + $end_time = $summit->getMeetingRoomBookingEndTime(); + + if(!$summit->isTimeFrameInsideSummitDuration($local_start_date, $local_end_date)) + throw new ValidationException("requested reservation period does not belong to summit period"); + $local_start_time = new \DateTime("now", $this->summit->getTimeZone()); + $local_start_time->setTime( + intval($start_time->format("H")), + intval($start_time->format("i")), + intval($start_time->format("s")) + ); + + $local_end_time = new \DateTime("now", $this->summit->getTimeZone()); + $local_end_time->setTime( + intval($end_time->format("H")), + intval($end_time->format("i")), + intval($end_time->format("s")) + ); + + $local_start_time->setDate + ( + intval($start_date->format("Y")), + intval($start_date->format("m")), + intval($start_date->format("d")) + ); + + $local_end_time->setDate + ( + intval($start_date->format("Y")), + intval($start_date->format("m")), + intval($start_date->format("d")) + ); + + if(!($local_start_time <= $local_start_date + && $local_end_date <= $local_end_time)) + throw new ValidationException("requested booking time slot is not allowed!"); + + $interval = $end_date->diff($start_date); + $minutes = ($interval->d * 24 * 60) + ($interval->h * 60) + $interval->i; + if($minutes != $summit->getMeetingRoomBookingSlotLength()) + throw new ValidationException("requested booking time slot is not allowed!"); + + $this->reservations->add($reservation); + $reservation->setRoom($this); + return $this; + } + + /** + * @return float + */ + public function getTimeSlotCost(): float + { + return floatval($this->time_slot_cost); + } + + /** + * @param float $time_slot_cost + */ + public function setTimeSlotCost(float $time_slot_cost): void + { + $this->time_slot_cost = $time_slot_cost; + } + + /** + * @return ArrayCollection + */ + public function getReservations(): ArrayCollection + { + return $this->reservations; + } + + public function clearReservations(){ + $this->reservations->clear(); + } + + /** + * @return string + */ + public function getClassName() + { + return self::ClassName; + } + + /** + * @param \DateTime $day should be on local summit day + * @return array + * @throws ValidationException + */ + public function getAvailableSlots(\DateTime $day):array{ + $availableSlots = []; + $summit = $this->summit; + $day = $day->setTimezone($summit->getTimeZone())->setTime(0, 0,0); + $booking_start_time = $summit->getMeetingRoomBookingStartTime(); + if(is_null($booking_start_time)) + throw new ValidationException("MeetingRoomBookingStartTime is null!"); + + $booking_end_time = $summit->getMeetingRoomBookingEndTime(); + if(is_null($booking_end_time)) + throw new ValidationException("MeetingRoomBookingEndTime is null!"); + + $booking_slot_len = $summit->getMeetingRoomBookingSlotLength(); + $start_datetime = clone $day; + $end_datetime = clone $day; + + $start_datetime->setTime( + intval($booking_start_time->format("H")), + intval($booking_start_time->format("i")), + 0); + $start_datetime->setTimezone($summit->getTimeZone()); + + $end_datetime->setTime( + intval($booking_end_time->format("H")), + intval($booking_end_time->format("i")), + 00); + $end_datetime->setTimezone($summit->getTimeZone()); + $criteria = Criteria::create(); + if(!$summit->isTimeFrameInsideSummitDuration($start_datetime, $end_datetime)) + throw new ValidationException("requested day does not belong to summit period"); + + $criteria + ->where(Criteria::expr()->gte('start_datetime', $summit->convertDateFromTimeZone2UTC($start_datetime))) + ->andWhere(Criteria::expr()->lte('end_datetime', $summit->convertDateFromTimeZone2UTC($end_datetime))) + ->andWhere(Criteria::expr()->notIn("status", [SummitRoomReservation::RequestedRefundStatus, SummitRoomReservation::RefundedStatus])); + + $reservations = $this->reservations->matching($criteria); + + while($start_datetime <= $end_datetime) { + $current_time_slot_end = clone $start_datetime; + $current_time_slot_end->add(new \DateInterval("PT" . $booking_slot_len . 'M')); + if($current_time_slot_end<=$end_datetime) + $availableSlots[$start_datetime->format('Y-m-d H:i:s').'|'.$current_time_slot_end->format('Y-m-d H:i:s')] = true; + $start_datetime = $current_time_slot_end; + } + + foreach ($reservations as $reservation){ + if(!$reservation instanceof SummitRoomReservation) continue; + $availableSlots[ + $summit->convertDateFromUTC2TimeZone($reservation->getStartDatetime())->format("Y-m-d H:i:s") + .'|'. + $summit->convertDateFromUTC2TimeZone($reservation->getEndDatetime())->format("Y-m-d H:i:s") + ] = false; + } + + return $availableSlots; + } + + /** + * @param \DateTime $day + * @return array + * @throws ValidationException + */ + public function getFreeSlots(\DateTime $day):array{ + $slots = $this->getAvailableSlots($day); + $free_slots = []; + foreach ($slots as $label => $status){ + if(!$status) continue; + $free_slots[] = $label; + } + return $free_slots; + } + + /** + * @return string + */ + public function getCurrency(): string + { + return $this->currency; + } + + /** + * @param string $currency + */ + public function setCurrency(string $currency): void + { + $this->currency = $currency; + } + + /** + * @return mixed + */ + public function getAttributes() + { + return $this->attributes; + } + + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeType.php b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeType.php new file mode 100644 index 00000000..d4411752 --- /dev/null +++ b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeType.php @@ -0,0 +1,85 @@ +values = new ArrayCollection(); + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType(string $type): void + { + $this->type = $type; + } + + /** + * @return ArrayCollection + */ + public function getValues(): ArrayCollection + { + return $this->values; + } + + /** + * @param ArrayCollection $values + */ + public function setValues(ArrayCollection $values): void + { + $this->values = $values; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeValue.php b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeValue.php new file mode 100644 index 00000000..27d2b623 --- /dev/null +++ b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAttributeValue.php @@ -0,0 +1,84 @@ +value; + } + + /** + * @param string $value + */ + public function setValue(string $value): void + { + $this->value = $value; + } + + /** + * @return SummitBookableVenueRoomAttributeType + */ + public function getType(): SummitBookableVenueRoomAttributeType + { + return $this->type; + } + + /** + * @param SummitBookableVenueRoomAttributeType $type + */ + public function setType(SummitBookableVenueRoomAttributeType $type): void + { + $this->type = $type; + } + + /** + * @return int + */ + public function getTypeId():int{ + try { + return is_null($this->type) ? 0 : $this->type->getId(); + } + catch(\Exception $ex){ + return 0; + } + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAvailableSlot.php b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAvailableSlot.php new file mode 100644 index 00000000..2643ef6c --- /dev/null +++ b/app/Models/Foundation/Summit/Locations/SummitBookableVenueRoomAvailableSlot.php @@ -0,0 +1,88 @@ +room = $room; + $this->start_date = $start_date; + $this->end_date = $end_date; + $this->is_free = $is_free; + } + + /** + * @return \DateTime + */ + public function getStartDate(): \DateTime + { + return $this->start_date; + } + + /** + * @return \DateTime + */ + public function getEndDate(): \DateTime + { + return $this->end_date; + } + + /** + * @return \DateTime + */ + public function getLocalStartDate(): \DateTime + { + return $this->room->getSummit()->convertDateFromUTC2TimeZone($this->start_date); + } + + /** + * @return \DateTime + */ + public function getLocalEndDate(): \DateTime + { + return $this->room->getSummit()->convertDateFromUTC2TimeZone($this->end_date); + } + + public function isFree():bool{ + return $this->is_free; + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitRoomReservation.php b/app/Models/Foundation/Summit/Locations/SummitRoomReservation.php new file mode 100644 index 00000000..4aa21ef1 --- /dev/null +++ b/app/Models/Foundation/Summit/Locations/SummitRoomReservation.php @@ -0,0 +1,329 @@ +start_datetime; + } + + /** + * @return \DateTime + */ + public function getLocalStartDatetime(): \DateTime + { + return $this->room->getSummit()->convertDateFromUTC2TimeZone($this->start_datetime); + } + + /** + * @param \DateTime $start_datetime + */ + public function setStartDatetime(\DateTime $start_datetime): void + { + $this->start_datetime = $start_datetime; + } + + /** + * @return \DateTime + */ + public function getEndDatetime(): \DateTime + { + return $this->end_datetime; + } + + /** + * @return \DateTime + */ + public function getLocalEndDatetime(): \DateTime + { + return $this->room->getSummit()->convertDateFromUTC2TimeZone($this->end_datetime); + } + + /** + * @param \DateTime $end_datetime + */ + public function setEndDatetime(\DateTime $end_datetime): void + { + $this->end_datetime = $end_datetime; + } + + /** + * @return string + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * @param string $status + */ + public function setStatus(string $status): void + { + $this->status = $status; + } + + /** + * @return Member + */ + public function getOwner(): Member + { + return $this->owner; + } + + /** + * @param Member $owner + */ + public function setOwner(Member $owner): void + { + $this->owner = $owner; + } + + /** + * @return SummitBookableVenueRoom + */ + public function getRoom(): SummitBookableVenueRoom + { + return $this->room; + } + + /** + * @param SummitBookableVenueRoom $room + */ + public function setRoom(SummitBookableVenueRoom $room): void + { + $this->room = $room; + } + + /** + * @return string + */ + public function getPaymentGatewayCartId(): string + { + return $this->payment_gateway_cart_id; + } + + /** + * @param string $payment_gateway_cart_id + */ + public function setPaymentGatewayCartId(string $payment_gateway_cart_id): void + { + $this->payment_gateway_cart_id = $payment_gateway_cart_id; + } + + /** + * @return \DateTime|null + */ + public function getApprovedPaymentDate(): ?\DateTime + { + return $this->approved_payment_date; + } + + /** + * @param \DateTime $approved_payment_date + */ + public function setApprovedPaymentDate(\DateTime $approved_payment_date): void + { + $this->approved_payment_date = $approved_payment_date; + } + + /** + * @return string + */ + public function getCurrency(): string + { + return $this->currency; + } + + /** + * @param string $currency + */ + public function setCurrency(string $currency): void + { + $this->currency = $currency; + } + + /** + * @return float + */ + public function getAmount(): float + { + return $this->amount; + } + + /** + * @param float $amount + */ + public function setAmount(float $amount): void + { + $this->amount = $amount; + } + + public function __construct() + { + parent::__construct(); + $this->amount = 0.0; + $this->status = self::ReservedStatus; + } + + /** + * @return string|null + */ + public function getPaymentGatewayClientToken(): ?string + { + return $this->payment_gateway_client_token; + } + + /** + * @param string $payment_gateway_client_token + */ + public function setPaymentGatewayClientToken(string $payment_gateway_client_token): void + { + $this->payment_gateway_client_token = $payment_gateway_client_token; + } + + public function setPayed():void{ + $this->status = self::PayedStatus; + $now = new \DateTime('now', new \DateTimeZone('UTC')); + $this->approved_payment_date = $now; + } + + public function requestRefund():void{ + $this->status = self::RequestedRefundStatus; + } + + public function setPaymentError(string $error):void{ + $this->status = self::ErrorStatus; + $this->last_error = $error; + } + + /** + * @return int + */ + public function getOwnerId(){ + try { + return is_null($this->owner) ? 0 : $this->owner->getId(); + } + catch(\Exception $ex){ + return 0; + } + } + + /** + * @return int + */ + public function getRoomId(){ + try { + return is_null($this->room) ? 0 : $this->room->getId(); + } + catch(\Exception $ex){ + return 0; + } + } + + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitVenue.php b/app/Models/Foundation/Summit/Locations/SummitVenue.php index 7f777167..9685dd1a 100644 --- a/app/Models/Foundation/Summit/Locations/SummitVenue.php +++ b/app/Models/Foundation/Summit/Locations/SummitVenue.php @@ -16,7 +16,6 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping AS ORM; use Doctrine\Common\Collections\ArrayCollection; use models\exceptions\ValidationException; - /** * @ORM\Entity * @ORM\Table(name="SummitVenue") diff --git a/app/Models/Foundation/Summit/Repositories/ISummitRepository.php b/app/Models/Foundation/Summit/Repositories/ISummitRepository.php index 8e18dda7..c550e52d 100644 --- a/app/Models/Foundation/Summit/Repositories/ISummitRepository.php +++ b/app/Models/Foundation/Summit/Repositories/ISummitRepository.php @@ -53,4 +53,10 @@ interface ISummitRepository extends IBaseRepository * @return Summit[] */ public function getCurrentAndFutureSummits(); + + /** + * @param string $slug + * @return Summit + */ + public function getBySlug(string $slug):Summit; } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Repositories/ISummitRoomReservationRepository.php b/app/Models/Foundation/Summit/Repositories/ISummitRoomReservationRepository.php new file mode 100644 index 00000000..a9b4baad --- /dev/null +++ b/app/Models/Foundation/Summit/Repositories/ISummitRoomReservationRepository.php @@ -0,0 +1,27 @@ +track_tag_groups = new ArrayCollection; $this->notifications = new ArrayCollection; $this->selection_plans = new ArrayCollection; + $this->meeting_booking_room_allowed_attributes = new ArrayCollection(); } /** @@ -641,6 +671,17 @@ class Summit extends SilverstripeBaseModel }); } + /** + * @return SummitBookableVenueRoom[] + */ + public function getBookableRooms() + { + return $this->locations->filter(function ($e) { + return $e instanceof SummitBookableVenueRoom; + }); + } + + /** * @return SummitAirport[] */ @@ -2420,4 +2461,85 @@ SQL; public function setRawSlug(string $slug):void{ $this->slug = $slug; } + + /** + * @return DateTime + */ + public function getMeetingRoomBookingStartTime():?DateTime + { + return $this->meeting_room_booking_start_time; + } + + /** + * @param DateTime $meeting_room_booking_start_time + */ + public function setMeetingRoomBookingStartTime(DateTime $meeting_room_booking_start_time): void + { + $this->meeting_room_booking_start_time = $meeting_room_booking_start_time; + } + + /** + * @return DateTime + */ + public function getMeetingRoomBookingEndTime():?DateTime + { + return $this->meeting_room_booking_end_time; + } + + /** + * @param DateTime $meeting_room_booking_end_time + */ + public function setMeetingRoomBookingEndTime(DateTime $meeting_room_booking_end_time): void + { + $this->meeting_room_booking_end_time = $meeting_room_booking_end_time; + } + + /** + * @return int + */ + public function getMeetingRoomBookingSlotLength(): int + { + return $this->meeting_room_booking_slot_length; + } + + /** + * @param int $meeting_room_booking_slot_length + */ + public function setMeetingRoomBookingSlotLength(int $meeting_room_booking_slot_length): void + { + $this->meeting_room_booking_slot_length = $meeting_room_booking_slot_length; + } + + /** + * @return int + */ + public function getMeetingRoomBookingMaxAllowed(): int + { + return $this->meeting_room_booking_max_allowed; + } + + /** + * @param int $meeting_room_booking_max_allowed + */ + public function setMeetingRoomBookingMaxAllowed(int $meeting_room_booking_max_allowed): void + { + $this->meeting_room_booking_max_allowed = $meeting_room_booking_max_allowed; + } + + /** + * @return mixed + */ + public function getMeetingBookingRoomAllowedAttributes() + { + return $this->meeting_booking_room_allowed_attributes; + } + + public function getMaxReservationsPerDay():int { + $interval = $this->meeting_room_booking_end_time->diff( $this->meeting_room_booking_start_time); + $minutes = $interval->days * 24 * 60; + $minutes += $interval->h * 60; + $minutes += $interval->i; + return intval ($minutes / $this->meeting_room_booking_slot_length); + } + } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0c09f622..7cb221a7 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -597,8 +597,27 @@ class AppServiceProvider extends ServiceProvider $value = trim($value); return isset($countries[$value]); }); + + Validator::extend('currency_iso', function($attribute, $value, $parameters, $validator) + { + $currencies = + [ + 'USD' => 'USD', + 'EUR' => 'EUR', + 'GBP' => 'GBP' + ]; + + $validator->addReplacer('currency_iso', function($message, $attribute, $rule, $parameters) use ($validator) { + return sprintf("%s should be a valid currency iso 4217 code", $attribute); + }); + if(!is_string($value)) return false; + $value = trim($value); + return isset($currencies[$value]); + }); } + + /** * Register any application services. * @return void diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 954c9a39..588786c1 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -1,10 +1,22 @@ - 'al.name:json_string', - 'description' => 'al.description:json_string', - 'address_1' => 'gll.address1:json_string', - 'address_2' => 'gll.address2:json_string', - 'zip_code' => 'gll.zip_code:json_string', - 'city' => 'gll.city:json_string', - 'state' => 'gll.state:json_string', - 'country' => 'gll.country:json_string', - 'sold_out' => 'h.sold_out:json_boolean', - 'is_main' => 'v.is_main:json_boolean', - 'class_name' => new DoctrineInstanceOfFilterMapping( + 'name' => 'al.name:json_string', + 'description' => 'al.description:json_string', + 'address_1' => 'gll.address1:json_string', + 'address_2' => 'gll.address2:json_string', + 'zip_code' => 'gll.zip_code:json_string', + 'city' => 'gll.city:json_string', + 'state' => 'gll.state:json_string', + 'country' => 'gll.country:json_string', + 'sold_out' => 'h.sold_out:json_boolean', + 'is_main' => 'v.is_main:json_boolean', + 'time_slot_cost' => 'br.time_slot_cost', + 'currency' => 'br.currency', + 'capacity' => 'r.capacity', + 'attribute' => new DoctrineFilterMapping + ( + "(bra.value :operator ':value' or bra.id = ':value')" + ), + 'class_name' => new DoctrineInstanceOfFilterMapping( "al", [ - SummitVenue::ClassName => SummitVenue::class, - SummitHotel::ClassName => SummitHotel::class, - SummitExternalLocation::ClassName => SummitExternalLocation::class, - SummitAirport::ClassName => SummitAirport::class, + SummitVenue::ClassName => SummitVenue::class, + SummitHotel::ClassName => SummitHotel::class, + SummitExternalLocation::ClassName => SummitExternalLocation::class, + SummitAirport::ClassName => SummitAirport::class, + SummitBookableVenueRoom::ClassName => SummitBookableVenueRoom::class, ] ) ]; @@ -109,6 +120,9 @@ final class DoctrineSummitLocationRepository ->leftJoin(SummitExternalLocation::class, 'el', 'WITH', 'el.id = gll.id') ->leftJoin(SummitHotel::class, 'h', 'WITH', 'h.id = el.id') ->leftJoin(SummitAirport::class, 'ap', 'WITH', 'ap.id = el.id') + ->leftJoin(SummitVenueRoom::class, 'r', 'WITH', 'r.id = al.id') + ->leftJoin(SummitBookableVenueRoom::class, 'br', 'WITH', 'br.id = al.id') + ->leftJoin('br.attributes', 'bra') ->leftJoin('al.summit', 's') ->where("s.id = :summit_id"); @@ -133,6 +147,28 @@ final class DoctrineSummitLocationRepository $query = $query->addOrderBy("al.id",'ASC'); } + if($filter->hasFilter("availability_day")){ + // special case, we need to figure if each room has available slots + $res = $query->getQuery()->execute(); + $rooms = []; + $availability_day = $filter->getUniqueFilter("availability_day")->getValue(); + $day = new \DateTime("@$availability_day"); + foreach ($res as $room){ + if(!$room instanceof SummitBookableVenueRoom) continue; + if(count($room->getFreeSlots($day)) > 0) + $rooms[] = $room; + } + + return new PagingResponse + ( + count($rooms), + $paging_info->getPerPage(), + $paging_info->getCurrentPage(), + $paging_info->getLastPage(count($rooms)), + array_slice( $rooms, $paging_info->getOffset(), $paging_info->getPerPage() ) + ); + } + $query = $query ->setFirstResult($paging_info->getOffset()) ->setMaxResults($paging_info->getPerPage()); diff --git a/app/Repositories/Summit/DoctrineSummitRepository.php b/app/Repositories/Summit/DoctrineSummitRepository.php index 63d2006b..2fec2b4e 100644 --- a/app/Repositories/Summit/DoctrineSummitRepository.php +++ b/app/Repositories/Summit/DoctrineSummitRepository.php @@ -105,6 +105,21 @@ final class DoctrineSummitRepository ->getOneOrNullResult(); } + /** + * @param string $slug + * @return Summit + */ + public function getBySlug(string $slug):Summit + { + return $this->getEntityManager()->createQueryBuilder() + ->select("s") + ->from(\models\summit\Summit::class, "s") + ->where('s.slug = :slug') + ->setParameter('slug', $slug) + ->getQuery() + ->getOneOrNullResult(); + } + /** * @return Summit */ diff --git a/app/Repositories/Summit/DoctrineSummitRoomReservationRepository.php b/app/Repositories/Summit/DoctrineSummitRoomReservationRepository.php new file mode 100644 index 00000000..a4816cdd --- /dev/null +++ b/app/Repositories/Summit/DoctrineSummitRoomReservationRepository.php @@ -0,0 +1,42 @@ +findOneBy(["payment_gateway_cart_id" => trim($payment_gateway_cart_id)]); + } +} \ No newline at end of file diff --git a/app/Security/SummitScopes.php b/app/Security/SummitScopes.php index fe2ff9c0..018772ee 100644 --- a/app/Security/SummitScopes.php +++ b/app/Security/SummitScopes.php @@ -18,47 +18,44 @@ */ final class SummitScopes { - const ReadSummitData = '%s/summits/read'; - const ReadAllSummitData = '%s/summits/read/all'; - const ReadNotifications = '%s/summits/read-notifications'; - const WriteNotifications = '%s/summits/write-notifications'; + const ReadSummitData = '%s/summits/read'; + const ReadAllSummitData = '%s/summits/read/all'; - const WriteSummitData = '%s/summits/write'; - const WriteSpeakersData = '%s/speakers/write'; - const ReadSpeakersData = '%s/speakers/read'; - const WriteTrackTagGroupsData = '%s/track-tag-groups/write'; - const WriteTrackQuestionTemplateData = '%s/track-question-templates/write'; - const WriteMySpeakersData = '%s/speakers/write/me'; - const ReadMySpeakersData = '%s/speakers/read/me'; + // bookable rooms + const ReadBookableRoomsData = '%s/bookable-rooms/read'; + const WriteMyBookableRoomsReservationData = '%s/bookable-rooms/my-reservations/write'; + const ReadMyBookableRoomsReservationData = '%s/bookable-rooms/my-reservations/read'; + const BookableRoomsReservation = '%s/bookable-rooms/reserve'; + const WriteBookableRoomsData = '%s/bookable-rooms/write'; - const PublishEventData = '%s/summits/publish-event'; - const WriteEventData = '%s/summits/write-event'; - const WriteVideoData = '%s/summits/write-videos'; - const WritePresentationVideosData = '%s/summits/write-presentation-videos'; - const WritePresentationLinksData = '%s/summits/write-presentation-links'; - const WritePresentationSlidesData = '%s/summits/write-presentation-slides'; - const WritePresentationMaterialsData = '%s/summits/write-presentation-materials'; + const ReadNotifications = '%s/summits/read-notifications'; + const WriteNotifications = '%s/summits/write-notifications'; + const WriteSummitData = '%s/summits/write'; + const WriteSpeakersData = '%s/speakers/write'; + const ReadSpeakersData = '%s/speakers/read'; + const WriteTrackTagGroupsData = '%s/track-tag-groups/write'; + const WriteTrackQuestionTemplateData = '%s/track-question-templates/write'; + const WriteMySpeakersData = '%s/speakers/write/me'; + const ReadMySpeakersData = '%s/speakers/read/me'; - const WriteAttendeesData = '%s/attendees/write'; + const PublishEventData = '%s/summits/publish-event'; + const WriteEventData = '%s/summits/write-event'; + const WriteVideoData = '%s/summits/write-videos'; + const WritePresentationVideosData = '%s/summits/write-presentation-videos'; + const WritePresentationLinksData = '%s/summits/write-presentation-links'; + const WritePresentationSlidesData = '%s/summits/write-presentation-slides'; + const WritePresentationMaterialsData = '%s/summits/write-presentation-materials'; - const WritePromoCodeData = '%s/promo-codes/write'; - - const WriteEventTypeData = '%s/event-types/write'; - - const WriteTracksData = '%s/tracks/write'; - - const WriteTrackGroupsData = '%s/track-groups/write'; - - const WriteLocationsData = '%s/locations/write'; - - const WriteRSVPTemplateData = '%s/rsvp-templates/write'; - - const WriteLocationBannersData = '%s/locations/banners/write'; - - const WriteSummitSpeakerAssistanceData = '%s/summit-speaker-assistance/write'; - - const WriteTicketTypeData = '%s/ticket-types/write'; - - const WritePresentationData = '%s/summits/write-presentation'; + const WriteAttendeesData = '%s/attendees/write'; + const WritePromoCodeData = '%s/promo-codes/write'; + const WriteEventTypeData = '%s/event-types/write'; + const WriteTracksData = '%s/tracks/write'; + const WriteTrackGroupsData = '%s/track-groups/write'; + const WriteLocationsData = '%s/locations/write'; + const WriteRSVPTemplateData = '%s/rsvp-templates/write'; + const WriteLocationBannersData = '%s/locations/banners/write'; + const WriteSummitSpeakerAssistanceData = '%s/summit-speaker-assistance/write'; + const WriteTicketTypeData = '%s/ticket-types/write'; + const WritePresentationData = '%s/summits/write-presentation'; } \ No newline at end of file diff --git a/app/Services/Apis/IPaymentGatewayAPI.php b/app/Services/Apis/IPaymentGatewayAPI.php new file mode 100644 index 00000000..3c774abf --- /dev/null +++ b/app/Services/Apis/IPaymentGatewayAPI.php @@ -0,0 +1,52 @@ +api_key = $api_key; + $this->webhook_secret = $webhook_secret; + } + + /** + * @param SummitRoomReservation $reservation + * @return array + */ + public function generatePayment(SummitRoomReservation $reservation):array + { + if(empty($this->api_key)) + throw new \InvalidArgumentException(); + + Stripe::setApiKey($this->api_key); + + $intent = PaymentIntent::create([ + 'amount' => $reservation->getAmount(), + 'currency' => $reservation->getCurrency(), + 'receipt_email' => $reservation->getOwner()->getEmail() + ]); + + return [ + 'client_token' => $intent->client_secret, + 'cart_id' => $intent->id, + ]; + } + + /** + * @param LaravelRequest $request + * @return array + * @throws SignatureVerification + * @throws \Exception + * @throws \InvalidArgumentException + */ + public function processCallback(LaravelRequest $request): array + { + try { + + WebhookSignature::verifyHeader( + $request->getContent(), + $request->header('Stripe-Signature'), + $this->webhook_secret + ); + + $event = Event::constructFrom(json_decode($request->getContent(), true)); + if(!in_array($event->type, ["payment_intent.succeeded", "payment_intent.payment_failed"])) + throw new \InvalidArgumentException(); + + $intent = $event->data->object; + if ($event->type == "payment_intent.succeeded") { + return [ + "event_type" => $event->type, + "cart_id" => $intent->id, + ]; + } + + if ($event->type == "payment_intent.payment_failed") { + $intent = $event->data->object; + return [ + "event_type" => $event->type, + "cart_id" => $intent->id, + "error" => [ + "last_payment_error" => $intent->last_payment_error, + "message" => $intent->last_payment_error->message + ] + ]; + } + } + catch(\UnexpectedValueException $e) { + // Invalid payload + throw $e; + } + catch(SignatureVerification $e) { + // Invalid signature + throw $e; + } + catch (\Exception $e){ + throw $e; + } + } + + /** + * @param array $payload + * @return bool + */ + public function isSuccessFullPayment(array $payload): bool + { + if(isset($payload['type']) && $payload['type'] == "payment_intent.succeeded") return true; + return false; + } + + /** + * @param array $payload + * @return string + */ + public function getPaymentError(array $payload): string + { + if(isset($payload['type']) && $payload['type'] == "payment_intent.payment_failed"){ + $error_message = $payload['error']["message"]; + return $error_message; + } + return null; + } + + /** + * @param string $cart_id + * @param int $amount + * @throws \InvalidArgumentException + */ + public function refundPayment(string $cart_id, int $amount = 0): void + { + if(empty($this->api_key)) + throw new \InvalidArgumentException(); + + Stripe::setApiKey($this->api_key); + $intent = PaymentIntent::retrieve($cart_id); + + if(is_null($intent)) + throw new \InvalidArgumentException(); + $charge = $intent->charges->data[0]; + if(!$charge instanceof Charge) + throw new \InvalidArgumentException(); + $params = []; + if($amount > 0 ){ + $params['amount'] = $amount; + } + $charge->refund($params); + } +} \ No newline at end of file diff --git a/app/Services/Model/ILocationService.php b/app/Services/Model/ILocationService.php index fdd991f6..be7e65e3 100644 --- a/app/Services/Model/ILocationService.php +++ b/app/Services/Model/ILocationService.php @@ -12,7 +12,9 @@ * limitations under the License. **/ use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; +use models\main\Member; use models\summit\SummitLocationImage; +use models\summit\SummitRoomReservation; use models\summit\SummitVenueRoom; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; @@ -213,4 +215,30 @@ interface ILocationService */ public function deleteLocationImage(Summit $summit, $location_id, $image_id); + /** + * @param Summit $summit + * @param int $room_id + * @param array $payload + * @return SummitRoomReservation + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addBookableRoomReservation(Summit $summit, int $room_id, array $payload):SummitRoomReservation; + + /** + * @param array $data + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function processBookableRoomPayment(array $payload):void; + + + /** + * @param Summit $sumit + * @param Member $owner + * @param int $reservation_id + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function cancelReservation(Summit $sumit, Member $owner, int $reservation_id):void; } \ No newline at end of file diff --git a/app/Services/Model/SummitLocationService.php b/app/Services/Model/SummitLocationService.php index eb785a14..6e875254 100644 --- a/app/Services/Model/SummitLocationService.php +++ b/app/Services/Model/SummitLocationService.php @@ -28,11 +28,14 @@ use App\Http\Utils\IFileUploader; use App\Models\Foundation\Summit\Factories\SummitLocationBannerFactory; use App\Models\Foundation\Summit\Factories\SummitLocationFactory; use App\Models\Foundation\Summit\Factories\SummitLocationImageFactory; +use App\Models\Foundation\Summit\Factories\SummitRoomReservationFactory; use App\Models\Foundation\Summit\Factories\SummitVenueFloorFactory; use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; use App\Models\Foundation\Summit\Repositories\ISummitLocationRepository; +use App\Models\Foundation\Summit\Repositories\ISummitRoomReservationRepository; use App\Services\Apis\GeoCodingApiException; use App\Services\Apis\IGeoCodingAPI; +use App\Services\Apis\IPaymentGatewayAPI; use App\Services\Model\Strategies\GeoLocation\GeoLocationStrategyFactory; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Event; @@ -40,10 +43,14 @@ use Illuminate\Support\Facades\Log; use libs\utils\ITransactionService; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\IMemberRepository; +use models\main\Member; use models\summit\Summit; use models\summit\SummitAbstractLocation; +use models\summit\SummitBookableVenueRoom; use models\summit\SummitGeoLocatedLocation; use models\summit\SummitLocationImage; +use models\summit\SummitRoomReservation; use models\summit\SummitVenue; use models\summit\SummitVenueFloor; use models\summit\SummitVenueRoom; @@ -70,28 +77,53 @@ final class SummitLocationService */ private $folder_service; + /** + * @var IPaymentGatewayAPI + */ + private $payment_gateway; + + /** + * @var IMemberRepository + */ + private $member_repository; + + /** + * @var ISummitRoomReservationRepository + */ + private $reservation_repository; + /** * SummitLocationService constructor. * @param ISummitLocationRepository $location_repository + * @param IMemberRepository $member_repository + * @param ISummitRoomReservationRepository $reservation_repository * @param IGeoCodingAPI $geo_coding_api * @param IFolderService $folder_service * @param IFileUploader $file_uploader + * @param IPaymentGatewayAPI $payment_gateway * @param ITransactionService $tx_service */ public function __construct ( ISummitLocationRepository $location_repository, + IMemberRepository $member_repository, + ISummitRoomReservationRepository $reservation_repository, IGeoCodingAPI $geo_coding_api, IFolderService $folder_service, IFileUploader $file_uploader, + IPaymentGatewayAPI $payment_gateway, ITransactionService $tx_service ) { parent::__construct($tx_service); - $this->location_repository = $location_repository; - $this->geo_coding_api = $geo_coding_api; - $this->file_uploader = $file_uploader; - $this->folder_service = $folder_service; + + $this->location_repository = $location_repository; + $this->member_repository = $member_repository; + $this->reservation_repository = $reservation_repository; + $this->geo_coding_api = $geo_coding_api; + $this->file_uploader = $file_uploader; + $this->folder_service = $folder_service; + $this->payment_gateway = $payment_gateway; } /** @@ -1646,4 +1678,107 @@ final class SummitLocationService }); } + + /** + * @param Summit $summit + * @param int $room_id + * @param array $payload + * @return SummitRoomReservation + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addBookableRoomReservation(Summit $summit, int $room_id, array $payload): SummitRoomReservation + { + return $this->tx_service->transaction(function () use ($summit, $room_id, $payload) { + + $room = $summit->getLocation($room_id); + + if (is_null($room)) { + throw new EntityNotFoundException(); + } + + if(!$room instanceof SummitBookableVenueRoom){ + throw new EntityNotFoundException(); + } + + $owner_id = $payload["owner_id"]; + + $owner = $this->member_repository->getById($owner_id); + + if (is_null($owner)) { + throw new EntityNotFoundException(); + } + + $payload['owner'] = $owner; + + $reservation = SummitRoomReservationFactory::build($summit, $payload); + + $room->addReservation($reservation); + + $result = $this->payment_gateway->generatePayment + ( + $reservation + ); + + $reservation->setPaymentGatewayCartId($result['cart_id']); + $reservation->setPaymentGatewayClientToken($result['client_token']); + + return $reservation; + }); + } + + /** + * @param array $payload + * @throws \Exception + */ + public function processBookableRoomPayment(array $payload): void + { + $this->tx_service->transaction(function () use ($payload) { + $reservation = $this->reservation_repository->getByPaymentGatewayCartId($payload['cart_id']); + + if(is_null($reservation)){ + throw new EntityNotFoundException(); + } + + if($this->payment_gateway->isSuccessFullPayment($payload)) { + $reservation->setPayed(); + return; + } + + $reservation->setPaymentError($this->payment_gateway->getPaymentError($payload)); + }); + } + + /** + * @param Summit $summit + * @param Member $owner + * @param int $reservation_id + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function cancelReservation(Summit $summit, Member $owner, int $reservation_id): void + { + $this->tx_service->transaction(function () use ($summit, $owner, $reservation_id) { + + $reservation = $owner->getReservationById($reservation_id); + + if(is_null($reservation)){ + throw new EntityNotFoundException(); + } + + if($reservation->getRoom()->getSummitId() != $summit->getId()){ + throw new EntityNotFoundException(); + } + + if($reservation->getStatus() == SummitRoomReservation::ReservedStatus) + throw new ValidationException("can not request a refund on a reserved booking!"); + + if($reservation->getStatus() == SummitRoomReservation::RequestedRefundStatus || + $reservation->getStatus() == SummitRoomReservation::RefundedStatus + ) + throw new ValidationException("can not request a refund on an already refunded booking!"); + + $reservation->requestRefund(); + }); + } } \ No newline at end of file diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php index 5e3ac57b..fc1574b1 100644 --- a/app/Services/ServicesProvider.php +++ b/app/Services/ServicesProvider.php @@ -13,10 +13,11 @@ **/ use App\Permissions\IPermissionsManager; use App\Permissions\PermissionsManager; -use App\Repositories\DoctrineRepository; use App\Services\Apis\CalendarSync\ICalendarSyncRemoteFacadeFactory; use App\Services\Apis\GoogleGeoCodingAPI; use App\Services\Apis\IGeoCodingAPI; +use App\Services\Apis\IPaymentGatewayAPI; +use App\Services\Apis\PaymentGateways\StripeApi; use App\Services\Model\AttendeeService; use App\Services\Model\FolderService; use App\Services\Model\IAttendeeService; @@ -125,6 +126,13 @@ final class ServicesProvider extends ServiceProvider return $api; }); + App::singleton(IPaymentGatewayAPI::class, function(){ + return new StripeApi( + Config::get("stripe.private_key", null), + Config::get("stripe.endpoint_secret", null) + ); + }); + App::singleton ( IAttendeeService::class, diff --git a/composer.json b/composer.json index 4abba62c..5d13a255 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "s-ichikawa/laravel-sendgrid-driver": "^2.0", "smarcet/caldavclient": "1.1.6", "smarcet/outlook-rest-client": "dev-master", + "stripe/stripe-php": "^6.37", "symfony/yaml": "4.2.2" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 23523835..9792384d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "d03caf628ec9076f0ab50d8cdab0685d", + "content-hash": "e8960c590d5b3ce6a88c1a2544a3751f", "packages": [ { "name": "cocur/slugify", @@ -3792,6 +3792,62 @@ ], "time": "2017-08-05T01:36:59+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v6.37.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "6915bed0b988ca837f3e15a1f31517a6172a663a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/6915bed0b988ca837f3e15a1f31517a6172a663a", + "reference": "6915bed0b988ca837f3e15a1f31517a6172a663a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "1.*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0", + "symfony/process": "~2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "time": "2019-05-23T23:59:23+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.2.0", diff --git a/config/stripe.php b/config/stripe.php new file mode 100644 index 00000000..6293d543 --- /dev/null +++ b/config/stripe.php @@ -0,0 +1,20 @@ + env('STRIPE_PRIVATE_KEY', ''), + // You can find your endpoint's secret in your webhook settings + "endpoint_secret" => env('STRIPE_ENDPOINT_SECRET', ''), +]; \ No newline at end of file diff --git a/database/migrations/model/Version20190529015655.php b/database/migrations/model/Version20190529015655.php new file mode 100644 index 00000000..861a6156 --- /dev/null +++ b/database/migrations/model/Version20190529015655.php @@ -0,0 +1,54 @@ +hasColumn("Summit","MeetingRoomBookingStartTime")) { + $builder->table('Summit', function (Table $table) { + $table->time("MeetingRoomBookingStartTime")->setNotnull(false); + $table->time("MeetingRoomBookingEndTime")->setNotnull(false); + $table->integer("MeetingRoomBookingSlotLength")->setNotnull(true)->setDefault(0); + $table->integer("MeetingRoomBookingMaxAllowed")->setNotnull(true)->setDefault(0); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $builder = new Builder($schema); + + $builder->table('Summit', function (Table $table) { + $table->dropColumn("MeetingRoomBookingStartTime"); + $table->dropColumn("MeetingRoomBookingEndTime"); + $table->dropColumn("MeetingRoomBookingSlotLength"); + $table->dropColumn("MeetingRoomBookingMaxAllowed"); + }); + } +} diff --git a/database/migrations/model/Version20190529142913.php b/database/migrations/model/Version20190529142913.php new file mode 100644 index 00000000..3dbc37c3 --- /dev/null +++ b/database/migrations/model/Version20190529142913.php @@ -0,0 +1,57 @@ +hasTable("SummitBookableVenueRoom")) { + $sql = <<addSql($sql); + $builder->create('SummitBookableVenueRoom', function (Table $table) { + $table->integer("ID", true, false); + $table->primary("ID"); + $table->string("Currency",3); + $table->decimal("TimeSlotCost", 9, 2)->setDefault('0.00'); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $sql = <<addSql($sql); + + $builder->drop('SummitBookableVenueRoom'); + } +} diff --git a/database/migrations/model/Version20190529142927.php b/database/migrations/model/Version20190529142927.php new file mode 100644 index 00000000..9b18521c --- /dev/null +++ b/database/migrations/model/Version20190529142927.php @@ -0,0 +1,64 @@ +hasTable("SummitRoomReservation")) { + $builder->create('SummitRoomReservation', function (Table $table) { + $table->integer("ID", true, false); + $table->primary("ID"); + $table->string('ClassName')->setDefault("SummitRoomReservation"); + $table->index("ClassName", "ClassName"); + $table->timestamp('Created'); + $table->timestamp('LastEdited'); + $table->timestamp('ApprovedPaymentDate')->setNotnull(false); + $table->timestamp('StartDateTime')->setNotnull(false); + $table->timestamp('EndDateTime')->setNotnull(false); + $table->string("Status"); + $table->string("LastError"); + $table->string("PaymentGatewayCartId", 512); + $table->decimal("Amount", 9, 2)->setDefault('0.00'); + $table->string("Currency", 3); + $table->index("PaymentGatewayCartId", "PaymentGatewayCartId"); + $table->integer("OwnerID", false, false)->setNotnull(false); + $table->index("OwnerID", "OwnerID"); + //$table->foreign("Member", "OwnerID", "ID"); + $table->integer("RoomID", false, false)->setNotnull(false); + $table->index("RoomID", "RoomID"); + //$table->foreign("SummitBookableVenueRoom", "RoomID", "ID"); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + (new Builder($schema))->drop('SummitRoomReservation'); + } +} diff --git a/database/migrations/model/Version20190530205326.php b/database/migrations/model/Version20190530205326.php new file mode 100644 index 00000000..00f14a96 --- /dev/null +++ b/database/migrations/model/Version20190530205326.php @@ -0,0 +1,42 @@ +hasTable("SummitBookableVenueRoomAttributeType")) { + $builder->create('SummitBookableVenueRoomAttributeType', function (Table $table) { + $table->integer("ID", true, false); + $table->primary("ID"); + $table->string('ClassName')->setDefault("SummitBookableVenueRoomAttributeType"); + $table->index("ClassName", "ClassName"); + $table->timestamp('Created'); + $table->timestamp('LastEdited'); + $table->string("Type", 255); + $table->index("Type", "Type"); + $table->integer("SummitID", false, false)->setNotnull(false); + $table->index("SummitID", "SummitID"); + $table->unique(["SummitID", "Type"], "SummitID_Type"); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + (new Builder($schema))->drop('BookableSummitVenueRoomAttributeType'); + } +} diff --git a/database/migrations/model/Version20190530205344.php b/database/migrations/model/Version20190530205344.php new file mode 100644 index 00000000..1fe724b7 --- /dev/null +++ b/database/migrations/model/Version20190530205344.php @@ -0,0 +1,56 @@ +hasTable("SummitBookableVenueRoomAttributeValue")) { + $builder->create('SummitBookableVenueRoomAttributeValue', function (Table $table) { + $table->integer("ID", true, false); + $table->primary("ID"); + $table->string('ClassName')->setDefault("SummitBookableVenueRoomAttributeValue"); + $table->index("ClassName", "ClassName"); + $table->timestamp('Created'); + $table->timestamp('LastEdited'); + $table->string("Value", 255); + $table->index("Value", "Value"); + $table->integer("TypeID", false, false)->setNotnull(false); + $table->index("TypeID", "TypeID"); + $table->unique(["TypeID", "Value"], "TypeID_Value"); + }); + } + + if(!$schema->hasTable("SummitBookableVenueRoom_Attributes")) { + $builder->create('SummitBookableVenueRoom_Attributes', function (Table $table) { + $table->integer("ID", true, false); + $table->primary("ID"); + $table->integer("SummitBookableVenueRoomID", false, false)->setNotnull(false)->setDefault(0); + $table->index("SummitBookableVenueRoomID", "SummitBookableVenueRoomID"); + $table->integer("SummitBookableVenueRoomAttributeValueID", false, false)->setNotnull(false)->setDefault(0); + $table->index("SummitBookableVenueRoomAttributeValueID", "SummitBookableVenueRoomAttributeValueID"); + $table->unique(["SummitBookableVenueRoomID", "SummitBookableVenueRoomAttributeValueID"], "RoomID_ValueID"); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + (new Builder($schema))->drop('SummitBookableVenueRoomAttributeValue'); + (new Builder($schema))->drop('SummitBookableVenueRoom_Attributes'); + } +} diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index aaab2c00..d0bf8c5b 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -93,6 +93,15 @@ class ApiEndpointsSeeder extends Seeder sprintf(SummitScopes::ReadAllSummitData, $current_realm) ], ], + [ + 'name' => 'get-summits-all-by-id-slug', + 'route' => '/api/v1/summits/all/{id}', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadBookableRoomsData, $current_realm), + ], + ], [ 'name' => 'get-summit-cached', 'route' => '/api/v1/summits/{id}', @@ -1050,6 +1059,85 @@ class ApiEndpointsSeeder extends Seeder sprintf(SummitScopes::WriteLocationsData, $current_realm) ], ], + // bookable rooms + [ + 'name' => 'get-bookable-venue-rooms', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadBookableRoomsData, $current_realm), + sprintf(SummitScopes::ReadAllSummitData, $current_realm) + ], + ], + [ + 'name' => 'get-bookable-venue-room', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms/{room_id}', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadBookableRoomsData, $current_realm), + sprintf(SummitScopes::ReadAllSummitData, $current_realm) + ], + ], + [ + 'name' => 'get-bookable-venue-room-availability', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms/{room_id}/availability/{day}', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadBookableRoomsData, $current_realm), + sprintf(SummitScopes::ReadAllSummitData, $current_realm) + ], + ], + [ + 'name' => 'get-my-bookable-venue-room-reservations', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms/all/reservations/me', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadMyBookableRoomsReservationData, $current_realm), + ], + ], + [ + 'name' => 'cancel-my-bookable-venue-room-reservation', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms/all/reservations/{reservation_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteMyBookableRoomsReservationData, $current_realm), + ], + ], + [ + 'name' => 'create-bookable-venue-room-reservation', + 'route' => '/api/v1/summits/{id}/locations/bookable-rooms/{room_id}/reservations', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteMyBookableRoomsReservationData, $current_realm), + ], + ], + [ + 'name' => 'add-bookable-venue-room', + 'route' => '/api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteBookableRoomsData, $current_realm), + ], + ], + [ + 'name' => 'update-bookable-venue-room', + 'route' => '/api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms/{room_id}', + 'http_method' => 'PUT', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteBookableRoomsData, $current_realm), + ], + ], + [ + 'name' => 'delete-bookable-venue-room', + 'route' => '/api/v1/summits/{id}/locations/venues/{venue_id}/bookable-rooms/{room_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteBookableRoomsData, $current_realm), + ], + ], // floor rooms [ 'name' => 'get-venue-floor-room', diff --git a/database/seeds/ApiScopesSeeder.php b/database/seeds/ApiScopesSeeder.php index 68aec30b..bb719657 100644 --- a/database/seeds/ApiScopesSeeder.php +++ b/database/seeds/ApiScopesSeeder.php @@ -186,6 +186,16 @@ final class ApiScopesSeeder extends Seeder 'short_description' => 'Write Summit Presentation Materials Data', 'description' => 'Grants write access for Summit Materials Links Data', ], + [ + 'name' => sprintf(SummitScopes::ReadMyBookableRoomsReservationData, $current_realm), + 'short_description' => 'Read my bookable rooms reservations', + 'description' => 'Read my bookable rooms reservations', + ], + [ + 'name' => sprintf(SummitScopes::WriteMyBookableRoomsReservationData, $current_realm), + 'short_description' => 'Write my bookable rooms reservations', + 'description' => 'Write my bookable rooms reservations', + ], ]; foreach ($scopes as $scope_info) { diff --git a/tests/MeetingRoomTest.php b/tests/MeetingRoomTest.php new file mode 100644 index 00000000..c307c741 --- /dev/null +++ b/tests/MeetingRoomTest.php @@ -0,0 +1,53 @@ +redis = Redis::connection(); + $this->redis->flushall(); + $this->createApplication(); + } + + public function testMeetingRoomsAvalableSlots(){ + $repository = EntityManager::getRepository(Summit::class); + $summit = $repository->getBySlug('shanghai-2019'); + $rooms = $summit->getBookableRooms(); + $this->assertTrue(count($rooms) > 0); + + $room = $rooms[0]; + + $room->getAvailableSlots(new \DateTime("2019-11-05")); + } + +} \ No newline at end of file diff --git a/tests/OAuth2SummitLocationsApiTest.php b/tests/OAuth2SummitLocationsApiTest.php index bcac49b3..8d36c982 100644 --- a/tests/OAuth2SummitLocationsApiTest.php +++ b/tests/OAuth2SummitLocationsApiTest.php @@ -1350,4 +1350,181 @@ final class OAuth2SummitLocationsApiTest extends ProtectedApiTest $content = $response->getContent(); $this->assertResponseStatus(204); } + + // bookable rooms tests + + public function testSummitGetBookableRoomsORFilter($summit_id = 27) + { + $params = [ + 'id' => $summit_id, + 'page' => 1, + 'per_page' => 10, + 'order' => '-id', + 'expand' => 'venue,attribute_type', + 'filter' => [ + "attribute==Ocean,attribute==Microwave", + "availability_day==1572912000", + ], + ]; + + $headers = + [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "GET", + "OAuth2SummitLocationsApiController@getBookableVenueRooms", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $rooms = json_decode($content); + $this->assertTrue(!is_null($rooms)); + } + + public function testSummitGetBookableRoomAvailability($summit_id = 27, $room_id = 483, $day = 1572912000) + { + $params = [ + 'id' => $summit_id, + 'room_id' => $room_id, + 'day' => $day, + ]; + + $headers = + [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "GET", + "OAuth2SummitLocationsApiController@getBookableVenueRoomAvailability", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $slots = json_decode($content); + $this->assertTrue(!is_null($slots)); + } + + /** + * @param int $summit_id + * @param int $room_id + * @param int $start_date + * @return mixed + */ + public function testBookableRoomReservation($summit_id =27, $room_id = 483, $start_date = 1572883200, $end_date = 1572886800){ + $params = [ + 'id' => $summit_id, + 'room_id' => $room_id, + ]; + + $data = [ + 'currency' => 'USD', + 'amount' => 325, + 'start_datetime' => $start_date, + 'end_datetime' => $end_date, + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitLocationsApiController@createBookableVenueRoomReservation", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $reservation = json_decode($content); + $this->assertTrue(!is_null($reservation)); + return $reservation; + } + + public function testGetMyReservations($summit_id = 27) + { + $params = [ + 'id' => $summit_id, + 'expand' => 'room' + ]; + + $headers = + [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "GET", + "OAuth2SummitLocationsApiController@getMyBookableVenueRoomReservations", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $reservations = json_decode($content); + $this->assertTrue(!is_null($reservations)); + } + + public function testCancelMyReservations($summit_id = 27, $reservation_id = 4) + { + $params = [ + 'id' => $summit_id, + 'reservation_id' => $reservation_id + ]; + + $headers = + [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "DELETE", + "OAuth2SummitLocationsApiController@cancelMyBookableVenueRoomReservation", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $reservations = json_decode($content); + $this->assertTrue(!is_null($reservations)); + } + } \ No newline at end of file diff --git a/tests/ProtectedApiTest.php b/tests/ProtectedApiTest.php index a863dfb6..eaf51c66 100644 --- a/tests/ProtectedApiTest.php +++ b/tests/ProtectedApiTest.php @@ -67,6 +67,8 @@ class AccessTokenServiceStub implements IAccessTokenService sprintf(OrganizationScopes::WriteOrganizationData, $url), sprintf(OrganizationScopes::ReadOrganizationData, $url), sprintf(SummitScopes::WritePresentationMaterialsData, $url), + sprintf(SummitScopes::ReadMyBookableRoomsReservationData, $url), + sprintf(SummitScopes::WriteMyBookableRoomsReservationData, $url), ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, '1','11624', 3600, 'WEB_APPLICATION', '', ''); @@ -119,6 +121,8 @@ class AccessTokenServiceStub2 implements IAccessTokenService sprintf(OrganizationScopes::WriteOrganizationData, $url), sprintf(OrganizationScopes::ReadOrganizationData, $url), sprintf(SummitScopes::WritePresentationMaterialsData, $url), + sprintf(SummitScopes::ReadMyBookableRoomsReservationData, $url), + sprintf(SummitScopes::WriteMyBookableRoomsReservationData, $url), ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, null,null, 3600, 'SERVICE', '', '');