Skip to content

Commit

Permalink
Tests now passing, after a bit of an sql-reactor
Browse files Browse the repository at this point in the history
  • Loading branch information
adamcharnock committed Jun 30, 2024
1 parent 027bc5b commit b590724
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 51 deletions.
9 changes: 2 additions & 7 deletions hordak/migrations/0048_hordak_transaction_view.mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,28 @@ SELECT
SELECT JSON_ARRAYAGG(account_id)
FROM hordak_leg
WHERE transaction_id = T.id AND amount > 0
GROUP BY account_id
) AS credit_account_ids,
(
SELECT JSON_ARRAYAGG(account_id)
FROM hordak_leg
WHERE transaction_id = T.id AND amount < 0
GROUP BY account_id
) AS debit_account_ids,
(
SELECT JSON_ARRAYAGG(name)
FROM hordak_leg JOIN hordak_account A ON A.id = hordak_leg.account_id
WHERE transaction_id = T.id AND amount > 0
GROUP BY name
) AS credit_account_names,
(
SELECT JSON_ARRAYAGG(name)
FROM hordak_leg JOIN hordak_account A ON A.id = hordak_leg.account_id
WHERE transaction_id = T.id AND amount < 0
GROUP BY name
) AS debit_account_names,
(
SELECT
-- TODO: MYSQL LIMITATION: Cannot handle amount calculation for multi-currency transactions
CASE WHEN COUNT(DISTINCT amount_currency) < 2 THEN JSON_OBJECT('amount', SUM(amount), 'currency', '???') END
CASE WHEN COUNT(DISTINCT amount_currency) < 2 THEN CONCAT('[', JSON_OBJECT('amount', SUM(amount), 'currency', amount_currency), ']') END
FROM hordak_leg
WHERE transaction_id = T.id
GROUP BY amount_currency
WHERE transaction_id = T.id AND amount > 0
) AS amount
FROM
hordak_transaction T
Expand Down
90 changes: 50 additions & 40 deletions hordak/migrations/0048_hordak_transaction_view.pg.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
------
create view hordak_transaction_view AS (SELECT
T.id
,JSONB_AGG(DISTINCT L_CR.account_id) as credit_account_ids
,JSONB_AGG(DISTINCT L_DR.account_id) as debit_account_ids
,JSONB_AGG(DISTINCT L_CR.name) as credit_account_names
,JSONB_AGG(DISTINCT L_DR.name) as debit_account_names
,JSONB_AGG(jsonb_build_object('amount', L.amount, 'currency', L.currency)) as amount
T.*,
-- Get ID and names of credited accounts
-- Note that this gets unique IDs and names. If there is a
-- way to implement this without DISTINCT then I would like that
-- as then we can be guaranteed to get back the same number
-- of account names and account IDs.
(
SELECT JSONB_AGG(L_CR.account_id)
FROM hordak_leg L_CR
INNER JOIN hordak_account A ON A.id = L_CR.account_id
WHERE L_CR.transaction_id = T.id AND L_CR.amount > 0
) AS credit_account_ids,
(
SELECT JSONB_AGG(L_DR.account_id)
FROM hordak_leg L_DR
INNER JOIN hordak_account A ON A.id = L_DR.account_id
WHERE L_DR.transaction_id = T.id AND L_DR.amount < 0
) AS debit_account_ids,
(
SELECT JSONB_AGG(A.name)
FROM hordak_leg L_CR
INNER JOIN hordak_account A ON A.id = L_CR.account_id
WHERE L_CR.transaction_id = T.id AND L_CR.amount > 0
) AS credit_account_names,
(
SELECT JSONB_AGG(A.name)
FROM hordak_leg L_DR
INNER JOIN hordak_account A ON A.id = L_DR.account_id
WHERE L_DR.transaction_id = T.id AND L_DR.amount < 0
) AS debit_account_names,
JSONB_AGG(jsonb_build_object('amount', L.amount, 'currency', L.currency)) as amount
FROM
hordak_transaction T
-- Get LEG amounts for each currency in the transaction
Expand All @@ -15,45 +40,30 @@ INNER JOIN LATERAL (
WHERE L.transaction_id = T.id AND L.amount > 0
GROUP BY amount_currency
) L ON True
-- Get ID and names of credited accounts
INNER JOIN LATERAL (
SELECT
account_id,
name
FROM hordak_leg
INNER JOIN hordak_account A on A.id = hordak_leg.account_id
WHERE transaction_id = T.id AND amount > 0
GROUP BY account_id, name
ORDER BY account_id
) L_CR ON True
-- Get ID and names of debited accounts
INNER JOIN LATERAL (
SELECT
account_id,
name
FROM hordak_leg
INNER JOIN hordak_account A on A.id = hordak_leg.account_id
WHERE transaction_id = T.id AND amount < 0
GROUP BY account_id, name
ORDER BY account_id
) L_DR ON True
GROUP BY T.id, T.uuid, T.timestamp, T.date, T.description
ORDER BY T.id DESC);
--- reverse:
drop view hordak_transaction_view;


SELECT SUM(amount) AS amount, amount_currency AS currency
FROM hordak_leg L
WHERE L.transaction_id = 1 AND L.amount > 0
GROUP BY amount_currency;


SELECT
account_id,
name
FROM hordak_leg
INNER JOIN hordak_account A on A.id = hordak_leg.account_id
WHERE transaction_id = 1 AND amount > 0
GROUP BY account_id, name
ORDER BY account_id;
SELECT JSONB_AGG(DISTINCT L_CR.account_id)
FROM hordak_leg L_CR
INNER JOIN hordak_account A ON A.id = L_CR.account_id
WHERE L_CR.transaction_id = 1 AND L_CR.amount > 0;

