#include "collection.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <limits>
#include <utility>
CollectionService::CollectionService(
DataSourceInterface& data_source,
ClockInterface& clock,
mw::CryptoInterface& crypto,
std::uint32_t maximum_accumulated_pulls)
: data_source_(data_source),
clock_(clock),
crypto_(crypto),
maximum_accumulated_pulls_(maximum_accumulated_pulls)
{}
std::pair<std::uint32_t, std::int64_t>
CollectionService::effectivePullState(
const User& user, std::int64_t current_day) const
{
const std::int64_t elapsed = std::max<std::int64_t>(
0, current_day - user.pull_refresh_day);
const std::uint64_t available = std::min<std::uint64_t>(
maximum_accumulated_pulls_,
static_cast<std::uint64_t>(user.stored_pulls) +
static_cast<std::uint64_t>(elapsed));
return {
static_cast<std::uint32_t>(available),
std::max(current_day, user.pull_refresh_day)};
}
mw::E<User> CollectionService::refresh(std::int64_t user_id)
{
const std::int64_t current_day = utcDay(clock_.now());
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto user = (*transaction)->getUserForUpdate(user_id);
if(!user)
{
return std::unexpected(std::move(user.error()));
}
if(!*user)
{
return std::unexpected(mw::httpError(404, "User not found"));
}
if(!(**user).username)
{
return std::unexpected(mw::httpError(
409, "Username onboarding is required"));
}
const auto [available, refresh_day] = effectivePullState(
**user, current_day);
auto updated = (*transaction)->updatePullState(
user_id, available, refresh_day);
if(!updated)
{
return std::unexpected(std::move(updated.error()));
}
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
(**user).stored_pulls = available;
(**user).pull_refresh_day = refresh_day;
return std::move(**user);
}
mw::E<PullResult> CollectionService::pull(std::int64_t user_id)
{
const std::int64_t current_day = utcDay(clock_.now());
auto transaction = data_source_.beginTransaction();
if(!transaction)
{
return std::unexpected(std::move(transaction.error()));
}
auto user = (*transaction)->getUserForUpdate(user_id);
if(!user)
{
return std::unexpected(std::move(user.error()));
}
if(!*user)
{
return std::unexpected(mw::httpError(404, "User not found"));
}
if(!(**user).username)
{
return std::unexpected(mw::httpError(
409, "Username onboarding is required"));
}
const auto [available, refresh_day] = effectivePullState(
**user, current_day);
auto refreshed = (*transaction)->updatePullState(
user_id, available, refresh_day);
if(!refreshed)
{
return std::unexpected(std::move(refreshed.error()));
}
if(available == 0)
{
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
return std::unexpected(mw::httpError(
409, "No pulls are available"));
}
auto cards = (*transaction)->getPoolCardsForUpdate();
if(!cards)
{
return std::unexpected(std::move(cards.error()));
}
auto entries = card_pool_.calculate(*cards);
if(entries.empty())
{
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
return std::unexpected(mw::httpError(409, "Card pool is empty"));
}
auto selected = card_pool_.select(entries, crypto_);
if(!selected)
{
return std::unexpected(std::move(selected.error()));
}
auto decremented = (*transaction)->updatePullState(
user_id, available - 1, refresh_day);
if(!decremented)
{
return std::unexpected(std::move(decremented.error()));
}
auto quantity = (*transaction)->incrementHolding(
user_id, selected->card.id);
if(!quantity)
{
return std::unexpected(std::move(quantity.error()));
}
auto committed = (*transaction)->commit();
if(!committed)
{
return std::unexpected(std::move(committed.error()));
}
return PullResult{std::move(selected->card), *quantity};
}