Initial commit

This commit is contained in:
user
2026-07-12 14:34:52 +04:00
commit 3abf6bd9c2
557 changed files with 68706 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
#include "base_generator.h"
#include <QElapsedTimer>
#include <QVariant>
BaseGenerator::ShouldLogSuccess
BaseGenerator::shouldLogAllSuccesses() const noexcept {
return [this]() { return true; };
}
BaseGenerator::ShouldLogSuccess
BaseGenerator::shouldLogEveryNPercent(int percent) const noexcept {
if (percent <= 0) {
return [this]() { return false; };
}
int oldLastPercentage = 0;
return [this, percent, oldLastPercentage]() mutable {
if (oldLastPercentage == lastPercentage_) {
return false;
}
oldLastPercentage = lastPercentage_;
return !bool(lastPercentage_ % percent);
};
}
BaseGenerator::ShouldLogSuccess
BaseGenerator::shouldLogFirstAndLastPercent() const noexcept {
return [this]() { return createdRecords_ == 1 || lastPercentage_ == 100; };
}
BaseGenerator::ShouldLogSuccess BaseGenerator::shouldLogEveryNSeconds(
int seconds, const ShouldLogSuccess &performOtherwise) const noexcept {
QElapsedTimer timer;
timer.start();
return [this, timer, seconds, performOtherwise]() mutable {
if (timer.elapsed() >= seconds * 1000) {
timer.restart();
return true;
}
return performOtherwise();
};
}
BaseGenerator::ShouldLogSuccess
BaseGenerator::defaultShouldLog(int percent, int seconds) const noexcept {
ShouldLogSuccess first =
shouldLogEveryNSeconds(seconds, shouldLogFirstAndLastPercent());
ShouldLogSuccess second = shouldLogEveryNPercent(percent);
return [first, second]() { return first() || second(); };
}
void BaseGenerator::generateAll(const ShouldLogSuccess &shouldLogSuccess) {
auto withoutLimit = []() { return true; };
generateImplementation(withoutLimit, shouldLogSuccess);
}
void BaseGenerator::generateByFillRatio(
double fillRatio, const ShouldLogSuccess &shouldLogSuccess) {
fillRatio >= 1.0 ? fillRatio = 1.0 : fillRatio;
qInfo() << "Limit fill ratio of records to create: " << fillRatio;
auto shouldContinue = [this, fillRatio]() -> bool {
double currentFillRatio = lastPercentage_ / 100.0;
bool shouldContinue = currentFillRatio < fillRatio;
if (!shouldContinue) {
qInfo() << "Limit of records to create reached";
}
return shouldContinue;
};
generateImplementation(shouldContinue, shouldLogSuccess);
};
void BaseGenerator::generateByRecordCount(
qsizetype totalRecordsToCreate, const ShouldLogSuccess &shouldLogSuccess) {
int limit = std::min(totalRecordsToCreate, calculateRemainingCapacity());
qInfo() << "Limit of records to create: " << limit;
auto shouldContinue = [this, limit]() -> bool {
bool shouldContinue = createdRecords_ < limit;
if (!shouldContinue) {
qInfo() << "Limit of records to create reached";
}
return shouldContinue;
};
generateImplementation(shouldContinue, shouldLogSuccess);
};
void BaseGenerator::generateImplementation(
std::function<bool()> shouldContinue,
std::function<bool()> shouldLogSuccess) {
maximumRecords_ = calculateRemainingCapacity();
qInfo() << "The number of records available for creation:" << maximumRecords_;
qInfo().noquote() << createTemplateMessage() << "Started";
while (shouldContinue() && prepareData()) {
bool success;
RepositoryResult result = createRecord();
if (!result.success) {
qWarning().noquote() << createTemplateMessage()
<< createFailureMessage(result.errorMessage);
continue;
}
++createdRecords_;
updatePercentage();
if (shouldLogSuccess()) {
qInfo().noquote() << createTemplateMessage() << createSuccessMessage();
}
clearStaleData();
}
qInfo().noquote() << createTemplateMessage() << "Completed";
}
void BaseGenerator::updatePercentage() {
int numerator = maximumRecords_ - calculateRemainingCapacity();
int newPercentage = std::floor(numerator * 100.0 / maximumRecords_);
lastPercentage_ = std::max(newPercentage, lastPercentage_);
}
QString BaseGenerator::createFailureMessage(const QString &errorMessage) const {
QString failureMessage = "Failure: Error message:\n\t%1";
return failureMessage.arg(errorMessage);
}
QString BaseGenerator::createSuccessMessage() const { return "Success"; }
QString BaseGenerator::createTemplateMessage() const {
auto digitCount = [](int number) {
if (number == 0) {
return 1;
}
return static_cast<int>(std::log10(std::abs(number))) + 1;
};
int maximumRecordsValue = calculateRemainingCapacity();
int newFieldWidth =
std::max(digitCount(createdRecords_), digitCount(maximumRecordsValue));
fieldWidth_ = std::max(fieldWidth_, newFieldWidth);
static constexpr int progressBarSize = 25;
static constexpr int progressBarStep = 100 / progressBarSize;
const QString progressBar(lastPercentage_ / progressBarStep, QChar('#'));
return QString("Creating records of entity %1 [ %2 <~ %3] [%4]:")
.arg(entityName_)
.arg(createdRecords_, fieldWidth_, 10, QChar(' '))
.arg(maximumRecordsValue, fieldWidth_, 10, QChar(' '))
.arg(progressBar.leftJustified(progressBarSize, QChar(' ')));
};
+72
View File
@@ -0,0 +1,72 @@
#ifndef BASE_GENERATOR_H
#define BASE_GENERATOR_H
#include "generator_interface.h"
#include "range_limiter.h"
#include <QDateTime>
#include <QRandomGenerator>
class BaseGenerator : public GeneratorInterface, public virtual RangeLimiter {
private:
QString entityName_;
int lastPercentage_;
mutable int fieldWidth_;
qsizetype createdRecords_;
qsizetype maximumRecords_;
public:
ShouldLogSuccess defaultShouldLog(int percent, int seconds) const noexcept;
ShouldLogSuccess shouldLogAllSuccesses() const noexcept;
ShouldLogSuccess shouldLogEveryNPercent(int percent) const noexcept;
ShouldLogSuccess shouldLogEveryNSeconds(
int seconds, const ShouldLogSuccess &performOtherwise) const noexcept;
ShouldLogSuccess shouldLogFirstAndLastPercent() const noexcept;
public:
// Virtual methods
void generateAll(const ShouldLogSuccess &shouldLogSuccess) final;
void generateByFillRatio(double fillRatio,
const ShouldLogSuccess &shouldLogSuccess) final;
void generateByRecordCount(qint64 totalRecordsToCreate,
const ShouldLogSuccess &shouldLogSuccess) final;
protected:
inline BaseGenerator(const QString &entityName) noexcept;
// General methods
inline QString formatDateTime(const QDateTime &dateTime_) const noexcept;
inline qsizetype createdRecords() const noexcept;
private: // create* methods depends on clearStaleData and createRecord
QString createFailureMessage(const QString &errorMessage) const override;
QString createSuccessMessage() const override;
QString createTemplateMessage() const override;
// Non-virtual method
void generateImplementation(std::function<bool()> shouldContinue,
std::function<bool()> shouldLogSuccess);
void updatePercentage(); // Depends on createRecord
protected: // Non-const methods
virtual RepositoryResult createRecord() = 0;
virtual bool prepareData() = 0;
virtual void clearStaleData() = 0;
};
BaseGenerator::BaseGenerator(const QString &entityName) noexcept
: createdRecords_(0), entityName_(entityName), fieldWidth_(0),
lastPercentage_(-1) {}
inline qsizetype BaseGenerator::createdRecords() const noexcept {
return createdRecords_;
}
QString
BaseGenerator::formatDateTime(const QDateTime &dateTime_) const noexcept {
return dateTime_.toString("yyyy-MM-dd'T'HH:mm:ss.000");
}
#endif
+75
View File
@@ -0,0 +1,75 @@
#include "date_time_generator.h"
DateTimeGenerator::DateTimeGenerator(const QPair<qint64, qint64> &dayRange,
const QPair<QTime, QTime> &timeRange)
: dayRange_(dayRange), timeRange_(timeRange) {
auto &[lowestDate, highestDate] = dayRange_;
if (lowestDate < 0 || highestDate < 0) {
qFatal() << "Lowest or highest date is negative";
}
if (lowestDate > highestDate) {
qCritical() << "Lowest date is greater than the highest date, values will "
"be swapped";
std::swap(lowestDate, highestDate);
}
auto &[lowestTime, highestTime] = timeRange_;
if (!lowestTime.isValid() || !highestTime.isValid()) {
qFatal() << "Lowest or highest time is not valid";
}
if (lowestTime > highestTime) {
qCritical() << "Lowest time is greater than the highest time, values will "
"be swapped";
std::swap(lowestTime, highestTime);
}
}
QDateTime DateTimeGenerator::generate(const QDateTime &upperBoundInclusive) {
auto [lowestDate, highestDate] = dayRange_;
auto [lowestTime, highestTime] = timeRange_;
const QDate &maxDateValue = upperBoundInclusive.date();
const qint64 maxTimeValueMs =
upperBoundInclusive.time().msecsSinceStartOfDay();
QDate epochDate = QDateTime::fromMSecsSinceEpoch(0).date();
qint64 daysFromEpochDateToMaxDateValue = epochDate.daysTo(maxDateValue);
highestDate = std::min(highestDate, daysFromEpochDateToMaxDateValue);
qint64 minusDeltaDays = -bounded(lowestDate, highestDate);
QDate dateSoldAt = maxDateValue.addDays(minusDeltaDays);
qint64 lowestTimeMs = lowestTime.msecsSinceStartOfDay();
qint64 highestTimeMs = highestTime.msecsSinceStartOfDay() + 1;
qint64 msecsSinceStartOfDay = bounded(lowestTimeMs, highestTimeMs);
if (msecsSinceStartOfDay > maxTimeValueMs) {
msecsSinceStartOfDay = maxTimeValueMs;
}
QTime timeSoldAt = QTime::fromMSecsSinceStartOfDay(msecsSinceStartOfDay);
return {std::move(dateSoldAt), std::move(timeSoldAt)};
}
QDateTime DateTimeGenerator::generate(const QDateTime &lowerBoundInclusive,
const QDateTime &upperBoundInclusive) {
if (lowerBoundInclusive > upperBoundInclusive) {
qFatal() << "Lower bound date is greater than the upper bound date";
}
const QDate &minDateValue = lowerBoundInclusive.date();
const QTime &minTimeValue = lowerBoundInclusive.time();
QDateTime unboundedLower = generate(upperBoundInclusive);
QDate boundedDate = unboundedLower.date() <= minDateValue
? minDateValue
: unboundedLower.date();
QTime boundedTime = unboundedLower.time() <= minTimeValue
? minTimeValue
: unboundedLower.time();
return {std::move(boundedDate), std::move(boundedTime)};
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef DATE_TIME_GENERATOR_H
#define DATE_TIME_GENERATOR_H
#include "range_limiter.h"
#include <QDateTime>
class DateTimeGenerator : public virtual RangeLimiter {
private:
QPair<qint64, qint64> dayRange_;
QPair<QTime, QTime> timeRange_;
public:
DateTimeGenerator(const QPair<qint64, qint64> &dayRange,
const QPair<QTime, QTime> &timeRange);
QDateTime generate(const QDateTime &lowerBoundInclusive,
const QDateTime &upperBoundInclusive);
QDateTime generate(const QDateTime &upperBoundInclusive);
inline void setDayRange(const QPair<qint64, qint64> &dayRange);
inline void setTimeRange(const QPair<QTime, QTime> &timeRange);
inline void setUpperBoundInclusive(const QDateTime &upperBoundInclusive);
};
inline void
DateTimeGenerator::setDayRange(const QPair<qint64, qint64> &dayRange) {
dayRange_ = dayRange;
}
inline void
DateTimeGenerator::setTimeRange(const QPair<QTime, QTime> &timeRange) {
timeRange_ = timeRange;
}
#endif // DATE_TIME_GENERATOR_H
+29
View File
@@ -0,0 +1,29 @@
#ifndef GENERATOR_INTERFACE_H
#define GENERATOR_INTERFACE_H
#include "repository_result.h"
#include <QString>
class GeneratorInterface {
public:
using ShouldLogSuccess = std::function<bool()>;
public:
inline virtual ~GeneratorInterface() noexcept = default;
virtual void generateAll(const ShouldLogSuccess &shouldLogSuccess) = 0;
virtual void
generateByFillRatio(double fillRatio,
const ShouldLogSuccess &shouldLogSuccess) = 0;
virtual void
generateByRecordCount(qint64 totalRecordsToCreate,
const ShouldLogSuccess &shouldLogSuccess) = 0;
protected: // Const methods
virtual QString createFailureMessage(const QString &errorMessage) const = 0;
virtual QString createSuccessMessage() const = 0;
virtual QString createTemplateMessage() const = 0;
virtual qsizetype calculateRemainingCapacity() const = 0;
};
#endif
+95
View File
@@ -0,0 +1,95 @@
#include "model_manager.h"
#include "genres_model.h"
#include "halls_model.h"
#include "movies_model.h"
#include "refunds_model.h"
#include "schema_metadata.h"
#include "sessions_model.h"
#include "tickets_model.h"
ModelManager::ModelManager() {
// Get connection to the database
database_ = QSqlDatabase::database();
if (!database_.isOpen()) {
qDebug() << "Database is not open. Cannot initialize models";
return;
}
initialize();
}
ModelManager::~ModelManager() {
qInfo() << "Models manager is being destroyed";
}
ModelManager &ModelManager::instance() {
static ModelManager instance;
return instance;
}
QStringList ModelManager::registeredModelTableNames() const {
return models_.keys();
}
QStringList
ModelManager::registeredModelTableNames(QString databaseTableName) const {
QStringList registeredModelTableNames;
for (const auto &modelTableName : models_.keys()) {
QSqlTableModel *currentModel = getModelAs<QSqlTableModel>(modelTableName);
QString currentDatabaseTableName = currentModel->tableName();
if (currentDatabaseTableName == databaseTableName) {
registeredModelTableNames.append(modelTableName);
}
}
return registeredModelTableNames;
}
void ModelManager::initialize() {
// clang-format off
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
registerModel<GenresModel>(
metadata.modelTableName("genres"), database_);
registerModel<HallsModel>(
metadata.modelTableName("halls"), database_);
registerModel<MoviesModel>(
metadata.modelTableName("movies"), database_);
registerModel<RefundsModel>(
metadata.modelTableName("refunds"), database_);
registerModel<SessionsModel>(
metadata.modelTableName("sessions"), database_);
registerModel<TicketsModel>(
metadata.modelTableName("tickets"), database_);
// clang-format on
}
QSqlTableModel *ModelManager::getModel(const QString &modelTableName) const {
QSqlTableModel *model = models_.value(modelTableName, nullptr);
if (model == nullptr) {
qDebug() << "Model" << modelTableName << "is not registered";
return nullptr;
}
qDebug() << "Getting model" << modelTableName << "with value" << model;
return model;
}
void ModelManager::resetFilter(const QString &modelTableName) {
BaseSqlTableModel *model = getModelAs<BaseSqlTableModel>(modelTableName);
model->resetFilter();
}
void ModelManager::setMultiColumnFilter(const QString &modelTableName,
const QString &filter) {
BaseSqlTableModel *model = getModelAs<BaseSqlTableModel>(modelTableName);
model->setMultiColumnFilter(filter);
}
+113
View File
@@ -0,0 +1,113 @@
#ifndef MODEL_MANAGER_H
#define MODEL_MANAGER_H
#include <QMap>
#include <QSqlDatabase>
#include <QSqlTableModel>
#include <QString>
// The model manager inherits the QObject class to delete child objects
class ModelManager : public QObject {
Q_OBJECT
private:
QMap<QString, QSqlTableModel *> models_;
QSqlDatabase database_;
public:
// Get the singleton instance
static ModelManager &instance();
QStringList registeredModelTableNames() const;
QStringList registeredModelTableNames(QString databaseTableName) const;
// Get a model
QSqlTableModel *getModel(const QString &modelTableName) const;
// Get a model with a specific type
template <typename T> T *getModelAs(const QString &modelTableName) const;
// Configure filters
void resetFilter(const QString &modelTableName);
void setMultiColumnFilter(const QString &modelTableName,
const QString &filter);
private:
// Register a model
template <typename T>
bool registerModel(const QString &modelTableName, const QSqlDatabase &db);
// Initialize all models
void initialize();
// Initialize a specific model
template <typename T> void initializeIfExists(T *model);
// Private constructor for singleton pattern
ModelManager();
// Private destructor
~ModelManager();
// Disable copy constructor and assignment operator
ModelManager(const ModelManager &) = delete;
ModelManager &operator=(const ModelManager &) = delete;
};
// NOTE: Implementation of template methods
template <typename T> void ModelManager::initializeIfExists(T *model) {
if constexpr (std::is_invocable_v<decltype(&T::initialize), T *>) {
model->initialize();
} else {
qDebug() << "Method initialize() is not available, skipping...";
}
}
template <typename T>
T *ModelManager::getModelAs(const QString &modelTableName) const {
auto *model = dynamic_cast<T *>(getModel(modelTableName));
if (!model) {
qFatal() << "Failed to downcast model for" << modelTableName
<< "(typeid name):" << typeid(T).name();
return nullptr;
}
qInfo() << "Successfully downcast model for" << modelTableName;
return model;
}
template <typename T>
bool ModelManager::registerModel(const QString &modelTableName,
const QSqlDatabase &db) {
static_assert(std::is_base_of_v<QSqlTableModel, T>,
"Registered model must be derived from QSqlTableModel");
static_assert(
std::is_constructible<T, QObject *, const QSqlDatabase &>::value,
"Model class must have a constructor accepting QObject* and const "
"QSqlDatabase&");
qInfo() << "Registering model:" << modelTableName;
if (models_.contains(modelTableName)) {
qInfo() << "Model" << modelTableName << "is already registered";
return true;
}
T *model = new T(this, db); // Qt will delete the model
model->setObjectName(modelTableName);
initializeIfExists(model);
models_.insert(modelTableName, model);
qInfo() << "Model" << modelTableName
<< "with database table name:" << model->tableName()
<< "was registered successfully";
return true;
}
#endif // MODEL_MANAGER_H
+34
View File
@@ -0,0 +1,34 @@
#ifndef RANGE_LIMITER_H
#define RANGE_LIMITER_H
#include <QDebug>
#include <QRandomGenerator>
class RangeLimiter {
private:
mutable QRandomGenerator gen_;
public:
inline RangeLimiter();
template <typename T> inline T bounded(T highest) const noexcept;
template <typename T> inline T bounded(T lowest, T highest) const noexcept;
};
RangeLimiter::RangeLimiter() : gen_(QRandomGenerator::securelySeeded()) {}
template <typename T> T RangeLimiter::bounded(T highest) const noexcept {
return gen_.bounded(highest);
}
template <typename T>
T RangeLimiter::bounded(T lowest, T highest) const noexcept {
if (lowest >= highest) {
QString errorMessage =
"Lowest value (%1) is greater than highest value (%2) or equal to it";
qFatal().noquote() << errorMessage.arg(lowest).arg(highest);
}
return gen_.bounded(lowest, highest);
}
#endif // RANGE_LIMITER_H
+168
View File
@@ -0,0 +1,168 @@
#include "refund_generator.h"
#include "create_refund_request.h"
#include "refunds_repository.h"
#include "repository_manager.h"
#include "repository_result.h"
#include "schema_metadata.h"
#include "session_dto.h"
#include "sessions_repository.h"
#include "ticket_dto.h"
#include "tickets_repository.h"
#include <QRandomGenerator>
RefundGenerator::RefundGenerator(const QPair<double, double> &refundRatioRange,
const QPair<qint64, qint64> &dayRange,
const QPair<QTime, QTime> &timeRange)
: BaseGenerator("Refund"), DateTimeGenerator(dayRange, timeRange),
refundRatioRange_(refundRatioRange) {
auto &[lowest, highest] = refundRatioRange_;
if (lowest > highest) {
qCritical() << "Lowest refund ratio is greater than the highest refund, "
"values will be swapped";
std::swap(lowest, highest);
}
initializeRepositories(); // First
initializeTicketIdToCacheValueMap(); // After initializing
// repositories
initializeRemainingCapacity(); // After initializing ticketIdToCacheValue
}
void RefundGenerator::initializeRepositories() {
RepositoryManager &repositoryManager = RepositoryManager::instance();
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
ticketsRepository_ = repositoryManager.getRepositoryAs<TicketsRepository>(
metadata.repositoryTableName("tickets"));
refundsRepository_ = repositoryManager.getRepositoryAs<RefundsRepository>(
metadata.repositoryTableName("refunds"));
sessionsRepository_ = repositoryManager.getRepositoryAs<SessionsRepository>(
metadata.repositoryTableName("sessions"));
if (!bool(ticketsRepository_ && refundsRepository_ && sessionsRepository_)) {
qFatal() << "Failed to initialize repositories";
}
}
void RefundGenerator::initializeTicketIdToCacheValueMap() {
QVector<SessionDTO> sessions;
RepositoryResult resultGetAllSessions =
sessionsRepository_->getAllSessions(sessions);
if (!resultGetAllSessions.success) {
qFatal() << "Failed to retrieve sessions";
}
QMap<qint32 /* sessionId */,
QPair<QDateTime /* beginAt */, double /* ticketPrice */>>
sessionIdToBeginAtAndTicketPrice;
for (const SessionDTO &session : sessions) {
sessionIdToBeginAtAndTicketPrice[session.sessionId()] =
qMakePair(session.beginAt(), session.ticketPrice());
}
QVector<TicketDTO> tickets;
RepositoryResult resultGetAllTickets =
ticketsRepository_->getAllTickets(tickets);
if (!resultGetAllTickets.success) {
qFatal() << "Failed to retrieve tickets";
}
for (const auto &ticket : tickets) {
const auto &[beginAt, ticketPrice] =
sessionIdToBeginAtAndTicketPrice[ticket.sessionId()];
CacheValue cacheValue{beginAt, ticket.soldAt(), ticketPrice};
if (calculateDurationMs(cacheValue) < 2) {
continue;
}
ticketIdToCacheValue_[ticket.ticketId()] = cacheValue;
}
}
QString
RefundGenerator::createFailureMessage(const QString &errorMessage) const {
const QString dateTimeFormat = "yyyy-MM-dd hh:mm:ss.zzz";
QString additionalInfo =
QString("Additional info: Ticket id: %1 Refund amount: %2 Sold at: %3 "
"Refund at: %4 Begin at: %5")
.arg(reservationContext_.ticketId)
.arg(reservationContext_.refundAmount)
.arg(reservationContext_.soldAt.toString(dateTimeFormat))
.arg(reservationContext_.refundAt.toString(dateTimeFormat))
.arg(reservationContext_.beginAt.toString(dateTimeFormat));
QString failureMessage = "Failure:\n\t%1\n\tError message: %2";
return failureMessage.arg(additionalInfo).arg(errorMessage);
}
QString RefundGenerator::createSuccessMessage() const {
return QString("Success: EId: %1 RAm: %2 RAt: %3")
.arg(reservationContext_.ticketId)
.arg(reservationContext_.refundAmount)
.arg(reservationContext_.refundAt.toString("yyyy-MM-dd hh:mm:ss"));
}
qsizetype RefundGenerator::calculateRemainingCapacity() const {
return remainingCapacity_;
}
RepositoryResult RefundGenerator::createRecord() {
CreateRefundRequest request;
request.setRefundAmount(reservationContext_.refundAmount);
request.setRefundAt(reservationContext_.refundAt);
request.setTicketId(reservationContext_.ticketId);
RepositoryResult result = refundsRepository_->createRefund(request);
if (result.success) {
--remainingCapacity_;
}
return result;
}
bool RefundGenerator::prepareData() {
if (ticketIdToCacheValue_.isEmpty()) {
qInfo() << "No tickets to refund";
return false;
}
qsizetype ticketIdIndex = bounded<qsizetype>(0, ticketIdToCacheValue_.size());
auto ticketIdIt = ticketIdToCacheValue_.cbegin();
std::advance(ticketIdIt, ticketIdIndex);
qint32 ticketId = ticketIdIt.key();
CacheValue cacheValue = ticketIdIt.value();
constexpr qint32 fiveMinutesMs = 5 * 1000 * 60;
qint64 deltaBoundMs = calculateDurationMs(cacheValue) / 2;
if (deltaBoundMs > fiveMinutesMs) {
deltaBoundMs = fiveMinutesMs;
}
const QDateTime refundAt =
// SoldAt < RefundAt < BeginAt
// SoldAt + n minute <= RefundAt <= BeginAt - n minute
generate(cacheValue.soldAt.addMSecs(+deltaBoundMs),
cacheValue.beginAt.addMSecs(-deltaBoundMs));
const auto [refundRatioLowest, refundRatioHighest] = refundRatioRange_;
int lowest = refundRatioLowest * 100;
int highest = refundRatioHighest * 100;
int numerator = bounded(lowest, highest);
double refundAmount = std::floor(numerator * cacheValue.ticketPrice) / 100.0;
ReservationContext context{cacheValue.beginAt, refundAt, cacheValue.soldAt,
refundAmount, ticketId};
reservationContext_ = std::move(context);
return true;
}
void RefundGenerator::clearStaleData() {
qint32 ticketId = reservationContext_.ticketId;
ticketIdToCacheValue_.remove(ticketId);
}
+76
View File
@@ -0,0 +1,76 @@
#ifndef REFUND_GENERATOR_H
#define REFUND_GENERATOR_H
#include "base_generator.h"
#include "date_time_generator.h"
#include <QDateTime>
#include <QRandomGenerator>
#include <QString>
// Forward declaration
class RefundsRepository;
class SessionsRepository;
class TicketsRepository;
class RefundGenerator : public BaseGenerator, public DateTimeGenerator {
private:
struct ReservationContext {
QDateTime beginAt;
QDateTime refundAt;
QDateTime soldAt;
double refundAmount;
qint32 ticketId;
};
struct CacheValue {
QDateTime beginAt;
QDateTime soldAt;
double ticketPrice;
};
private:
QMap<qint32 /* ticketId */, CacheValue> // WARNING: There can be a lot
ticketIdToCacheValue_; // of records, so 1 QPair is used, not 3 QMap
QPair<double, double> refundRatioRange_;
ReservationContext reservationContext_;
qsizetype remainingCapacity_;
RefundsRepository *refundsRepository_;
SessionsRepository *sessionsRepository_;
TicketsRepository *ticketsRepository_;
public:
RefundGenerator(const QPair<double, double> &refundRatioRange,
const QPair<qint64, qint64> &dayRange,
const QPair<QTime, QTime> &timeRange);
private:
void initializeRepositories();
void initializeTicketIdToCacheValueMap();
inline void initializeRemainingCapacity();
private:
inline qint64 calculateDurationMs(const CacheValue &cacheValue) const;
// Vitual methods
private: // Const methods
QString createFailureMessage(const QString &errorMessage) const override;
QString createSuccessMessage() const override;
qsizetype calculateRemainingCapacity() const override;
private: // Non-const methods
RepositoryResult createRecord() override;
bool prepareData() override;
void clearStaleData() override;
};
void RefundGenerator::initializeRemainingCapacity() {
remainingCapacity_ = ticketIdToCacheValue_.size();
}
qint64
RefundGenerator::calculateDurationMs(const CacheValue &cacheValue) const {
return cacheValue.soldAt.msecsTo(cacheValue.beginAt);
}
#endif
+9
View File
@@ -0,0 +1,9 @@
#include "relation_id_config.h"
RelationIdConfig::RelationIdConfig(const QString &translentTableName,
const QString &modelTableName,
const QString &primaryIdColumnName,
const QString &repositoryTableName) noexcept
: translatedTableName_(translentTableName), modelTableName_(modelTableName),
primaryIdColumnName_(primaryIdColumnName),
repositoryTableName_(repositoryTableName) {}
+66
View File
@@ -0,0 +1,66 @@
#ifndef RALATION_ID_CONFIG_H
#define RALATION_ID_CONFIG_H
#include "QString"
class RelationIdConfig {
private:
QString modelTableName_;
QString primaryIdColumnName_;
QString repositoryTableName_;
QString translatedTableName_;
public:
RelationIdConfig() = default;
explicit RelationIdConfig(const QString &databaseModelName,
const QString &modelTableName,
const QString &primaryIdColumnName,
const QString &repositoryTableName) noexcept;
inline const QString &translatedTableName() const;
inline const QString &modelTableName() const;
inline const QString &primaryIdColumnName() const;
inline const QString &repositoryTableName() const;
inline void setTranslatedTableName(const QString &translatedTableName);
inline void setModelTableName(const QString &modelTableName);
inline void setPrimaryIdColumnName(const QString &primaryIdColumnName);
inline void setRepositoryTableName(const QString &repositoryTableName);
};
const QString &RelationIdConfig::translatedTableName() const {
return translatedTableName_;
}
const QString &RelationIdConfig::modelTableName() const {
return modelTableName_;
}
const QString &RelationIdConfig::primaryIdColumnName() const {
return primaryIdColumnName_;
}
const QString &RelationIdConfig::repositoryTableName() const {
return repositoryTableName_;
}
void RelationIdConfig::setTranslatedTableName(
const QString &translatedTableName) {
translatedTableName_ = translatedTableName;
}
void RelationIdConfig::setModelTableName(const QString &modelTableName) {
modelTableName_ = modelTableName;
}
void RelationIdConfig::setPrimaryIdColumnName(
const QString &primaryIdColumnName) {
primaryIdColumnName_ = primaryIdColumnName;
}
void RelationIdConfig::setRepositoryTableName(
const QString &repositoryTableName) {
repositoryTableName_ = repositoryTableName;
}
#endif // RALATION_ID_CONFIG_H
+83
View File
@@ -0,0 +1,83 @@
#include "repository_manager.h"
#include "genres_repository.h"
#include "halls_repository.h"
#include "movies_repository.h"
#include "refunds_repository.h"
#include "roles_repository.h"
#include "schema_metadata.h"
#include "sessions_repository.h"
#include "sessions_statistics_repository.h"
#include "tickets_repository.h"
#include "users_repository.h"
RepositoryManager::RepositoryManager() {
database_ = QSqlDatabase::database();
if (!database_.isOpen()) {
qDebug() << "Database is not open. Cannot initialize repositories";
return;
}
initialize();
}
RepositoryManager::~RepositoryManager() {
qInfo() << "Repositories manager is being destroyed";
}
RepositoryManager &RepositoryManager::instance() {
static RepositoryManager instance;
return instance;
}
void RepositoryManager::initialize() {
// clang-format off
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
// Tables
registerRepository<GenresRepository>(
metadata.repositoryTableName("genres"));
registerRepository<HallsRepository>(
metadata.repositoryTableName("halls"));
registerRepository<MoviesRepository>(
metadata.repositoryTableName("movies"));
registerRepository<RefundsRepository>(
metadata.repositoryTableName("refunds"));
registerRepository<SessionsRepository>(
metadata.repositoryTableName("sessions"));
registerRepository<TicketsRepository>(
metadata.repositoryTableName("tickets"));
registerRepository<UsersRepository>(
metadata.repositoryTableName("users"));
registerRepository<RolesRepository>(
metadata.repositoryTableName("roles"));
// Views
registerRepository<SessionsStatisticsRepository>(
metadata.repositoryTableName("sessions_statistics"));
// clang-format on
}
RepositoryInterface *
RepositoryManager::getRepository(const QString &repositoryTableName) const {
RepositoryInterface *repository =
repositories_.value(repositoryTableName, nullptr);
if (repository == nullptr) {
qDebug() << "Repository" << repositoryTableName << "is not registered";
return nullptr;
}
qDebug() << "Getting model" << repositoryTableName << "with value"
<< repository;
return repository;
}
+90
View File
@@ -0,0 +1,90 @@
// repository_manager.h
#ifndef REPOSITORY_MANAGER_H
#define REPOSITORY_MANAGER_H
#include "repository_interface.h"
#include <QMap>
#include <QSqlDatabase>
// The repository manager inherits the QObject class to delete child objects
class RepositoryManager : public QObject {
Q_OBJECT
private:
// Database connection
QSqlDatabase database_;
// Map of repository names to instances
QMap<QString, RepositoryInterface *> repositories_;
public:
// Singleton instance accessor
static RepositoryManager &instance();
// Get a repository by name
RepositoryInterface *getRepository(const QString &repositoryTableName) const;
// Get a repository by name and type
template <typename T>
T *getRepositoryAs(const QString &repositoryTableName) const;
private:
// Register a new repository
template <typename T>
bool registerRepository(const QString &repositoryTableName);
// Initialize repositories
void initialize();
// Private constructor for singleton
RepositoryManager();
// Destructor to clean up resources
~RepositoryManager();
// Delete copy constructor
RepositoryManager(const RepositoryManager &) = delete;
// Delete assignment operator
RepositoryManager &operator=(const RepositoryManager &) = delete;
};
// NOTE: Implementation of template methods
template <typename T>
T *RepositoryManager::getRepositoryAs(
const QString &repositoryTableName) const {
auto *repository = dynamic_cast<T *>(getRepository(repositoryTableName));
if (!repository) {
qFatal() << "Failed to downcast repository for" << repositoryTableName
<< "(typeid name):" << typeid(T).name();
return nullptr;
}
qInfo() << "Successfully downcast repository for" << repositoryTableName;
return repository;
}
template <typename T>
bool RepositoryManager::registerRepository(const QString &repositoryTableName) {
static_assert(
std::is_constructible<T, QSqlDatabase &, QObject *>::value,
"Repository class must have a constructor accepting QSqlDatabase& and "
"QObject*");
if (repositories_.contains(repositoryTableName)) {
qInfo() << "Repository" << repositoryTableName << "is already registered";
return true;
}
auto *repository = new T(database_, this); // Qt will delete the model
repository->setObjectName(repositoryTableName);
repositories_.insert(repositoryTableName, repository);
qInfo() << "Repository" << repositoryTableName
<< "was registered successfully";
return true;
}
#endif // REPOSITORY_MANAGER_H
+137
View File
@@ -0,0 +1,137 @@
#include "schema_metadata.h"
#include <QDebug>
QMap<QString, RelationIdConfig> SchemaMetadata::databaseTableNameToConfigMap_;
std::once_flag SchemaMetadata::initFlag;
void SchemaMetadata::initialize() {
qInfo() << "Initializing schema metadata...";
RelationIdConfig genreConfig;
genreConfig.setPrimaryIdColumnName("genre_id");
genreConfig.setTranslatedTableName(tr("Genres"));
genreConfig.setModelTableName("GenresModel");
genreConfig.setRepositoryTableName("GenresRepository");
databaseTableNameToConfigMap_.insert("genres", genreConfig);
RelationIdConfig hallConfig;
hallConfig.setPrimaryIdColumnName("hall_id");
hallConfig.setTranslatedTableName(tr("Halls"));
hallConfig.setModelTableName("HallsModel");
hallConfig.setRepositoryTableName("HallsRepository");
databaseTableNameToConfigMap_.insert("halls", hallConfig);
RelationIdConfig movieConfig;
movieConfig.setPrimaryIdColumnName("movie_id");
movieConfig.setTranslatedTableName(tr("Movies"));
movieConfig.setModelTableName("MoviesModel");
movieConfig.setRepositoryTableName("MoviesRepository");
databaseTableNameToConfigMap_.insert("movies", movieConfig);
RelationIdConfig refundConfig;
refundConfig.setPrimaryIdColumnName("refund_id");
refundConfig.setTranslatedTableName(tr("Refunds"));
refundConfig.setModelTableName("RefundsModel");
refundConfig.setRepositoryTableName("RefundsRepository");
databaseTableNameToConfigMap_.insert("refunds", refundConfig);
RelationIdConfig sessionConfig;
sessionConfig.setPrimaryIdColumnName("session_id");
sessionConfig.setTranslatedTableName(tr("Sessions"));
sessionConfig.setModelTableName("SessionsModel");
sessionConfig.setRepositoryTableName("SessionsRepository");
databaseTableNameToConfigMap_.insert("sessions", sessionConfig);
RelationIdConfig ticketConfig;
ticketConfig.setPrimaryIdColumnName("ticket_id");
ticketConfig.setTranslatedTableName(tr("Tickets"));
ticketConfig.setModelTableName("TicketsModel");
ticketConfig.setRepositoryTableName("TicketsRepository");
databaseTableNameToConfigMap_.insert("tickets", ticketConfig);
RelationIdConfig userConfig;
userConfig.setPrimaryIdColumnName("user_id");
userConfig.setTranslatedTableName(tr("Users"));
userConfig.setRepositoryTableName("UsersRepository");
databaseTableNameToConfigMap_.insert("users", userConfig);
RelationIdConfig roleConfig;
roleConfig.setPrimaryIdColumnName("role_id");
roleConfig.setTranslatedTableName(tr("Roles"));
roleConfig.setRepositoryTableName("RolesRepository");
databaseTableNameToConfigMap_.insert("roles", roleConfig);
RelationIdConfig tableInfoConfig;
tableInfoConfig.setTranslatedTableName(tr("Sessions statistics"));
tableInfoConfig.setRepositoryTableName("SessionsStatisticsRepository");
databaseTableNameToConfigMap_.insert("sessions_statistics", tableInfoConfig);
qInfo() << "Schema metadata initialized successfully";
}
SchemaMetadata::SchemaMetadata() noexcept {
std::call_once(initFlag, &SchemaMetadata::initialize);
}
SchemaMetadata::SchemaMetadata(const SchemaMetadata &) {}
SchemaMetadata &SchemaMetadata::operator=(const SchemaMetadata &) {
return *this;
}
SchemaMetadata SchemaMetadata::defaultSchema() { return SchemaMetadata(); }
QString SchemaMetadata::modelTableName(const QString &databaseTableName) {
QString modelTableName = relationIdConfig(databaseTableName).modelTableName();
if (modelTableName.isEmpty()) {
qFatal() << "Model table name is empty: Database table name:"
<< databaseTableName;
}
return modelTableName;
}
QString SchemaMetadata::primaryIdColumnName(const QString &databaseTableName) {
QString primaryIdColumnName =
relationIdConfig(databaseTableName).primaryIdColumnName();
if (primaryIdColumnName.isEmpty()) {
qFatal() << "Primary id column name is empty: Database table name:"
<< databaseTableName;
}
return primaryIdColumnName;
}
QString SchemaMetadata::repositoryTableName(const QString &databaseTableName) {
QString repositoryTableName =
relationIdConfig(databaseTableName).repositoryTableName();
if (repositoryTableName.isEmpty()) {
qFatal() << "Repository table name is empty: Database table name:"
<< databaseTableName;
}
return repositoryTableName;
}
QString SchemaMetadata::translatedTableName(const QString &databaseTableName) {
QString translatedTableName =
relationIdConfig(databaseTableName).translatedTableName();
if (translatedTableName.isEmpty()) {
qFatal() << "Translated table name is empty: Database table name:"
<< databaseTableName;
}
return translatedTableName;
}
RelationIdConfig
SchemaMetadata::relationIdConfig(const QString &databaseTableName) {
RelationIdConfig config =
databaseTableNameToConfigMap_.value(databaseTableName);
return config;
}
+40
View File
@@ -0,0 +1,40 @@
#ifndef SCHEMA_METADATA_H
#define SCHEMA_METADATA_H
#include "relation_id_config.h"
#include <QMap>
#include <QObject>
#include <mutex>
class SchemaMetadata : QObject {
Q_OBJECT
private:
static QMap<QString, RelationIdConfig> databaseTableNameToConfigMap_;
static std::once_flag initFlag;
public:
SchemaMetadata &operator=(const SchemaMetadata &);
SchemaMetadata(const SchemaMetadata &);
QString modelTableName(const QString &databaseTableName);
QString primaryIdColumnName(const QString &databaseTableName);
QString repositoryTableName(const QString &databaseTableName);
QString translatedTableName(const QString &databaseTableName);
RelationIdConfig relationIdConfig(const QString &databaseTableName);
inline auto asKeyValueRange();
static SchemaMetadata defaultSchema();
private:
SchemaMetadata() noexcept;
static void initialize();
};
auto SchemaMetadata::asKeyValueRange() {
return databaseTableNameToConfigMap_.asKeyValueRange();
}
#endif // SCHEMA_METADATA_H
+254
View File
@@ -0,0 +1,254 @@
#include "session_generator.h"
#include "create_session_request.h"
#include "hall_dto.h"
#include "halls_repository.h"
#include "movie_dto.h"
#include "movies_repository.h"
#include "repository_manager.h"
#include "repository_result.h"
#include "schema_metadata.h"
#include "sessions_repository.h"
SessionGenerator::SessionGenerator(
const ReservationManager &reservationReference)
: BaseGenerator("Session"), reservationReference_(reservationReference) {
initializeRepositories(); // First
qint32 extraTime = 45; // Extra time for cleaning the room
initializeMovieIdToDurationMap(extraTime); // After initializing repositories
initializeBounds(); // After initializing movieIdToDuration
initializeDayManager(); // After initializing bounds
initializeHallIdToReservationManagerMap(); // After initializing dayManager
}
void SessionGenerator::initializeRepositories() {
RepositoryManager &repositoryManager = RepositoryManager::instance();
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
moviesRepository_ = repositoryManager.getRepositoryAs<MoviesRepository>(
metadata.repositoryTableName("movies"));
hallsRepository_ = repositoryManager.getRepositoryAs<HallsRepository>(
metadata.repositoryTableName("halls"));
sessionsRepository_ = repositoryManager.getRepositoryAs<SessionsRepository>(
metadata.repositoryTableName("sessions"));
if (!bool(moviesRepository_ && hallsRepository_ && sessionsRepository_)) {
qFatal() << "Failed to initialize repositories";
}
}
void SessionGenerator::initializeMovieIdToDurationMap(
qint32 additionalMinutes) {
QVector<MovieDTO> movies;
RepositoryResult result = moviesRepository_->getAllMovies(movies);
if (!result.success) {
qFatal() << "Failed to retrieve movies";
}
for (const MovieDTO &movie : movies) {
movieIdToDurationMinutes_[movie.movieId()] =
movie.duration() + additionalMinutes;
}
if (movieIdToDurationMinutes_.isEmpty()) {
qFatal() << "Failed to initialize movieIdToDuration";
}
qInfo() << "Number of movies:" << movieIdToDurationMinutes_.size();
}
void SessionGenerator::initializeBounds() {
qint32 lowerBoundReservationMin = *std::min_element(
movieIdToDurationMinutes_.cbegin(), movieIdToDurationMinutes_.cend());
lowerBoundReservationMs_ = lowerBoundReservationMin * 60'000; // 60 * 1000
qint32 upperBoundReservationMin = *std::max_element(
movieIdToDurationMinutes_.cbegin(), movieIdToDurationMinutes_.cend());
upperBoundReservationMs_ = upperBoundReservationMin * 60'000; // 60 * 1000
qInfo() << "Lower bound reservation in Ms:" << lowerBoundReservationMs_;
qInfo() << "Upper bound reservation in Ms:" << upperBoundReservationMs_;
}
void SessionGenerator::initializeDayManager() {
QTime minDuration = QTime::fromMSecsSinceStartOfDay(lowerBoundReservationMs_);
DayManager dayManager(reservationReference_.reference());
dayManager.setMinDuration(minDuration);
reservationReference_.setReference(dayManager);
if (!reservationReference_.reference().minDuration().isValid()) {
qFatal() << "Failed to initialize dayManager";
}
qInfo() << "Day manager: Min duration:"
<< reservationReference_.reference().minDuration();
qInfo() << "Day manager: Slot count:"
<< reservationReference_.reference().slotCount();
}
void SessionGenerator::initializeHallIdToReservationManagerMap() {
QVector<HallDTO> halls;
RepositoryResult result = hallsRepository_->getAllHalls(halls);
if (!result.success) {
qFatal() << "Failed to retrieve halls";
}
for (const HallDTO &hall : halls) {
hallIdToReservationManager_[hall.hallId()] = reservationReference_;
}
if (hallIdToReservationManager_.isEmpty()) {
qFatal() << "Failed to initialize hallIdToReservationManager";
}
qInfo() << "Number of halls:" << hallIdToReservationManager_.size();
}
RepositoryResult SessionGenerator::createRecord() {
CreateSessionRequest request;
request.setBeginAt(reservationContext_.beginAt);
request.setHallId(reservationContext_.hallId);
request.setMovieId(reservationContext_.movieId);
request.setTicketPrice(reservationContext_.ticketPrice);
RepositoryResult result = sessionsRepository_->createSession(request);
return result;
}
QString
SessionGenerator::createFailureMessage(const QString &errorMessage) const {
QString additionalInfo =
QString("Additional info: Hall id: %1 Begin at: %2")
.arg(reservationContext_.hallId)
.arg(reservationContext_.beginAt.toString("yyyy-MM-dd hh:mm:ss"));
QString failureMessage = "Failure:\n\t%1\n\tError message: %2";
return failureMessage.arg(additionalInfo).arg(errorMessage);
}
QString SessionGenerator::createSuccessMessage() const {
qint32 hallId = reservationContext_.hallId;
const ReservationManager &manager = hallIdToReservationManager_.value(hallId);
QTime duration = calculateDuration(reservationContext_);
// return QString("Success: TAD: %1 TAS: %2 EId: %3 AD: %4 AS: %5 CR: %6 B: %7
// D: %8")
return QString("Success: TAD: %1 TAS: %2 EId: %3 AD: %4 AS: %5 CR: %6")
.arg(calculateAvailableDaysCount()) // Total available days
.arg(calculateAvailableSlotsCount()) // Total available slots
.arg(hallId) // Entity id
.arg(manager.calculateAvailableDaysCount()) // Available days for the
// current hall
.arg(manager.calculateAvailableSlotsCount()) // Available slots for the
// current hall
.arg(manager.createdRecords()); // Created records for the
// current hall
// .arg(reservationContext_.beginAt.toString( //
// "yyyy-MM-dd hh:mm:ss")) // Begin
// // of created record
// .arg(duration.toString("hh:mm:ss")); // Duration of created record
}
qsizetype SessionGenerator::calculateRemainingCapacity() const {
// Maximum number of sessions that can be created
return std::accumulate(hallIdToReservationManager_.cbegin(),
hallIdToReservationManager_.cend(), 0,
[&](qsizetype sum, const ReservationManager &manager) {
return sum + manager.calculatePossibleReservations(
lowerBoundReservationMs_);
});
};
bool SessionGenerator::prepareData() {
std::random_device rd;
std::mt19937 g{rd()};
QList<qint32> hallIds = hallIdToReservationManager_.keys();
std::shuffle(hallIds.begin(), hallIds.end(), g);
QList<qint32> movieIds = movieIdToDurationMinutes_.keys();
std::shuffle(movieIds.begin(), movieIds.end(), g);
for (qint32 hallId : hallIds) {
if (prepareData(hallId, movieIds)) {
return true;
}
}
qWarning() << "No available halls";
return false;
}
bool SessionGenerator::prepareData(qint32 hallId,
const QList<qint32> &movieIds) {
for (qint32 movieId : movieIds) {
ReservationManager &manager = hallIdToReservationManager_[hallId];
auto [beginDateTime, endTime] = manager.reserve(duration(movieId));
if (!beginDateTime.isValid() || !endTime.isValid()) {
continue;
}
const QDate epochDate = QDateTime::fromMSecsSinceEpoch(0).date();
const qint64 deltaDays = epochDate.daysTo(QDate(2025, 1, 1));
const QDate newBeginDate = beginDateTime.date().addDays(deltaDays);
const QTime beginTime = beginDateTime.time();
reservationContext_ = ReservationContext{
QDateTime(std::move(newBeginDate), std::move(beginTime)),
std::move(endTime),
bounded(10'000, 50'000) / 100.0,
hallId,
movieId,
};
return true;
}
qCritical()
<< "SessionGenerator::prepareData: No available movies for hall with id"
<< hallId;
return false;
}
QTime SessionGenerator::duration(qint32 movieId) const {
qint32 durationMin = movieIdToDurationMinutes_[movieId];
qint64 durationMs = durationMin * 60'000; // 60 * 1000
QTime durationTime = QTime::fromMSecsSinceStartOfDay(durationMs);
return durationTime;
}
qsizetype SessionGenerator::calculateAvailableDaysCount() const {
return std::accumulate(hallIdToReservationManager_.cbegin(),
hallIdToReservationManager_.cend(), 0,
[&](int sum, const ReservationManager &manager) {
return sum + manager.calculateAvailableDaysCount();
});
}
qsizetype SessionGenerator::calculateAvailableSlotsCount() const {
return std::accumulate(hallIdToReservationManager_.cbegin(),
hallIdToReservationManager_.cend(), 0,
[&](int sum, const ReservationManager &manager) {
return sum + manager.calculateAvailableSlotsCount();
});
}
QTime SessionGenerator::calculateDuration(ReservationContext context) const {
QTime beginTime = context.beginAt.time();
QTime endTime = context.endTime;
qint64 durationMs = beginTime.msecsTo(endTime);
return QTime::fromMSecsSinceStartOfDay(durationMs);
}
void SessionGenerator::clearStaleData() {
qint64 hallId = reservationContext_.hallId;
if (hallIdToReservationManager_[hallId].calculateAvailableDaysCount() == 0) {
qWarning() << "No available days for hall" << hallId;
hallIdToReservationManager_.remove(hallId);
}
}
+72
View File
@@ -0,0 +1,72 @@
#ifndef SESSION_GENERATOR_H
#define SESSION_GENERATOR_H
#include "base_generator.h"
#include "reservation_manager.h"
#include <QDateTime>
#include <QMetaType>
#include <QRandomGenerator>
#include <QString>
#include <QVector>
// Forward declaration
class SessionsRepositoryInterface;
class HallsRepositoryInterface;
class MoviesRepositoryInterface;
class SessionGenerator : public BaseGenerator {
private:
struct ReservationContext {
QDateTime beginAt;
QTime endTime;
double ticketPrice;
qint32 hallId;
qint32 movieId;
};
private:
QMap<qint32 /* movieId */, qint32 /* minutes */> movieIdToDurationMinutes_;
QMap<qint32 /* hallId */, ReservationManager> hallIdToReservationManager_;
ReservationContext reservationContext_;
ReservationManager reservationReference_;
qint64 lowerBoundReservationMs_;
qint64 upperBoundReservationMs_;
// Repositories
HallsRepositoryInterface *hallsRepository_;
MoviesRepositoryInterface *moviesRepository_;
SessionsRepositoryInterface *sessionsRepository_;
public:
SessionGenerator(const ReservationManager &reservationReference = {});
// Non-virtual methods
private:
QTime calculateDuration(ReservationContext context) const;
QTime duration(qint32 movieId) const;
bool prepareData(qint32 hallId, const QList<qint32> &movieIds);
qsizetype calculateAvailableDaysCount() const;
qsizetype calculateAvailableSlotsCount() const;
void initializeBounds();
void initializeDayManager();
void initializeHallIdToReservationManagerMap();
void initializeMovieIdToDurationMap(qint32 additionalMinutes);
void initializeRepositories();
// Vitual methods
private: // Const methods
QString createFailureMessage(const QString &errorMessage) const override;
QString createSuccessMessage() const override;
qsizetype calculateRemainingCapacity() const override;
private: // Non-const methods
RepositoryResult createRecord() override;
bool prepareData() override;
void clearStaleData() override;
private:
friend class SessionGeneratorTest;
};
#endif // SESSION_GENERATOR_H
+219
View File
@@ -0,0 +1,219 @@
#include "ticket_generator.h"
#include "create_ticket_request.h"
#include "hall_dto.h"
#include "halls_repository.h"
#include "repository_manager.h"
#include "repository_result.h"
#include "schema_metadata.h"
#include "session_dto.h"
#include "sessions_repository.h"
#include "tickets_repository.h"
#include <QDebug>
#include <QStack>
#include <numeric>
#include "repository_result.h"
std::random_device TicketGenerator::rd_;
std::mt19937 TicketGenerator::generator_(rd_());
TicketGenerator::TicketGenerator(
const QPair<qint32, qint32> &dayRange, const QPair<QTime, QTime> &timeRange,
const QPair<double, double> &fillRatioPerSessionRange)
: BaseGenerator("Ticket"), DateTimeGenerator(dayRange, timeRange) {
if (!setFillRatioPerSessionRange(fillRatioPerSessionRange)) {
qFatal() << "Failed to set fill ratio per session range";
}
initializeRepositories();
initializeSessionIdToAvailableSeatMap(); // After initializing repositories
initializeSessionIdToBeginAtMap(); // After initializing repositories
initializeRemainingCapacity(); // After initializing sessionToAvailableSeats
qInfo() << "Number of sessions:" << sessionIdToAvailableSeatNumbers_.size();
}
void TicketGenerator::initializeRepositories() {
RepositoryManager &repositoryManager = RepositoryManager::instance();
SchemaMetadata metadata = SchemaMetadata::defaultSchema();
hallsRepository_ = repositoryManager.getRepositoryAs<HallsRepository>(
metadata.repositoryTableName("halls"));
sessionsRepository_ = repositoryManager.getRepositoryAs<SessionsRepository>(
metadata.repositoryTableName("sessions"));
ticketsRepository_ = repositoryManager.getRepositoryAs<TicketsRepository>(
metadata.repositoryTableName("tickets"));
if (!bool(ticketsRepository_ && sessionsRepository_)) {
qFatal() << "Failed to initialize repositories";
}
}
void TicketGenerator::initializeSessionIdToAvailableSeatMap() {
QVector<HallDTO> halls;
RepositoryResult resultGetAllHalls = hallsRepository_->getAllHalls(halls);
if (!resultGetAllHalls.success) {
qFatal() << "Failed to retrieve halls";
}
QVector<SessionDTO> sessions;
RepositoryResult resultGetAllSessions =
sessionsRepository_->getAllSessions(sessions);
if (!resultGetAllSessions.success) {
qFatal() << "Failed to retrieve sessions";
}
QMap<qint32 /* sessionId */, QSet<qint32> /* availableSeatNumbers */>
hallIdToAvailableSeatNumbers;
for (const HallDTO &hall : halls) {
qint32 targetCapacity =
std::ceil(hall.capacity() * generateFillRatioPerSession());
QVector<qint32> availableSeats(targetCapacity);
std::iota(availableSeats.begin(), availableSeats.end(), 1);
std::shuffle(availableSeats.begin(), availableSeats.end(), generator_);
QSet<qint32> buffer;
for (qint32 i = 0; i < targetCapacity; ++i) {
buffer.insert(availableSeats[i]);
}
hallIdToAvailableSeatNumbers[hall.hallId()] = std::move(buffer);
}
for (const SessionDTO &session : sessions) {
sessionIdToAvailableSeatNumbers_[session.sessionId()] =
hallIdToAvailableSeatNumbers[session.hallId()];
}
}
void TicketGenerator::initializeSessionIdToBeginAtMap() {
QVector<SessionDTO> sessions;
RepositoryResult resultGetAllSessions =
sessionsRepository_->getAllSessions(sessions);
if (!resultGetAllSessions.success) {
qFatal() << "Failed to retrieve sessions";
}
for (const SessionDTO &session : sessions) {
sessionIdToBeginAt_[session.sessionId()] = session.beginAt();
}
}
bool TicketGenerator::setFillRatioPerSessionRange(
const QPair<double, double> &range) {
const auto &[minValue, maxValue] = range;
if (minValue > maxValue) {
return false;
}
auto &[minFillRatioPerSession, maxFillRatioPerSession] =
fillRatioPerSessionRange_;
minFillRatioPerSession = std::max(minValue, 0.0);
maxFillRatioPerSession = std::min(maxValue, 1.0);
return true;
}
double TicketGenerator::generateFillRatioPerSession() const {
const auto &[min, max] = fillRatioPerSessionRange_;
std::uniform_real_distribution<double> distribution(min, max);
return distribution(generator_);
}
void TicketGenerator::initializeRemainingCapacity() {
remainingCapacity_ =
std::accumulate(sessionIdToAvailableSeatNumbers_.cbegin(),
sessionIdToAvailableSeatNumbers_.cend(), 0,
[&](qsizetype sum, const QSet<qint32> &availableSeats) {
return sum + availableSeats.size();
});
}
QString
TicketGenerator::createFailureMessage(const QString &errorMessage) const {
QString additionalInfo =
QString("Additional info: Session id: %1 Seat number: %2 Sold at: %3 "
"Begin at: %4")
.arg(reservationContext_.sessionId)
.arg(reservationContext_.seatNumber)
.arg(reservationContext_.soldAt.toString("yyyy-MM-dd hh:mm:ss"))
.arg(reservationContext_.beginAt.toString("yyyy-MM-dd hh:mm:ss"));
QString failureMessage = "Failure:\n\t%1\n\tError message: %2";
return failureMessage.arg(additionalInfo).arg(errorMessage);
}
QString TicketGenerator::createSuccessMessage() const {
return QString("Success: EId: %1 SN: %2")
.arg(reservationContext_.sessionId)
.arg(reservationContext_.seatNumber);
}
qsizetype TicketGenerator::calculateRemainingCapacity() const {
return remainingCapacity_;
}
RepositoryResult TicketGenerator::createRecord() {
CreateTicketRequest request;
request.setSessionId(reservationContext_.sessionId);
request.setSeatNumber(reservationContext_.seatNumber);
request.setSoldAt(reservationContext_.soldAt);
RepositoryResult result = ticketsRepository_->createTicket(request);
if (result.success) {
--remainingCapacity_;
}
return result;
}
bool TicketGenerator::prepareData() {
if (remainingCapacity_ <= 0) {
qInfo() << "All tickets have been sold";
return false;
}
qsizetype deltaIndex =
bounded<qsizetype>(0, sessionIdToAvailableSeatNumbers_.size());
auto it = sessionIdToAvailableSeatNumbers_.begin();
std::advance(it, deltaIndex);
const qint32 sessionId = it.key();
QSet<qint32> &availableSeatNumbers = it.value(); // Not empty
const QDateTime &beginAt = sessionIdToBeginAt_[sessionId];
constexpr qint32 minus20MinutesMs = -20 * 1000 * 60;
// soldAt < beginAt || soldAt + 20 minute <= beginAt
QDateTime soldAt = generate(beginAt.addMSecs(minus20MinutesMs));
qsizetype seatNumberIndex =
bounded<qsizetype>(0, availableSeatNumbers.size());
auto seatNumberIt = availableSeatNumbers.cbegin();
std::advance(seatNumberIt, seatNumberIndex);
qint32 seatNumber = *seatNumberIt;
ReservationContext context{beginAt, soldAt, seatNumber, sessionId};
reservationContext_ = std::move(context);
return true;
}
void TicketGenerator::clearStaleData() {
qint32 sessionId = reservationContext_.sessionId;
qint32 seatNumber = reservationContext_.seatNumber;
QSet<qint32> &availableSeatNumbers =
sessionIdToAvailableSeatNumbers_[sessionId];
availableSeatNumbers.remove(seatNumber);
if (availableSeatNumbers.empty()) {
qWarning() << "All tickets for session with id" << sessionId
<< "have been sold";
sessionIdToAvailableSeatNumbers_.remove(sessionId);
sessionIdToBeginAt_.remove(sessionId);
}
}
+68
View File
@@ -0,0 +1,68 @@
#ifndef TICKET_GENERATOR_H
#define TICKET_GENERATOR_H
#include "base_generator.h"
#include "date_time_generator.h"
#include "tickets_repository_interface.h"
#include <QDateTime>
#include <QRandomGenerator>
#include <QString>
#include <QVector>
// Forward declaration
class HallsRepositoryInterface;
class SessionsRepositoryInterface;
class TicketsRepositoryInterface;
class TicketGenerator : public BaseGenerator, public DateTimeGenerator {
private:
struct ReservationContext {
QDateTime beginAt;
QDateTime soldAt;
qint32 seatNumber;
qint32 sessionId;
};
private:
static std::random_device rd_;
static std::mt19937 generator_;
private:
QMap<qint32 /* sessionId */, QDateTime> sessionIdToBeginAt_;
QMap<qint32 /* sessionId */, QSet<qint32> /* availableSeatNumbers */>
sessionIdToAvailableSeatNumbers_;
QPair<double, double> fillRatioPerSessionRange_;
ReservationContext reservationContext_;
qsizetype remainingCapacity_;
HallsRepositoryInterface *hallsRepository_;
SessionsRepositoryInterface *sessionsRepository_;
TicketsRepositoryInterface *ticketsRepository_;
public:
TicketGenerator(const QPair<qint32, qint32> &dayRange,
const QPair<QTime, QTime> &timeRange,
const QPair<double, double> &fillRatioPerSessionRange);
private:
void initializeRemainingCapacity();
void initializeRepositories();
void initializeSessionIdToAvailableSeatMap();
void initializeSessionIdToBeginAtMap();
bool setFillRatioPerSessionRange(const QPair<double, double> &range);
double generateFillRatioPerSession() const;
// Vitual methods
private: // Const methods
QString createFailureMessage(const QString &errorMessage) const override;
QString createSuccessMessage() const override;
qsizetype calculateRemainingCapacity() const override;
private: // Non-const methods
RepositoryResult createRecord() override;
bool prepareData() override;
void clearStaleData() override;
};
#endif // TICKET_GENERATOR_H