changeset 899:563ef254bc3a

IN 32: Adds object verification for the construction of a ReconciledSurvey even after completing the survey. Adds customisation for the presentationText of a Money object, including if it should be locale aware.
author John Schneiderman <JohnMS@member.fsf.org>
date Mon, 03 Oct 2022 19:37:42 +0200
parents 97c104efbb5e
children 5c3c58aa9448
files src/file-storage/internal/XmlBudgetFile.cpp src/foundation/external/foundation/PresentationText.cpp src/foundation/external/foundation/PresentationText.h src/surveying/external/surveying/ReconciledSurvey.cpp src/surveying/external/surveying/ReconciledSurvey.h src/surveying/unit-tests/ReconciledSurvey-unit-tests.cpp
diffstat 6 files changed, 180 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- a/src/file-storage/internal/XmlBudgetFile.cpp	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/file-storage/internal/XmlBudgetFile.cpp	Mon Oct 03 19:37:42 2022 +0200
@@ -643,7 +643,7 @@
 					},
 					reconciliationElement.date_
 				},
-				{} // TODO: distribution amounts
+				{} // TODO: distribution amounts and validity value
 			}
 		);
 	}
--- a/src/foundation/external/foundation/PresentationText.cpp	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/foundation/external/foundation/PresentationText.cpp	Mon Oct 03 19:37:42 2022 +0200
@@ -43,14 +43,20 @@
 	return QString::fromStdString(toStdString(code));
 }
 
-QString drn::foundation::presentationText(const Money& money)
+QString drn::foundation::presentationText(
+	const Money& money,
+	const bool localeAware,
+	const bool withCurrency
+)
 {
 	const auto major{QString::number(money.major())};
 	const auto minor{QString::number(money.minor())};
 	const QLocale locale{};
-	return QString{"%1%2%3"}.arg(major)
-		.arg(locale.decimalPoint())
-		.arg(minor, minorUnitDigits(money.code()) + precision, '0');
+	return QString{"%1%2%3%4"}
+		.arg(major)
+		.arg(localeAware ? locale.decimalPoint() : QChar{'.'})
+		.arg(minor, minorUnitDigits(money.code()) + precision, '0')
+		.arg(withCurrency ? presentationText(money.code()) : QString{});
 }
 
 QString drn::foundation::presentationText(const QDate& value)
--- a/src/foundation/external/foundation/PresentationText.h	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/foundation/external/foundation/PresentationText.h	Mon Oct 03 19:37:42 2022 +0200
@@ -46,7 +46,11 @@
 {
 
 DRN_FOUNDATION_EXPORT ::QString presentationText(const pecunia::currency::Iso4217Codes& code);
-DRN_FOUNDATION_EXPORT ::QString presentationText(const pecunia::currency::Money& money);
+DRN_FOUNDATION_EXPORT ::QString presentationText(
+	const pecunia::currency::Money& money,
+	const bool localeAware = true, // TODO: strong type or leave as bool?
+	const bool withCurrency = false
+);
 DRN_FOUNDATION_EXPORT ::QString presentationText(const ::QDate& value);
 DRN_FOUNDATION_EXPORT ::QString presentationText(const ::qint8& value);
 DRN_FOUNDATION_EXPORT ::QString presentationText(const ::quint8& value);
--- a/src/surveying/external/surveying/ReconciledSurvey.cpp	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/surveying/external/surveying/ReconciledSurvey.cpp	Mon Oct 03 19:37:42 2022 +0200
@@ -19,18 +19,28 @@
 *******************************************************************************/
 #include "ReconciledSurvey.h"
 using pecunia::currency::Money;
+using ::QString;
 using std::map;
 using drn::banking::BankAccount;
 using drn::banking::presentationText;
 using drn::banking::ReconciledBankAccount;
 using drn::budgeting::BudgetItemIdentifier;
+using drn::foundation::Optional;
 using drn::surveying::ReconciledSurvey;
 using drn::surveying::ReconciledSurveys;
 
 #include <pecunia/Math.h>
 using pecunia::math::sum;
+#include <QCryptographicHash>
+using ::QCryptographicHash;
 #include <QDate>
 using ::QDate;
+#include <QObject>
+using ::QObject;
+#include <QStringBuilder>
+// using operator%
+#include <Qt>
+using Qt::DateFormat;
 
 #include <ostream>
 using std::ostream;
@@ -45,22 +55,57 @@
 using drn::banking::BankName;
 #include <banking/BankingErrors.h>
 using drn::banking::BankError;
-#include <foundation/Optional.hpp>
-using drn::foundation::Optional;
+#include <foundation/Error.h>
+using drn::foundation::Error;
 #include <foundation/PresentationText.h>
 using drn::foundation::presentationText;
 
 
+namespace
+{
+
+QString calculateValidity(
+	ReconciledBankAccount reconciled,
+	map<BudgetItemIdentifier, Money> distribution
+)
+{
+	QString value{presentationText(reconciled.bankAccount())};
+	value = value % presentationText(reconciled.balance(), false, true);
+
+	if (reconciled.reconciledOn().hasValue())
+		value = value % reconciled.reconciledOn()->toString(DateFormat::ISODate);
+
+	for (const auto& itemAmount : distribution)
+		value = value % presentationText(itemAmount.first)
+				% presentationText(itemAmount.second, false, true);
+	return QCryptographicHash::hash(value.toUtf8(), QCryptographicHash::Sha256).toBase64();
+}
+
+}
+
 //{ ReconciledSurvey
 
 ReconciledSurvey::ReconciledSurvey(
 	ReconciledBankAccount reconciled,
-	map<BudgetItemIdentifier, Money> distribution
+	map<BudgetItemIdentifier, Money> distribution,
+	Optional<QString> validity
 ) :
 	reconciled_{move(reconciled)},
-	distribution_{move(distribution)}
+	distribution_{move(distribution)},
+	validity_{move(validity)}
 {
-	// TODO: perform expected verifications. Note that distribution may not always be exactly equal due to currency exchange rate fluctuations, so use a has of the values to verify everything was correct at the time it was created.
+	if (this->validity_.hasValue())
+	{
+		const auto validityCheck{calculateValidity(this->reconciled_, this->distribution_)};
+
+		if (this->validity_ != validityCheck)
+			throw Error{
+				QObject::tr(
+					"The supplied values for a reconciled account does not match the expected "
+						"validity check value, %1."
+				).arg(validityCheck)
+			};
+	}
 }
 
 const ReconciledBankAccount& ReconciledSurvey::reconciled() const noexcept
@@ -68,12 +113,16 @@
 	return this->reconciled_;
 }
 
