AkomptaBackend / api /tests_syscohada.py
rinogeek's picture
fix: correct transaction dates (2024 clock drift); improve validate_date; add fix_dates command
833c388
Raw
History Blame Contribute Delete
6.71 kB
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from django.test import TestCase
from django.utils import timezone
from .models import (
Transaction,
User,
SyscohadaCRMappingRule,
SyscohadaBilanBalance,
)
from .syscohada_reports import compute_compte_resultat, generate_bilan_csv, generate_compte_resultat_csv
from .serializers import TransactionSerializer
class SyscohadaComputationTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email="syscohada@test.com",
password="pass12345",
first_name="Sys",
last_name="Coha",
account_type="personal",
initial_balance=Decimal("1000.00"),
agreed_terms=True,
)
def _dt(self, year: int, month: int, day: int):
return timezone.make_aware(datetime(year, month, day, 12, 0, 0))
def test_compte_resultat_marks_unmapped_transactions(self):
# Year N
tx_income = Transaction.objects.create(
user=self.user,
name="Vente marchandises",
amount=Decimal("5000.00"),
type="income",
category="Ventes",
date=self._dt(2026, 2, 2),
currency="XOF",
)
tx_transport = Transaction.objects.create(
user=self.user,
name="Taxi",
amount=Decimal("200.00"),
type="expense",
category="Transport",
date=self._dt(2026, 2, 3),
currency="XOF",
)
tx_unknown = Transaction.objects.create(
user=self.user,
name="Dépense inconnue",
amount=Decimal("50.00"),
type="expense",
category="Inconnu",
date=self._dt(2026, 2, 4),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
# TA should include the income (default mapping sees "ventes")
self.assertGreaterEqual(compte.values_n.get("TA", Decimal("0")), Decimal("5000.00"))
# RG should include transport
self.assertGreaterEqual(compte.values_n.get("RG", Decimal("0")), Decimal("200.00"))
# Unknown should be marked as unmapped (and goes to RJ fallback)
self.assertIn(tx_unknown.id, compte.unmapped_tx_ids_n)
self.assertGreaterEqual(compte.values_n.get("RJ", Decimal("0")), Decimal("50.00"))
# If we add an explicit rule, it should stop being "unmapped"
SyscohadaCRMappingRule.objects.create(
user=self.user,
ref="RH",
tx_type="expense",
category_pattern="Inconnu",
match_mode="contains",
priority=1,
is_active=True,
)
compte2 = compute_compte_resultat(self.user, 2026)
self.assertNotIn(tx_unknown.id, compte2.unmapped_tx_ids_n)
self.assertGreaterEqual(compte2.values_n.get("RH", Decimal("0")), Decimal("50.00"))
def test_bilan_can_balance_with_user_inputs(self):
# Create a simple year with net profit
Transaction.objects.create(
user=self.user,
name="Vente",
amount=Decimal("1000.00"),
type="income",
category="Ventes",
date=self._dt(2026, 1, 10),
currency="XOF",
)
Transaction.objects.create(
user=self.user,
name="Transport",
amount=Decimal("100.00"),
type="expense",
category="Transport",
date=self._dt(2026, 1, 11),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
cash = self.user.initial_balance + compte.total_income_n - compte.total_expense_n
resultat = compte.resultat_net_n
# Provide capital so that: CA + CJ == BS (minimal bilan)
SyscohadaBilanBalance.objects.create(
user=self.user,
year=2026,
section="PASSIF",
ref="CA",
net=(cash - resultat),
note="Capital (test)",
)
bilan_csv = generate_bilan_csv(self.user, compte).decode("utf-8")
# Sanity: contains key refs
self.assertIn("ACTIF,BS", bilan_csv)
self.assertIn("PASSIF,CA", bilan_csv)
self.assertIn("PASSIF,CJ", bilan_csv)
def test_exports_generate_csv_bytes(self):
Transaction.objects.create(
user=self.user,
name="Vente",
amount=Decimal("100.00"),
type="income",
category="Ventes",
date=self._dt(2026, 3, 1),
currency="XOF",
)
compte = compute_compte_resultat(self.user, 2026)
cr_csv = generate_compte_resultat_csv(compte)
bilan_csv = generate_bilan_csv(self.user, compte)
self.assertIsInstance(cr_csv, (bytes, bytearray))
self.assertIsInstance(bilan_csv, (bytes, bytearray))
self.assertGreater(len(cr_csv), 50)
self.assertGreater(len(bilan_csv), 50)
def test_uses_created_at_when_transaction_date_is_way_off(self):
"""
If client sends a wrong `date` (ex: device clock in 2024) but the server-side
`created_at` is in 2026, SYSCOHADA calculations should include it in 2026.
"""
tx = Transaction.objects.create(
user=self.user,
name="Vente (bad date)",
amount=Decimal("100.00"),
type="income",
category="Ventes",
date=self._dt(2024, 3, 16), # wrong
currency="XOF",
)
# Force created_at to 2026 (simulate real server record time)
Transaction.objects.filter(id=tx.id).update(created_at=self._dt(2026, 5, 17))
compte = compute_compte_resultat(self.user, 2026)
self.assertGreaterEqual(compte.values_n.get("TA", Decimal("0")), Decimal("100.00"))
def test_transaction_serializer_clamps_bad_client_dates(self):
# Date far away (client clock wrong) should be clamped to "now" by default.
payload = {
"name": "Bad date tx",
"amount": "100.00",
"type": "income",
"category": "Ventes",
"currency": "XOF",
"date": self._dt(2024, 3, 16).isoformat().replace("+00:00", "Z"),
}
class _Req:
query_params = {}
user = self.user
serializer = TransactionSerializer(data=payload, context={"request": _Req()})
self.assertTrue(serializer.is_valid(), serializer.errors)
value = serializer.validated_data["date"]
now = timezone.now()
self.assertLessEqual(abs((value - now).days), 1)