Refactoring revocation bookable rooms reservations

refactored task due a need on registration new feature

Change-Id: I658954d8b7132b183595a4bdeb1634e2e681ddec
This commit is contained in:
smarcet 2019-09-01 14:08:14 -03:00
parent f25ebdb2bf
commit 037a1420bb
9 changed files with 239 additions and 56 deletions

View File

@ -12,6 +12,7 @@
* limitations under the License.
**/
use App\Models\Foundation\Summit\Repositories\ISummitRoomReservationRepository;
use App\Services\Model\ILocationService;
use Illuminate\Support\Facades\Log;
use libs\utils\ITransactionService;
use models\summit\SummitRoomReservation;
@ -52,29 +53,22 @@ final class SummitRoomReservationRevocationCommand extends Command {
/**
* @var ISummitRoomReservationRepository
* @var ILocationService
*/
private $reservations_repository;
private $location_service;
/**
* @var ITransactionService
*/
private $tx_service;
/**
* SummitRoomReservationRevocationCommand constructor.
* @param ISummitRoomReservationRepository $reservations_repository
* @param ITransactionService $tx_service
* @param ILocationService $location_service
*/
public function __construct
(
ISummitRoomReservationRepository $reservations_repository,
ITransactionService $tx_service
ILocationService $location_service
)
{
parent::__construct();
$this->reservations_repository = $reservations_repository;
$this->tx_service = $tx_service;
$this->location_service = $location_service;
}
/**
@ -96,20 +90,7 @@ final class SummitRoomReservationRevocationCommand extends Command {
$start = time();
$lifetime = intval(Config::get("bookable_rooms.reservation_lifetime", 30));
Log::info(sprintf("SummitRoomReservationRevocationCommand: using lifetime of %s ", $lifetime));
$this->tx_service->transaction(function() use($lifetime){
$filter = new Filter();
$filter->addFilterCondition(FilterElement::makeEqual('status', SummitRoomReservation::ReservedStatus));
$eol = new \DateTime('now', new \DateTimeZone(SilverstripeBaseModel::DefaultTimeZone));
$eol->sub(new \DateInterval('PT'.$lifetime.'M'));
$filter->addFilterCondition(FilterElement::makeLowerOrEqual('created', $eol->getTimestamp() ));
$page = $this->reservations_repository->getAllByPage(new PagingInfo(1, 100), $filter);
foreach($page->getItems() as $reservation){
Log::warning(sprintf("cancelling reservation %s create at %s", $reservation->getId(), $reservation->getCreated()->format("Y-m-d h:i:sa")));
$reservation->cancel();
}
});
$this->location_service->revokeBookableRoomsReservedOlderThanNMinutes($lifetime);
$end = time();
$delta = $end - $start;
$this->info(sprintf("execution call %s seconds", $delta));

View File

@ -11,7 +11,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use models\summit\Summit;
use models\summit\SummitRoomReservation;
use models\utils\IBaseRepository;
@ -19,7 +18,6 @@ use utils\Filter;
use utils\Order;
use utils\PagingInfo;
use utils\PagingResponse;
/**
* Interface ISummitRoomReservationRepository
* @package App\Models\Foundation\Summit\Repositories
@ -30,7 +28,7 @@ interface ISummitRoomReservationRepository extends IBaseRepository
* @param string $payment_gateway_cart_id
* @return SummitRoomReservation|null
*/
public function getByPaymentGatewayCartId(string $payment_gateway_cart_id): ?SummitRoomReservation;
public function getByPaymentGatewayCartIdExclusiveLock(string $payment_gateway_cart_id): ?SummitRoomReservation;
/**
* @param Summit $summit
@ -40,4 +38,12 @@ interface ISummitRoomReservationRepository extends IBaseRepository
* @return PagingResponse
*/
public function getAllBySummitByPage(Summit $summit, PagingInfo $paging_info, Filter $filter = null, Order $order = null): PagingResponse;
/**
* @param int $minutes
* @param int $max
* @return mixed
* @throws \Exception
*/
public function getAllReservedOlderThanXMinutes(int $minutes, int $max = 100);
}

View File