-const map<BudgetItemIdentifier, Money>& ReconciledSurvey::distribution(
-) const noexcept
+const map<BudgetItemIdentifier, Money>& ReconciledSurvey::distribution() const noexcept
 {
 	return this->distribution_;
 }
 
+const Optional<QString>& ReconciledSurvey::validity() const noexcept
+{
+	return this->validity_;
+}
+
 bool drn::surveying::operator==(const ReconciledSurvey& lhs, const ReconciledSurvey& rhs)
 {
 	return tie(lhs.reconciled(), lhs.distribution()) == tie(rhs.reconciled(), rhs.distribution());
@@ -151,6 +200,7 @@
 				.arg(presentationText(reconciledBalance))
 				.arg(presentationText(expectedBalance))
 		};
+	// TODO: survey.verification_ = calculateVerification();
 
 	if (this->count(ba) == 0)
 	{
--- a/src/surveying/external/surveying/ReconciledSurvey.h	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/surveying/external/surveying/ReconciledSurvey.h	Mon Oct 03 19:37:42 2022 +0200
@@ -21,6 +21,7 @@
 #define DRN_SURVEYING_RECONCILEDSURVEY_H_
 
 #include <pecunia/Money.h>
+#include <QString>
 
 #include <iosfwd>
 #include <map>
@@ -28,6 +29,7 @@
 #include <budgeting/BudgetItemIdentifier.h>
 #include <banking/BankAccount.h>
 #include <banking/Reconciliation.h>
+#include <foundation/Optional.hpp>
 
 #include "surveying_export.h"
 
@@ -42,38 +44,46 @@
 class AccountNumber;
 
 }
