Skip to content

Commit

Permalink
(PC-30861)[BO] feat: add reason cancellation with author for booking …
Browse files Browse the repository at this point in the history
…and collective
  • Loading branch information
blbpro committed Sep 3, 2024
1 parent 003b3e4 commit 75191fa
Show file tree
Hide file tree
Showing 23 changed files with 400 additions and 49 deletions.
2 changes: 1 addition & 1 deletion api/alembic_version_conflict_detection.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
869f0d3be788 (pre) (head)
63fa42fd8352 (post) (head)
da2896f47266 (post) (head)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
add reason in enum CollectiveBookingCancellationReasons
"""

from alembic import op


# pre/post deployment: post
# revision identifiers, used by Alembic.
revision = "01a23d1b5687"
down_revision = "63fa42fd8352"
branch_labels: tuple[str] | None = None
depends_on: list[str] | None = None


def upgrade() -> None:
op.execute("ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_EVENT_CANCELLED' ")
op.execute("ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OVERBOOKING' ")
op.execute("ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_BENEFICIARY_REQUEST' ")
op.execute("ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OFFER_MODIFIED' ")
op.execute(
"ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS' "
)
op.execute("ALTER TYPE \"bookingcancellationreasons\" ADD VALUE IF NOT EXISTS 'OFFERER_CONNECT_AS' ")


def downgrade() -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
add reason in enum BookingCancellationReasons
"""

from alembic import op


# pre/post deployment: post
# revision identifiers, used by Alembic.
revision = "da2896f47266"
down_revision = "01a23d1b5687"
branch_labels: tuple[str] | None = None
depends_on: list[str] | None = None


def upgrade() -> None:
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_EVENT_CANCELLED' ")
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OVERBOOKING' ")
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_BENEFICIARY_REQUEST' ")
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OFFER_MODIFIED' ")
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS' ")
op.execute("ALTER TYPE \"cancellation_reason\" ADD VALUE IF NOT EXISTS 'OFFERER_CONNECT_AS' ")


def downgrade() -> None:
pass
16 changes: 13 additions & 3 deletions api/src/pcapi/core/bookings/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,10 @@ def _cancel_external_booking(booking: Booking, stock: Stock) -> None:


