Files
shopship/apps/bizapps/tests.py
T
2026-08-31 15:11:52 +03:30

297 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import uuid
from datetime import date
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from apps.account.models import User
from apps.bizapps.api.services import InvoiceService, LedgerService
from apps.bizapps.models import Customer, LedgerEntry, LedgerInstallment
# این ماژول لایه‌ی «هماهنگیِ عادی/حرفه‌ای»ِ دفتر درآمد و هزینه را پوشش می‌دهد —
# دقیقا همان بخشی که کاربر صریحا خواسته «تست‌نویسی برایش خیلی مهم است»، چون
# باگ اینجا یعنی مانده‌ی بدهیِ نمایش‌داده‌شده به کاربر با واقعیت فرق کند.
def _make_user(phone="09120000000") -> User:
return User.objects.create_user(phone=phone, is_active=True, state=User.State.ACTIVE)
class LedgerEntryModelTests(TestCase):
"""دسته‌بندیِ هر ردیف باید دقیقا به نوعِ حرفه‌ایِ درستش نگاشت شود — این
همان «هماهنگی» است که کاربر مثال زد (فروش قسطی = طلب، نه درآمدِ صرف)."""
def setUp(self):
self.user = _make_user()
def _entry(self, **kwargs):
defaults = dict(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="تست",
amount=100,
entry_date=date.today(),
)
defaults.update(kwargs)
return LedgerEntry.objects.create(**defaults)
def test_sale_credit_is_receivable_debt(self):
entry = self._entry(category=LedgerEntry.Category.SALE_CREDIT, total_amount=100)
self.assertTrue(entry.is_debt)
self.assertEqual(entry.professional_type, "receivable")
def test_purchase_credit_is_payable_debt(self):
entry = self._entry(
entry_type=LedgerEntry.EntryType.EXPENSE,
category=LedgerEntry.Category.PURCHASE_CREDIT,
total_amount=100,
)
self.assertTrue(entry.is_debt)
self.assertEqual(entry.professional_type, "payable")
def test_simple_categories_are_not_debt(self):
for category in [
LedgerEntry.Category.SALE_CASH,
LedgerEntry.Category.SERVICE_INCOME,
LedgerEntry.Category.OTHER_INCOME,
]:
entry = self._entry(category=category)
self.assertFalse(entry.is_debt, category)
self.assertEqual(entry.professional_type, "simple", category)
def test_remaining_amount_tracks_installments(self):
entry = self._entry(category=LedgerEntry.Category.SALE_CREDIT, total_amount=1000)
self.assertEqual(entry.remaining_amount, 1000)
LedgerInstallment.objects.create(
id=str(uuid.uuid4()), ledger_entry=entry, amount=400, paid_date=date.today()
)
entry.refresh_from_db()
self.assertEqual(entry.paid_amount, 400)
self.assertEqual(entry.remaining_amount, 600)
LedgerInstallment.objects.create(
id=str(uuid.uuid4()), ledger_entry=entry, amount=600, paid_date=date.today()
)
entry.refresh_from_db()
self.assertEqual(entry.remaining_amount, 0)
def test_remaining_amount_is_zero_for_non_debt_entries(self):
entry = self._entry(category=LedgerEntry.Category.SALE_CASH)
self.assertEqual(entry.remaining_amount, 0)
class LedgerServiceTests(TestCase):
def setUp(self):
self.user = _make_user()
self.customer = Customer.objects.create(
id=str(uuid.uuid4()), user=self.user, name="مشتری", phone="09121112222"
)
def test_recompute_balance_sums_open_receivables(self):
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="فاکتور ۱",
amount=500,
category=LedgerEntry.Category.SALE_CREDIT,
entry_date=date.today(),
total_amount=500,
counterparty_phone="09121112222",
)
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="فاکتور ۲",
amount=300,
category=LedgerEntry.Category.SALE_CREDIT,
entry_date=date.today(),
total_amount=300,
counterparty_phone="09121112222",
)
LedgerService.recompute_customer_balance(self.user, "09121112222")
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 800)
def test_balance_drops_as_installments_are_paid(self):
entry = LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="فاکتور",
amount=1000,
category=LedgerEntry.Category.SALE_CREDIT,
entry_date=date.today(),
total_amount=1000,
counterparty_phone="09121112222",
)
LedgerService.recompute_customer_balance(self.user, "09121112222")
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 1000)
LedgerInstallment.objects.create(
id=str(uuid.uuid4()), ledger_entry=entry, amount=1000, paid_date=date.today()
)
LedgerService.recompute_customer_balance(self.user, "09121112222")
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 0)
def test_ignores_other_users_entries(self):
other = _make_user(phone="09129998888")
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=other,
entry_type=LedgerEntry.EntryType.INCOME,
title="فاکتور مالِ یکی دیگه",
amount=999,
category=LedgerEntry.Category.SALE_CREDIT,
entry_date=date.today(),
total_amount=999,
counterparty_phone="09121112222",
)
LedgerService.recompute_customer_balance(self.user, "09121112222")
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 0)
def test_export_csv_filters_by_date_range(self):
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="داخل بازه",
amount=100,
category=LedgerEntry.Category.SALE_CASH,
entry_date=date(2026, 1, 15),
)
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.EXPENSE,
title="خارج از بازه",
amount=50,
category=LedgerEntry.Category.RENT,
entry_date=date(2026, 3, 1),
)
csv_text = LedgerService.export_csv(self.user, start=date(2026, 1, 1), end=date(2026, 1, 31))
self.assertIn("داخل بازه", csv_text)
self.assertNotIn("خارج از بازه", csv_text)
class InvoiceLedgerIntegrationTests(TestCase):
"""اتصالِ فاکتور <-> دفتر: طبق درخواست، فقط فروشِ قسطی باید خودکار یک
ردیفِ بدهی‌دار در دفتر بسازد؛ فروشِ نقدی نباید دفتر را شلوغ کند."""
def setUp(self):
self.user = _make_user()
self.customer = Customer.objects.create(
id=str(uuid.uuid4()), user=self.user, name="مشتری قسطی", phone="09123334444"
)
def _invoice_data(self, payment_type):
return {
"customer": self.customer,
"buyer_name": self.customer.name,
"buyer_phone": self.customer.phone,
"buyer_address": "",
"items": [
{"product": None, "description": "کالای آزاد", "quantity": 1, "unit_price": 100, "discount_percent": 0}
],
"payment_type": payment_type,
}
def test_credit_invoice_creates_linked_receivable_entry(self):
invoice = InvoiceService.create_invoice(self.user, self._invoice_data("credit"))
entry = LedgerEntry.objects.get(linked_invoice=invoice)
self.assertEqual(entry.category, LedgerEntry.Category.SALE_CREDIT)
self.assertEqual(entry.total_amount, invoice.final_amount)
self.assertEqual(entry.remaining_amount, invoice.final_amount)
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, invoice.final_amount)
def test_cash_invoice_does_not_create_ledger_entry(self):
invoice = InvoiceService.create_invoice(self.user, self._invoice_data("cash"))
self.assertFalse(LedgerEntry.objects.filter(linked_invoice=invoice).exists())
def test_marking_invoice_paid_settles_the_linked_entry(self):
invoice = InvoiceService.create_invoice(self.user, self._invoice_data("credit"))
InvoiceService.update_status(self.user, invoice.id, payment_status="paid")
entry = LedgerEntry.objects.get(linked_invoice=invoice)
self.assertEqual(entry.remaining_amount, 0)
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 0)
def test_cancelling_unpaid_credit_invoice_removes_the_entry(self):
invoice = InvoiceService.create_invoice(self.user, self._invoice_data("credit"))
InvoiceService.cancel_invoice(self.user, invoice.id)
self.assertFalse(LedgerEntry.objects.filter(linked_invoice=invoice).exists())
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 0)
def test_cancelling_partially_paid_credit_invoice_keeps_the_entry(self):
invoice = InvoiceService.create_invoice(self.user, self._invoice_data("credit"))
entry = LedgerEntry.objects.get(linked_invoice=invoice)
LedgerInstallment.objects.create(
id=str(uuid.uuid4()), ledger_entry=entry, amount=50, paid_date=date.today()
)
invoice.order_status = invoice.OrderStatus.PENDING_PAYMENT
invoice.save(update_fields=["order_status"])
InvoiceService.cancel_invoice(self.user, invoice.id)
# قسطی که واقعا پرداخت شده نباید محو شود — تاریخچه‌ی مالی دست‌نخورده می‌ماند.
self.assertTrue(LedgerEntry.objects.filter(linked_invoice=invoice).exists())
class LedgerApiTests(TestCase):
"""چند نقطه‌ی حیاتیِ سطح API — اعتبارسنجی اضافه‌شدنِ قسطِ بیش از حد،
و این‌که خروجیِ CSV واقعا با content-type درست برمی‌گردد."""
def setUp(self):
self.user = _make_user()
self.client = APIClient()
token = str(RefreshToken.for_user(self.user).access_token)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")
def test_create_debt_entry_without_total_amount_is_rejected(self):
response = self.client.post(
reverse("bizapps:ledger-list"),
{
"entry_type": "income",
"title": "فروش قسطی بدون مبلغ کل",
"amount": 100,
"category": "sale_credit",
"entry_date": date.today().isoformat(),
},
format="json",
)
self.assertEqual(response.status_code, 400)
def test_installment_exceeding_remaining_amount_is_rejected(self):
entry = LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
entry_type=LedgerEntry.EntryType.INCOME,
title="فروش قسطی",
amount=1000,
category=LedgerEntry.Category.SALE_CREDIT,
entry_date=date.today(),
total_amount=1000,
)
response = self.client.post(
reverse("bizapps:ledger-installment-list", kwargs={"entry_id": entry.id}),
{"amount": 5000, "paid_date": date.today().isoformat()},
format="json",
)
self.assertEqual(response.status_code, 400)
def test_export_returns_csv(self):
response = self.client.get(reverse("bizapps:ledger-export"))
self.assertEqual(response.status_code, 200)
self.assertIn("text/csv", response["Content-Type"])