-namespace foundation
-{
-
-template<typename>
-class Optional;
-
-}
 namespace surveying
 {
 
 class DRN_SURVEYING_EXPORT ReconciledSurvey
 {
 	banking::ReconciledBankAccount reconciled_;
-	std::map<
-		budgeting::BudgetItemIdentifier,
-		pecunia::currency::Money
-	> distribution_;
+	std::map<budgeting::BudgetItemIdentifier, pecunia::currency::Money> distribution_;
+	foundation::Optional<::QString> validity_;
 
 public:
 	ReconciledSurvey() = default;
+	/**
+	 * @brief Full and partial initialisation constructor.
+	 *
+	 * @exception Error When a supplied validity value does not match the calculated value.
+	 *
+	 * @param reconciled The bank account that was reconciled.
+	 * @param distribution The distribution of the balance in the account.
+	 * @param validity When supplied, the value that represents an object whose values were verified
+	 * after having completed a survey. This is in lieu of verifying the balances upon object
+	 * creation since exchange rates change over time. If not supplied, no verification is
+	 * performed, i.e. the object is partially initialised.
+	 */
 	ReconciledSurvey(
 		banking::ReconciledBankAccount reconciled,
 		std::map<
 			budgeting::BudgetItemIdentifier,
 			pecunia::currency::Money
-		> distribution
+		> distribution,
+		foundation::Optional<::QString> validity = {}
 	);
 	const banking::ReconciledBankAccount& reconciled() const noexcept;
 	const std::map<
 		budgeting::BudgetItemIdentifier,
 		pecunia::currency::Money
 	>& distribution() const noexcept;
+	/**
+	 * @brief The value that represents a valid object after having completed a survey.
+	 */
+	const foundation::Optional<::QString>& validity() const noexcept;
 };
 
 class DRN_SURVEYING_EXPORT ReconciledSurveys : std::map<banking::BankAccount, ReconciledSurvey>
--- a/src/surveying/unit-tests/ReconciledSurvey-unit-tests.cpp	Mon Oct 03 17:52:12 2022 +0200
+++ b/src/surveying/unit-tests/ReconciledSurvey-unit-tests.cpp	Mon Oct 03 19:37:42 2022 +0200
@@ -37,10 +37,30 @@
 #include <map>
 using std::map;
 
+#include <accounting/AccountCode.h>
+using drn::accounting::AccountCode;
+#include <accounting/AccountNumber.h>
+using drn::accounting::AccountNumber;
+#include <banking/BankAccount.h>
+using drn::banking::BankAccount;
+#include <banking/BankAccountType.h>
+using drn::banking::BankAccountType;
+using drn::banking::SupportedAccountTypes;
+#include <banking/BankName.h>
+using drn::banking::BankName;
 #include <banking/Reconciliation.h>
 using drn::banking::ReconciledBankAccount;
 #include <budgeting/BudgetItemIdentifier.h>
 using drn::budgeting::BudgetItemIdentifier;
+#include <budgeting/BudgetItemTypes.h>
+using drn::budgeting::BudgetItemTypes;
+#include <budgeting/BudgetSource.h>
+using drn::budgeting::BudgetSource;
+#include <foundation/Error.h>
+using drn::foundation::Error;
+#include <foundation/Optional.hpp>
+using drn::foundation::inPlace;
+using drn::foundation::Optional;
 #include <surveying/ReconciledSurvey.h>
 using drn::surveying::ReconciledSurvey;
 
@@ -56,12 +76,77 @@
 {
 	Q_OBJECT
 
+	const map<BudgetItemIdentifier, Money> distribution_{
+		{
+			{
+				BudgetItemIdentifier{BudgetItemTypes::Bill, BudgetSource{"Web Hosting"}},
+				Money{50, 1234, Iso4217Codes::USD}
+			}
+		}
+	};
+	const ReconciledBankAccount reconciled_{
+		BankAccount{
+			BankName{"C1st Credit Union"},
+			BankAccountType{
+				AccountCode{AccountNumber{4321}, "Primary Share"},
+				SupportedAccountTypes::Savings
+			}
+		},
+		Money{50, 1234, Iso4217Codes::USD},
+		Optional<QDate>{inPlace, 2022, 10, 3}
+	};
+	const Optional<QString> validity_{
+		inPlace,
+		QStringLiteral("MxlBAZPxUL+Ken7+CdjrmJhKe37ejHWG3USj5PBeCWw=")
+	};
+
 private slots:
 	void constructor_DefaultInit_ShouldSet()
 	{
 		ReconciledSurvey survey{};
 		QVERIFY_THAT(survey.distribution(), equals(map<BudgetItemIdentifier, Money>{}));
 		QVERIFY_THAT(survey.reconciled(), equals(ReconciledBankAccount{}));
+		QVERIFY_FALSE(survey.validity().hasValue());
+	}
+
+	void constructor_PartialInit_ShouldSet()
+	{
+		const ReconciledSurvey survey{this->reconciled_, this->distribution_};
+		QVERIFY_THAT(survey.distribution(), equals(this->distribution_));
+		QVERIFY_THAT(survey.reconciled(), equals(this->reconciled_));
+		QVERIFY_FALSE(survey.validity().hasValue());
+	}
+
+	void constructor_FullInit_ShouldSet()
+	{
+		const ReconciledSurvey survey{this->reconciled_, this->distribution_, this->validity_};
+		QVERIFY_THAT(survey.distribution(), equals(this->distribution_));
+		QVERIFY_THAT(survey.reconciled(), equals(this->reconciled_));
+		QVERIFY_THAT(survey.validity(), equals(this->validity_));
+	}
+
+	void constructor_ReconciledMissmatchFromValidity_ShouldThrow()
+	{
+		QVERIFY_EXCEPTION_THROWN(
+			(ReconciledSurvey{{}, this->distribution_, this->validity_}),
+			Error
+		);
+	}
+
+	void constructor_DistributionMissmatchFromValidity_ShouldThrow()
+	{
+		QVERIFY_EXCEPTION_THROWN(
+			(ReconciledSurvey{this->reconciled_, {}, this->validity_}),
+			Error
+		);
+	}
+
+	void constructor_ValidityMissmatch_ShouldThrow()
+	{
+		QVERIFY_EXCEPTION_THROWN(
+			(ReconciledSurvey{this->reconciled_, this->distribution_, QStringLiteral("123456789")}),
+			Error
+		);
 	}
 };