def _cancel_bookings_from_stock(
stock: offers_models.Stock, reason: BookingCancellationReasons, one_side_cancellation: bool = False
stock: offers_models.Stock,
reason: BookingCancellationReasons,
one_side_cancellation: bool = False,
author_id: int | None = None,
) -> list[Booking]:
"""
Cancel multiple bookings and update the users' credit information on Batch.
Expand All @@ -638,6 +641,7 @@ def _cancel_bookings_from_stock(
reason,
cancel_even_if_used=typing.cast(bool, stock.offer.isEvent),
one_side_cancellation=one_side_cancellation,
author_id=author_id,
):
deleted_bookings.append(booking)

Expand All @@ -659,8 +663,14 @@ def cancel_booking_by_offerer(booking: Booking) -> None:
user_emails_job.send_booking_cancellation_emails_to_user_and_offerer_job.delay(booking.id)


def cancel_bookings_from_stock_by_offerer(stock: offers_models.Stock) -> list[Booking]:
return _cancel_bookings_from_stock(stock, BookingCancellationReasons.OFFERER, one_side_cancellation=True)
def cancel_bookings_from_stock_by_offerer(
stock: offers_models.Stock, author_id: int | None = None, user_connect_as: bool | None = None
) -> list[Booking]:
if user_connect_as:
cancellation_reason = BookingCancellationReasons.OFFERER_CONNECT_AS
else:
cancellation_reason = BookingCancellationReasons.OFFERER
return _cancel_bookings_from_stock(stock, cancellation_reason, one_side_cancellation=True, author_id=author_id)


def cancel_bookings_from_rejected_offer(offer: offers_models.Offer) -> list[Booking]:
Expand Down
6 changes: 6 additions & 0 deletions api/src/pcapi/core/bookings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class BookingCancellationReasons(enum.Enum):
REFUSED_BY_INSTITUTE = "REFUSED_BY_INSTITUTE"
FINANCE_INCIDENT = "FINANCE_INCIDENT"
BACKOFFICE = "BACKOFFICE"
BACKOFFICE_EVENT_CANCELLED = "BACKOFFICE_EVENT_CANCELLED"
BACKOFFICE_OVERBOOKING = "BACKOFFICE_OVERBOOKING"
BACKOFFICE_BENEFICIARY_REQUEST = "BACKOFFICE_BENEFICIARY_REQUEST"
BACKOFFICE_OFFER_MODIFIED = "BACKOFFICE_OFFER_MODIFIED"
BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS = "BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS"
OFFERER_CONNECT_AS = "OFFERER_CONNECT_AS"


class BookingStatus(enum.Enum):
Expand Down
16 changes: 12 additions & 4 deletions api/src/pcapi/core/educational/api/booking.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def get_collective_booking_by_id(booking_id: int) -> educational_models.Collecti
return collective_booking


def cancel_collective_offer_booking(offer_id: int) -> None:
def cancel_collective_offer_booking(offer_id: int, author_id: int, user_connect_as: bool) -> None:
collective_offer: educational_models.CollectiveOffer | None = (
educational_models.CollectiveOffer.query.filter(educational_models.CollectiveOffer.id == offer_id)
.options(
Expand All @@ -305,7 +305,7 @@ def cancel_collective_offer_booking(offer_id: int) -> None:
collective_stock = collective_offer.collectiveStock

# Offer is reindexed in the end of this function
cancelled_booking = _cancel_collective_booking_by_offerer(collective_stock)
cancelled_booking = _cancel_collective_booking_by_offerer(collective_stock, author_id, user_connect_as)

logger.info(
"Cancelled collective booking from offer",
Expand Down Expand Up @@ -337,6 +337,7 @@ def notify_pro_pending_booking_confirmation_limit_in_3_days() -> None:
def _cancel_collective_booking(
collective_booking: educational_models.CollectiveBooking,
reason: educational_models.CollectiveBookingCancellationReasons,
author_id: int,
) -> None:
with transaction():
educational_repository.get_and_lock_collective_stock(stock_id=collective_booking.collectiveStock.id)
Expand All @@ -346,7 +347,7 @@ def _cancel_collective_booking(
# The booking cannot be used nor reimbursed yet, otherwise
# `cancel_booking` will fail. Thus, there is no finance
# event to cancel here.
collective_booking.cancel_booking(reason)
collective_booking.cancel_booking(reason=reason, author_id=author_id)
except exceptions.CollectiveBookingAlreadyCancelled:
return

Expand All @@ -363,6 +364,8 @@ def _cancel_collective_booking(

def _cancel_collective_booking_by_offerer(
collective_stock: educational_models.CollectiveStock,
author_id: int,
user_connect_as: bool,
) -> educational_models.CollectiveBooking:
"""
Cancel booking.
Expand All @@ -380,9 +383,14 @@ def _cancel_collective_booking_by_offerer(
if booking_to_cancel is None:
raise exceptions.NoCollectiveBookingToCancel()

if user_connect_as:
cancellation_reason = educational_models.CollectiveBookingCancellationReasons.OFFERER_CONNECT_AS
else:
cancellation_reason = educational_models.CollectiveBookingCancellationReasons.OFFERER
_cancel_collective_booking(
booking_to_cancel,
educational_models.CollectiveBookingCancellationReasons.OFFERER,
cancellation_reason,
author_id,
)

return booking_to_cancel
Expand Down
6 changes: 6 additions & 0 deletions api/src/pcapi/core/educational/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ class CollectiveBookingCancellationReasons(enum.Enum):
PUBLIC_API = "PUBLIC_API"
FINANCE_INCIDENT = "FINANCE_INCIDENT"
BACKOFFICE = "BACKOFFICE"
BACKOFFICE_EVENT_CANCELLED = "BACKOFFICE_EVENT_CANCELLED"
BACKOFFICE_OVERBOOKING = "BACKOFFICE_OVERBOOKING"
BACKOFFICE_BENEFICIARY_REQUEST = "BACKOFFICE_BENEFICIARY_REQUEST"
BACKOFFICE_OFFER_MODIFIED = "BACKOFFICE_OFFER_MODIFIED"
BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS = "BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS"
OFFERER_CONNECT_AS = "OFFERER_CONNECT_AS"


class Ministry(enum.Enum):
Expand Down
14 changes: 8 additions & 6 deletions api/src/pcapi/core/offers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,12 +885,12 @@ def _invalidate_bookings(bookings: list[bookings_models.Booking]) -> list[bookin
return bookings


def _delete_stock(stock: models.Stock) -> None:
def _delete_stock(stock: models.Stock, author_id: int | None = None, user_connect_as: bool | None = None) -> None:
stock.isSoftDeleted = True
repository.save(stock)

# the algolia sync for the stock will happen within this function
cancelled_bookings = bookings_api.cancel_bookings_from_stock_by_offerer(stock)
cancelled_bookings = bookings_api.cancel_bookings_from_stock_by_offerer(stock, author_id, user_connect_as)

logger.info(
"Deleted stock and cancelled its bookings",
Expand All @@ -908,9 +908,9 @@ def _delete_stock(stock: models.Stock) -> None:
)


def delete_stock(stock: models.Stock) -> None:
def delete_stock(stock: models.Stock, author_id: int | None = None, user_connect_as: bool | None = None) -> None:
validation.check_stock_is_deletable(stock)
_delete_stock(stock)
_delete_stock(stock, author_id, user_connect_as)


def create_mediation(
Expand Down Expand Up @@ -1542,13 +1542,15 @@ def batch_delete_draft_offers(query: BaseQuery) -> None:
db.session.commit()


def batch_delete_stocks(stocks_to_delete: list[models.Stock]) -> None:
def batch_delete_stocks(
stocks_to_delete: list[models.Stock], author_id: int | None, user_connect_as: bool | None
) -> None:
# We want to check that all stocks can be deleted first
for stock in stocks_to_delete:
validation.check_stock_is_deletable(stock)

for stock in stocks_to_delete:
_delete_stock(stock)
_delete_stock(stock, author_id, user_connect_as)


def get_or_create_label(label: str, venue: offerers_models.Venue) -> models.PriceCategoryLabel:
Expand Down
6 changes: 6 additions & 0 deletions api/src/pcapi/core/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ def has_partner_page(self) -> bool:
).scalar()
return has_partner_page

@property
def is_connect_as(self) -> bool:
if self.impersonator:
return True
return False


class DiscordUser(PcObject, Base, Model):
__tablename__ = "discord_user"
Expand Down
12 changes: 11 additions & 1 deletion api/src/pcapi/routes/backoffice/bookings/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ class CancelCollectiveBookingForm(FlaskForm):
choices=utils.choices_from_enum(
educational_models.CollectiveBookingCancellationReasons,
formatter=filters.format_booking_cancellation,
exclude_opts=(
educational_models.CollectiveBookingCancellationReasons.OFFERER,
educational_models.CollectiveBookingCancellationReasons.OFFERER_CONNECT_AS,
educational_models.CollectiveBookingCancellationReasons.PUBLIC_API,
),
),
)

Expand All @@ -224,7 +229,12 @@ class CancelIndividualBookingForm(FlaskForm):
reason = fields.PCSelectWithPlaceholderValueField(
"Raison",
choices=utils.choices_from_enum(
bookings_models.BookingCancellationReasons, formatter=filters.format_booking_cancellation
bookings_models.BookingCancellationReasons,
formatter=filters.format_booking_cancellation,
exclude_opts=(
bookings_models.BookingCancellationReasons.OFFERER,
bookings_models.BookingCancellationReasons.OFFERER_CONNECT_AS,
),
),
)

Expand Down
76 changes: 76 additions & 0 deletions api/src/pcapi/routes/backoffice/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,24 @@ def format_booking_cancellation(
bookings_models.BookingCancellationReasons.OFFERER
| educational_models.CollectiveBookingCancellationReasons.OFFERER
):
if author:
return Markup('Annulée par l\'acteur culturel (<a href="{url}">{email}</a>)').format(
url=url_for("backoffice_web.pro_user.get", user_id=author.id),
email=author.email,
)
return "Annulée par l'acteur culturel"
case (
bookings_models.BookingCancellationReasons.OFFERER_CONNECT_AS
| educational_models.CollectiveBookingCancellationReasons.OFFERER_CONNECT_AS
):
if author:
return Markup(
'Annulée pour l\'acteur culturel par <a href="{url}">{full_name}</a> via Connect As'
).format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée pour l'acteur culturel via Connect As"
case (
bookings_models.BookingCancellationReasons.BENEFICIARY
| educational_models.CollectiveBookingCancellationReasons.BENEFICIARY
Expand Down Expand Up @@ -266,6 +283,65 @@ def format_booking_cancellation(
full_name=author.full_name,
)
return "Annulée sur le backoffice"
case (
educational_models.CollectiveBookingCancellationReasons.BACKOFFICE_EVENT_CANCELLED
| bookings_models.BookingCancellationReasons.BACKOFFICE_EVENT_CANCELLED
):
if author:
return Markup(
'Annulée sur le backoffice par <a href="{url}">{full_name}</a> pour annulation d’évènement'
).format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée sur le backoffice pour annulation d’évènement"
case (
educational_models.CollectiveBookingCancellationReasons.BACKOFFICE_OVERBOOKING
| bookings_models.BookingCancellationReasons.BACKOFFICE_OVERBOOKING
):
if author:
return Markup('Annulée sur le backoffice par <a href="{url}">{full_name}</a> pour surbooking').format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée sur le backoffice pour surbooking"
case (
educational_models.CollectiveBookingCancellationReasons.BACKOFFICE_BENEFICIARY_REQUEST
| bookings_models.BookingCancellationReasons.BACKOFFICE_BENEFICIARY_REQUEST
):
if author:
return Markup(
'Annulée sur le backoffice par <a href="{url}">{full_name}</a> sur demande du bénéficiaire'
).format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée sur le backoffice sur demande du bénéficiaire"
case (
educational_models.CollectiveBookingCancellationReasons.BACKOFFICE_OFFER_MODIFIED
| bookings_models.BookingCancellationReasons.BACKOFFICE_OFFER_MODIFIED
):
if author:
return Markup(
'Annulée sur le backoffice par <a href="{url}">{full_name}</a> pour modification des informations de l\'offre'
).format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée sur le backoffice pour modification des informations de l'offre"
case (
educational_models.CollectiveBookingCancellationReasons.BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS
| bookings_models.BookingCancellationReasons.BACKOFFICE_OFFER_WITH_WRONG_INFORMATIONS
):
if author:
return Markup(
"Annulée sur le backoffice par <a href=\"{url}\">{full_name}</a> pour erreur d'information dans l'offre"
).format(
url=url_for("backoffice_web.bo_users.get_bo_user", user_id=author.id),
full_name=author.full_name,
)
return "Annulée sur le backoffice pour pour erreur d'information dans l'offre"

case (
bookings_models.BookingCancellationReasons.REFUSED_BY_INSTITUTE
| educational_models.CollectiveBookingCancellationReasons.REFUSED_BY_INSTITUTE
Expand Down
4 changes: 3 additions & 1 deletion api/src/pcapi/routes/pro/collective_bookings.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ def cancel_collective_offer_booking(offer_id: int) -> None:
check_user_has_access_to_offerer(current_user, offerer.id)

try:
educational_api_booking.cancel_collective_offer_booking(offer_id)
educational_api_booking.cancel_collective_offer_booking(
offer_id, current_user.real_user.id, current_user.is_connect_as
)
except collective_exceptions.CollectiveStockNotFound:
raise ApiErrors(
{"code": "NO_ACTIVE_STOCK_FOUND", "message": "No active stock has been found with this id"}, 404
Expand Down
2 changes: 1 addition & 1 deletion api/src/pcapi/routes/pro/offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def delete_stocks(offer_id: int, body: offers_serialize.DeleteStockListBody) ->

rest.check_user_has_access_to_offerer(current_user, offer.venue.managingOffererId)
stocks_to_delete = [stock for stock in offer.stocks if stock.id in body.ids_to_delete]
offers_api.batch_delete_stocks(stocks_to_delete)
offers_api.batch_delete_stocks(stocks_to_delete, current_user.real_user.id, current_user.is_connect_as)


@private_api.route("/offers/<int:offer_id>/stocks/all-delete", methods=["POST"])
Expand Down
2 changes: 1 addition & 1 deletion api/src/pcapi/routes/pro/stocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def delete_stock(stock_id: int) -> serialization.StockIdResponseModel:
offerer_id = stock.offer.venue.managingOffererId
check_user_has_access_to_offerer(current_user, offerer_id)
try:
offers_api.delete_stock(stock)
offers_api.delete_stock(stock, current_user.real_user.id, current_user.is_connect_as)
except offers_exceptions.OfferEditionBaseException as error:
raise ApiErrors(error.errors, status_code=400)
return serialization.StockIdResponseModel.from_orm(stock)
Loading

0 comments on commit 75191fa

Please sign in to comment.