@ -26,6 +26,12 @@ interface IBaseRepository
*/
public function getById($id);
/**
* @param int $id
* @return IEntity
*/
public function getByIdExclusiveLock($id);
/**
* @param IEntity $entity
* @param bool $sync

View File

@ -31,11 +31,23 @@ use Doctrine\ORM\Tools\Pagination\Paginator;
abstract class DoctrineRepository extends EntityRepository implements IBaseRepository
{
/**
* @param int $id
* @return IEntity|null|object
*/
public function getById($id)
{
return $this->find($id);
}
/**
* @param int $id
* @return IEntity|null|object
*/
public function getByIdExclusiveLock($id){
return $this->find($id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE);
}
/**
* @param $entity
* @param bool $sync

View File

@ -16,6 +16,7 @@ use App\Repositories\SilverStripeDoctrineRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use models\summit\Summit;
use models\summit\SummitRoomReservation;
use models\utils\SilverstripeBaseModel;
use utils\DoctrineFilterMapping;
use utils\DoctrineJoinFilterMapping;
use utils\Filter;
@ -26,7 +27,7 @@ use utils\PagingResponse;
* Class DoctrineSummitRoomReservationRepository
* @package App\Repositories\Summit
*/
class DoctrineSummitRoomReservationRepository
final class DoctrineSummitRoomReservationRepository
extends SilverStripeDoctrineRepository
implements ISummitRoomReservationRepository
{
@ -156,10 +157,42 @@ class DoctrineSummitRoomReservationRepository
* @param string $payment_gateway_cart_id
* @return SummitRoomReservation|null
*/
public function getByPaymentGatewayCartId(string $payment_gateway_cart_id):?SummitRoomReservation
public function getByPaymentGatewayCartIdExclusiveLock(string $payment_gateway_cart_id):?SummitRoomReservation
{
return $this->findOneBy(["payment_gateway_cart_id" => trim($payment_gateway_cart_id)]);
$query = $this->getEntityManager()
->createQueryBuilder()
->select("e")
->from($this->getBaseEntity(), "e")
->where("e.payment_gateway_cart_id = payment_gateway_cart_id");
$query->setParameter("payment_gateway_cart_id", trim($payment_gateway_cart_id));
return $query->getQuery()->setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)->getOneOrNullResult();
}
/**
* @param int $minutes
* @param int $max
* @return mixed
* @throws \Exception
*/
public function getAllReservedOlderThanXMinutes(int $minutes, int $max = 100)
{
$eol = new \DateTime('now', new \DateTimeZone(SilverstripeBaseModel::DefaultTimeZone));
$eol->sub(new \DateInterval('PT' . $minutes . 'M'));
$query = $this->getEntityManager()
->createQueryBuilder()
->select("e")
->from($this->getBaseEntity(), "e")
->where("e.created <= :eol")
->andWhere("e.status = :status");
$query->setParameter("eol", $eol);
$query->setParameter("status", SummitRoomReservation::ReservedStatus);
return $query->getQuery()->setMaxResults($max)->getResult();
}
}

View File

