File size: 6,711 Bytes
833c388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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)