-module(logic_booking). -include("records.hrl"). -export([create_booking/2, confirm_booking/2, confirm_booking/3, cancel_booking/2, cancel_booking/3, get_booking/2, list_bookings/2, list_user_bookings/1, delete_booking/2, list_bookings_admin/0, get_booking_admin/1, list_event_bookings/1, list_event_bookings/2, process_timeout_bookings/0, cancel_pending_for_owner/1, cancel_pending_for_calendar/1]). %%%------------------------------------------------------------------- %%% @doc Создание бронирования с учётом commercial / confirmation / capacity. %%% @end %%%------------------------------------------------------------------- -spec create_booking(UserId :: binary(), EventId :: binary()) -> {ok, #booking{}} | {error, full | already_booked | not_found | personal_calendar | subscription_inactive | own_event | event_not_active | access_denied}. create_booking(UserId, EventId) -> case core_event:get_by_id(EventId) of {ok, #event{status = active} = Event} -> case core_calendar:get_by_id(Event#event.calendar_id) of {ok, Calendar} -> create_on_calendar(UserId, Event, Calendar); {error, not_found} -> {error, not_found} end; {ok, _} -> {error, event_not_active}; {error, not_found} -> {error, not_found} end. create_on_calendar(UserId, _Event, #calendar{owner_id = UserId}) -> {error, own_event}; create_on_calendar(_UserId, _Event, #calendar{type = personal}) -> {error, personal_calendar}; create_on_calendar(UserId, Event, #calendar{type = commercial} = Calendar) -> case logic_calendar:booking_open(Calendar) of false -> {error, subscription_inactive}; true -> case active_booking(Event#event.id, UserId) of {ok, _} -> {error, already_booked}; {error, not_found} -> case check_capacity(Event#event.id, Event#event.capacity) of {ok, _} -> Initial = initial_status(Calendar#calendar.confirmation), case core_booking:create(Event#event.id, UserId, Initial) of {ok, Booking} when Initial =:= confirmed -> Now = calendar:universal_time(), core_booking:update(Booking#booking.id, [{status, confirmed}, {confirmed_at, Now}]); Other -> Other end; {error, full} -> {error, full} end end end; create_on_calendar(_, _, _) -> {error, access_denied}. initial_status(auto) -> confirmed; initial_status(_) -> pending. %%%------------------------------------------------------------------- -spec confirm_booking(BookingId :: binary(), UserId :: binary()) -> {ok, #booking{}} | {error, not_found | access_denied | full}. confirm_booking(BookingId, UserId) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> case can_manage_event_bookings(UserId, Booking#booking.event_id) of true -> case Booking#booking.status of pending -> case event_capacity_ok(Booking#booking.event_id) of true -> Now = calendar:universal_time(), core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]); false -> {error, full} end; _ -> {error, access_denied} end; {error, Reason} -> {error, Reason} end; Error -> Error end. -spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) -> {ok, #booking{}} | {error, not_found | access_denied | full}. confirm_booking(UserId, BookingId, confirm) -> confirm_booking(BookingId, UserId); confirm_booking(UserId, BookingId, decline) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> case can_manage_event_bookings(UserId, Booking#booking.event_id) of true -> case Booking#booking.status of pending -> core_booking:update(BookingId, [{status, cancelled}]); _ -> {error, access_denied} end; {error, Reason} -> {error, Reason} end; Error -> Error end. -spec cancel_booking(BookingId :: binary(), UserId :: binary()) -> {ok, #booking{}} | {error, not_found | access_denied}. cancel_booking(BookingId, UserId) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> case Booking#booking.status of cancelled -> {ok, Booking}; _ -> case Booking#booking.user_id =:= UserId of true -> core_booking:update(BookingId, [{status, cancelled}]); false -> {error, access_denied} end end; Error -> Error end. -spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) -> {ok, #booking{}} | {error, not_found | access_denied}. cancel_booking(UserId, BookingId, cancel) -> cancel_booking(BookingId, UserId). -spec get_booking(BookingId :: binary(), UserId :: binary()) -> {ok, #booking{}} | {error, not_found | access_denied}. get_booking(BookingId, UserId) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> case Booking#booking.user_id =:= UserId of true -> {ok, Booking}; false -> case can_manage_event_bookings(UserId, Booking#booking.event_id) of true -> {ok, Booking}; {error, _} -> {error, access_denied} end end; Error -> Error end. -spec list_bookings(EventId :: binary(), UserId :: binary()) -> {ok, [#booking{}]}. list_bookings(EventId, UserId) -> {ok, Bookings} = core_booking:list_by_event(EventId), Filtered = case admin_utils:is_admin(UserId) of true -> Bookings; false -> [B || B <- Bookings, B#booking.user_id =:= UserId] end, {ok, Filtered}. -spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}. list_user_bookings(UserId) -> core_booking:list_by_user(UserId). -spec delete_booking(BookingId :: binary(), UserId :: binary()) -> ok | {error, not_found | access_denied}. delete_booking(BookingId, UserId) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> case Booking#booking.user_id =:= UserId of true -> core_booking:delete(BookingId); false -> {error, access_denied} end; Error -> Error end. -spec get_booking_admin(BookingId :: binary()) -> {ok, #booking{}} | {error, not_found}. get_booking_admin(BookingId) -> core_booking:get_by_id(BookingId). -spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}. list_event_bookings(EventId) -> core_booking:list_by_event(EventId). -spec list_event_bookings(UserId :: binary(), EventId :: binary()) -> {ok, [#booking{}]} | {error, not_found | access_denied}. list_event_bookings(UserId, EventId) -> case can_manage_event_bookings(UserId, EventId) of true -> core_booking:list_by_event(EventId); {error, Reason} -> {error, Reason} end. -spec list_bookings_admin() -> {ok, [#booking{}]}. list_bookings_admin() -> {ok, core_booking:list_all()}. %%%------------------------------------------------------------------- %%% @doc Авто-confirm/cancel по политике {timeout, N}. %%% @end %%%------------------------------------------------------------------- -spec process_timeout_bookings() -> ok. process_timeout_bookings() -> NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), Pending = mnesia:dirty_match_object(#booking{status = pending, _ = '_'}), lists:foreach(fun(B) -> maybe_timeout(B, NowSec) end, Pending), ok. maybe_timeout(#booking{id = Id, event_id = EventId, created_at = Created} = Booking, NowSec) -> case core_event:get_by_id(EventId) of {ok, Event} -> case core_calendar:get_by_id(Event#event.calendar_id) of {ok, #calendar{confirmation = {timeout, N}}} when is_integer(N), N > 0 -> CreatedSec = calendar:datetime_to_gregorian_seconds(Created), case NowSec - CreatedSec >= N of true -> case event_capacity_ok(EventId) of true -> Now = calendar:universal_time(), _ = core_booking:update(Id, [{status, confirmed}, {confirmed_at, Now}]); false -> _ = core_booking:update(Id, [{status, cancelled}]) end; false -> ok end; _ -> ok end; _ -> ok end, Booking. %%%------------------------------------------------------------------- %%% @doc Отмена всех pending владельца (expire подписки). %%% @end %%%------------------------------------------------------------------- -spec cancel_pending_for_owner(UserId :: binary()) -> ok. cancel_pending_for_owner(UserId) -> case core_calendar:list_by_owner(UserId) of {ok, Cals} -> lists:foreach(fun(#calendar{id = Id, type = commercial}) -> cancel_pending_for_calendar(Id); (_) -> ok end, Cals); _ -> ok end. -spec cancel_pending_for_calendar(CalendarId :: binary()) -> ok. cancel_pending_for_calendar(CalendarId) -> case core_event:list_by_calendar(CalendarId) of {ok, Events} -> lists:foreach(fun(#event{id = EventId}) -> {ok, Bookings} = core_booking:list_by_event(EventId), lists:foreach(fun (#booking{id = Bid, status = pending}) -> _ = core_booking:update(Bid, [{status, cancelled}]); (_) -> ok end, Bookings) end, Events); _ -> ok end, ok. %%%=================================================================== %%% INTERNAL %%%=================================================================== -spec can_manage_event_bookings(UserId :: binary(), EventId :: binary()) -> true | {error, not_found | access_denied}. can_manage_event_bookings(UserId, EventId) -> case core_event:get_by_id(EventId) of {ok, Event} -> case core_calendar:get_by_id(Event#event.calendar_id) of {ok, Calendar} -> OwnerOk = Calendar#calendar.owner_id =:= UserId, AdminOk = admin_utils:is_admin(UserId), SpecOk = is_binary(Event#event.specialist_id) andalso Event#event.specialist_id =/= <<>> andalso Event#event.specialist_id =:= UserId andalso core_calendar_specialist:is_active_specialist( Calendar#calendar.id, UserId), case OwnerOk orelse AdminOk orelse SpecOk of true -> true; false -> {error, access_denied} end; {error, not_found} -> {error, not_found} end; {error, not_found} -> {error, not_found} end. -spec check_capacity(EventId :: binary(), Capacity :: integer() | undefined) -> {ok, integer() | unlimited} | {error, full}. check_capacity(_EventId, undefined) -> {ok, unlimited}; check_capacity(_EventId, 0) -> {ok, unlimited}; check_capacity(EventId, Capacity) -> case occupied_slots(EventId) < Capacity of true -> {ok, Capacity - occupied_slots(EventId)}; false -> {error, full} end. event_capacity_ok(EventId) -> case core_event:get_by_id(EventId) of {ok, #event{capacity = Cap}} when Cap =:= undefined; Cap =:= 0 -> true; {ok, #event{capacity = Cap}} when is_integer(Cap) -> %% при confirm текущий pending уже в occupied — слот свой, ок если <= Cap occupied_slots(EventId) =< Cap; _ -> false end. occupied_slots(EventId) -> {ok, Bookings} = core_booking:list_by_event(EventId), length([B || B <- Bookings, B#booking.status =:= pending orelse B#booking.status =:= confirmed]). active_booking(EventId, UserId) -> Match = #booking{event_id = EventId, user_id = UserId, _ = '_'}, case [B || B <- mnesia:dirty_match_object(Match), B#booking.status =:= pending orelse B#booking.status =:= confirmed] of [B | _] -> {ok, B}; [] -> {error, not_found} end.