@ -12,6 +12,15 @@
* limitations under the License.
**/
use Illuminate\Http\Request as LaravelRequest;
use Exception;
/**
* Class CartAlreadyPaidException
* @package App\Services\Apis
*/
class CartAlreadyPaidException extends Exception {
}
/**
* Interface IPaymentGatewayAPI
* @package App\Services\Apis
@ -49,4 +58,29 @@ interface IPaymentGatewayAPI
* @throws \InvalidArgumentException
*/
public function refundPayment(string $cart_id, float $amount, string $currency): void;
/**
* @param string $cart_id
* @return mixed|void
* @throws CartAlreadyPaidException
*/
public function abandonCart(string $cart_id);
/**
* @param string $status
* @return bool
*/
public function canAbandon(string $status):bool;
/**
* @param string $cart_id
* @return string
*/
public function getCartStatus(string $cart_id):string;
/**
* @param string $status
* @return bool
*/
public function isSucceeded(string $status):bool;
}

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Services\Apis\CartAlreadyPaidException;
use App\Services\Apis\IPaymentGatewayAPI;
use Illuminate\Http\Request as LaravelRequest;
use models\exceptions\ValidationException;
@ -245,4 +246,70 @@ final class StripeApi implements IPaymentGatewayAPI
}
$charge->refund($params);
}
/**
* @param string $cart_id
* @return mixed|void
* @throws CartAlreadyPaidException
*/
public function abandonCart(string $cart_id)
{
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();
if(!in_array($intent->status,[ PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD,
PaymentIntent::STATUS_REQUIRES_CAPTURE,
PaymentIntent::STATUS_REQUIRES_CONFIRMATION,
PaymentIntent::STATUS_REQUIRES_ACTION
]))
throw new CartAlreadyPaidException(sprintf("cart id %s has status %s", $cart_id, $intent->status));
$intent->cancel();
}
/**
* @param string $status
* @return bool
*/
public function canAbandon(string $status): bool
{
return in_array($status,[
PaymentIntent::STATUS_REQUIRES_PAYMENT_METHOD,
PaymentIntent::STATUS_REQUIRES_CAPTURE,
PaymentIntent::STATUS_REQUIRES_CONFIRMATION,
PaymentIntent::STATUS_REQUIRES_ACTION
]);
}
/**
* @param string $status
* @return bool
*/
public function isSucceeded(string $status):bool {
return $status == PaymentIntent::STATUS_SUCCEEDED;
}
/**
* @param string $cart_id
* @return string
*/
public function getCartStatus(string $cart_id): string
{
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();
return $intent->status;
}
}

View File

