Files
shopship/apps/bizapps/tests.py
T

297 lines
12 KiB
Python
Raw Normal View History

2026-08-31 15:11:52 +03:30
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"])