SELECT JSONB_AGG(DISTINCT L_DR.account_id)
FROM hordak_leg L_DR
INNER JOIN hordak_account A ON A.id = L_DR.account_id
WHERE L_DR.transaction_id = 1 AND L_DR.amount < 0;

SELECT JSONB_AGG(DISTINCT A.name)
FROM hordak_leg L_CR
INNER JOIN hordak_account A ON A.id = L_CR.account_id
WHERE L_CR.transaction_id = 1 AND L_CR.amount > 0;

SELECT JSONB_AGG( A.name)
FROM hordak_leg L_DR
INNER JOIN hordak_account A ON A.id = L_DR.account_id
WHERE L_DR.transaction_id = 1 AND L_DR.amount < 0;
91 changes: 91 additions & 0 deletions hordak/migrations/0049_transactionview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generated by Django 4.2 on 2024-06-30 11:57

from django.db import migrations, models
import django.db.models.deletion
import hordak.utilities.db


class Migration(migrations.Migration):
dependencies = [
("hordak", "0048_hordak_transaction_view"),
]

operations = [
migrations.CreateModel(
name="TransactionView",
fields=[
(
"parent",
models.OneToOneField(
db_column="id",
editable=False,
on_delete=django.db.models.deletion.DO_NOTHING,
primary_key=True,
related_name="view",
serialize=False,
to="hordak.transaction",
),
),
("uuid", models.UUIDField(editable=False, verbose_name="uuid")),
(
"timestamp",
models.DateTimeField(
editable=False,
help_text="The creation date of this transaction object",
verbose_name="timestamp",
),
),
(
"date",
models.DateField(
editable=False,
help_text="The date on which this transaction occurred",
verbose_name="date",
),
),
(
"description",
models.TextField(editable=False, verbose_name="description"),
),
(
"amount",
hordak.utilities.db.BalanceField(
editable=False,
help_text="The total amount transferred in this transaction",
),
),
(
"credit_account_ids",
models.JSONField(
editable=False,
help_text="List of account ids for the credit legs of this transaction",
),
),
(
"debit_account_ids",
models.JSONField(
editable=False,
help_text="List of account ids for the debit legs of this transaction",
),
),
(
"credit_account_names",
models.JSONField(
editable=False,
help_text="List of account names for the credit legs of this transaction",
),
),
(
"debit_account_names",
models.JSONField(
editable=False,
help_text="List of account names for the debit legs of this transaction",
),
),
],
options={
"db_table": "hordak_transaction_view",
"managed": False,
},
),
]
25 changes: 23 additions & 2 deletions hordak/tests/models/test_db_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hordak.models import Leg, LegView, Transaction, TransactionView
from hordak.tests.utils import DataProvider
from hordak.utilities.currency import Balance
from hordak.utilities.test import mysql_only, postgres_only


class LegViewTestCase(DataProvider, DbTransactionTestCase):
Expand Down Expand Up @@ -139,7 +140,8 @@ def test_amount_single_currency(self):
view: TransactionView = TransactionView.objects.get()
self.assertEqual(view.amount, Balance([Money(100, "USD")]))

def test_amount_multi_currency(self):
@postgres_only("Only postgres supports multi-currency account amounts")
def test_amount_multi_currency_postgres(self):
self.credit_account_eur = self.account(currencies=["EUR"], name="Credit EUR")
self.debit_account_eur = self.account(currencies=["EUR"], name="Debit EUR")

Expand All @@ -154,6 +156,25 @@ def test_amount_multi_currency(self):
account=self.debit_account_eur,
amount=Money(-90, "EUR"),
)

view: TransactionView = TransactionView.objects.get()
self.assertEqual(view.amount, Balance([Money(100, "USD"), Money(90, "EUR")]))

@mysql_only("No MySQL support for multi-currency account amounts")
def test_amount_multi_currency_mysql(self):
# Multi-currency transaction amounts only available in postgres
self.credit_account_eur = self.account(currencies=["EUR"], name="Credit EUR")
self.debit_account_eur = self.account(currencies=["EUR"], name="Debit EUR")

with db_transaction.atomic():
Leg.objects.create(
transaction=self.transaction,
account=self.credit_account_eur,
amount=Money(90, "EUR"),
)
Leg.objects.create(
transaction=self.transaction,
account=self.debit_account_eur,
amount=Money(-90, "EUR"),
)
view: TransactionView = TransactionView.objects.get()
self.assertEqual(view.amount, None)
2 changes: 0 additions & 2 deletions hordak/utilities/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def from_db_value(self, value, expression, connection):
if value is None:
return value
try:
breakpoint()
return json_to_balance(value)
except ValueError:
raise ValidationError("Invalid JSON format")
Expand Down Expand Up @@ -46,7 +45,6 @@ def get_prep_value(self, value):
def json_to_balance(json_: Union[str, List[dict]]) -> Balance:
if isinstance(json_, str):
json_ = json.loads(json_)

return Balance([Money(m["amount"], m["currency"]) for m in json_])


Expand Down
2 changes: 2 additions & 0 deletions hordak/utilities/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
def migration_operations_from_sql(file_path: Path):
operations = []
sql: str = file_path.read_text(encoding="utf8").strip().strip("-")
# Mysql needs to have spaces after a '--' comment
sql = sql.replace("-- ----", "------").replace("-- -", "---")
if not sql:
return []

Expand Down
19 changes: 19 additions & 0 deletions hordak/utilities/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from unittest import skip

from django.db import connection


def _id(obj):
return obj


def postgres_only(reason="Test is postgresql-specific"):
if not connection.vendor == "postgresql":
return skip(reason)
return _id


def mysql_only(reason="Test is postgresql-specific"):
if not connection.vendor == "mysql":
return skip(reason)
return _id

0 comments on commit b590724

Please sign in to comment.