@ -329,4 +329,9 @@ interface ILocationService
* @return SummitVenueRoom
*/
public function removeRoomImage(Summit $summit, int $venue_id, int $room_id):SummitVenueRoom;
/**
* @param int $minutes
*/
public function revokeBookableRoomsReservedOlderThanNMinutes(int $minutes):void;
}

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Events\CreatedBookableRoomReservation;
use App\Events\FloorDeleted;
use App\Events\FloorInserted;
@ -55,6 +56,7 @@ use models\summit\SummitRoomReservation;
use models\summit\SummitVenue;
use models\summit\SummitVenueFloor;
use models\summit\SummitVenueRoom;
/**
* Class SummitLocationService
* @package App\Services\Model
@ -1710,14 +1712,14 @@ final class SummitLocationService
throw new EntityNotFoundException('member not found');
}
if($owner->getReservationsCountBySummit($summit) >= $summit->getMeetingRoomBookingMaxAllowed())
throw new ValidationException(sprintf("member %s already reached maximun quantity of reservations (%s)", $owner->getId(), $summit->getMeetingRoomBookingMaxAllowed() ));
if ($owner->getReservationsCountBySummit($summit) >= $summit->getMeetingRoomBookingMaxAllowed())
throw new ValidationException(sprintf("member %s already reached maximun quantity of reservations (%s)", $owner->getId(), $summit->getMeetingRoomBookingMaxAllowed()));
$payload['owner'] = $owner;
$currency = trim($payload['currency']);
if($room->getCurrency() != $currency){
if ($room->getCurrency() != $currency) {
throw new ValidationException
(
sprintf
@ -1731,7 +1733,7 @@ final class SummitLocationService
$amount = intval($payload['amount']);
if($room->getTimeSlotCost() != $amount){
if ($room->getTimeSlotCost() != $amount) {
throw new ValidationException
(
sprintf
@ -1751,20 +1753,20 @@ final class SummitLocationService
$result = $this->payment_gateway->generatePayment
(
[
"amount" => $reservation->getAmount(),
"currency" => $reservation->getCurrency(),
"amount" => $reservation->getAmount(),
"currency" => $reservation->getCurrency(),
"receipt_email" => $reservation->getOwner()->getEmail(),
"metadata" => [
"type" => "bookable_room_reservation",
"metadata" => [
"type" => "bookable_room_reservation",
"room_id" => $room->getId(),
]
]
);
if(!isset($result['cart_id']))
if (!isset($result['cart_id']))
throw new ValidationException("payment gateway error");
if(!isset($result['client_token']))
if (!isset($result['client_token']))
throw new ValidationException("payment gateway error");
$reservation->setPaymentGatewayCartId($result['cart_id']);
@ -1782,7 +1784,7 @@ final class SummitLocationService
{
$this->tx_service->transaction(function () use ($payload) {
$reservation = $this->reservation_repository->getByPaymentGatewayCartId($payload['cart_id']);
$reservation = $this->reservation_repository->getByPaymentGatewayCartIdExclusiveLock($payload['cart_id']);
if (is_null($reservation)) {
throw new EntityNotFoundException(sprintf("there is no reservation with cart_id %s", $payload['cart_id']));
@ -1794,8 +1796,7 @@ final class SummitLocationService
$reservation->setPaid();
return;
}
}
catch (ValidationException $ex){
} catch (ValidationException $ex) {
Log::error($ex);
Log::warning("doing refund of cancelled reservation");
$reservation->setStatus(SummitRoomReservation::RequestedRefundStatus);
@ -1860,9 +1861,9 @@ final class SummitLocationService
throw new EntityNotFoundException();
}
$status = $reservation->getStatus();
$status = $reservation->getStatus();
$validStatuses = [SummitRoomReservation::RequestedRefundStatus, SummitRoomReservation::PayedStatus];
if(!in_array($status, $validStatuses))
if (!in_array($status, $validStatuses))
throw new ValidationException
(
sprintf
@ -1872,18 +1873,17 @@ final class SummitLocationService
)
);
if($amount <= 0){
if ($amount <= 0) {
throw new ValidationException("can not refund an amount lower than zero!");
}
if($amount > intval($reservation->getAmount())){
if ($amount > intval($reservation->getAmount())) {
throw new ValidationException("can not refund an amount greater than paid one!");
}
try{
try {
$this->payment_gateway->refundPayment($reservation->getPaymentGatewayCartId(), $amount, $reservation->getCurrency());
}
catch (\Exception $ex){
} catch (\Exception $ex) {
throw new ValidationException($ex->getMessage());
}
@ -2153,7 +2153,7 @@ final class SummitLocationService
if (!$venue instanceof SummitVenue) {
throw new EntityNotFoundException
(
"venue not found"
"venue not found"
);
}
@ -2277,7 +2277,7 @@ final class SummitLocationService
if (is_null($room)) {
throw new EntityNotFoundException
(
'room not found'
'room not found'
);
}
@ -2296,7 +2296,7 @@ final class SummitLocationService
throw new ValidationException(sprintf("file exceeds max_file_size (%s MB).", ($max_file_size / 1024) / 1024));
}
$image = $this->file_uploader->build($file, sprintf('summits/%s/locations/%s/rooms', $summit->getId(), $venue_id ), true);
$image = $this->file_uploader->build($file, sprintf('summits/%s/locations/%s/rooms', $summit->getId(), $venue_id), true);
$room->setImage($image);
return $image;
@ -2331,7 +2331,7 @@ final class SummitLocationService
);
}
if(!$room->hasImage())
if (!$room->hasImage())
throw new ValidationException("room has no image set");
$room->clearImage();
@ -2339,4 +2339,43 @@ final class SummitLocationService
return $room;
});
}
/**
* @param int $minutes
*/
public function revokeBookableRoomsReservedOlderThanNMinutes(int $minutes): void
{
// this is done in this way to avoid db lock contentions
$reservations = $this->tx_service->transaction(function () use ($minutes) {
return $this->reservation_repository->getAllReservedOlderThanXMinutes($minutes);
});
foreach ($reservations as $reservation) {
$this->tx_service->transaction(function () use ($reservation) {
try {
$reservation = $this->reservation_repository->getByIdExclusiveLock($reservation->getId());
if (!$reservation instanceof SummitRoomReservation) return;
Log::warning(sprintf("cancelling reservation %s created at %s", $reservation->getId(), $reservation->getCreated()->format("Y-m-d h:i:sa")));
$status = $this->payment_gateway->getCartStatus($reservation->getPaymentGatewayCartId());
if (!$this->payment_gateway->canAbandon($status)) {
Log::warning(sprintf("reservation %s created at %s can not be cancelled external status %s", $reservation->getId(), $reservation->getCreated()->format("Y-m-d h:i:sa"), $status));
if($this->payment_gateway->isSucceeded($status)){
$reservation->setPaid();
}
return;
}
$this->payment_gateway->abandonCart($reservation->getPaymentGatewayCartId());
$reservation->cancel();
} catch (\Exception $ex) {
Log::warning($ex);
}
});
}
}
}