Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/tests/test_legacy_import.py

379 lines
14 KiB
Python

"""Tests for legacy Hesabix v1 API import helpers."""
from __future__ import annotations
import json
import zipfile
from io import BytesIO
import pytest
from app.services.legacy_import.archive import parse_legacy_archive
from app.services.legacy_import.expense_income_rows import (
build_expense_income_payload,
normalize_api_document_rows,
)
from app.services.legacy_import.id_map import LegacyIdMap
from app.services.legacy_import.mappers import (
map_legacy_person_types,
normalize_server_url,
parse_legacy_date,
)
def test_normalize_server_url():
assert normalize_server_url("") == "https://app.hesabix.ir"
assert normalize_server_url("app.hesabix.ir") == "https://app.hesabix.ir"
assert normalize_server_url("https://app.hesabix.ir/") == "https://app.hesabix.ir"
def test_format_legacy_validation_error_economic_id_too_long():
from pydantic import ValidationError
from adapters.api.v1.schemas import BusinessCreateRequest, BusinessField, BusinessType
from app.services.legacy_import.errors import format_legacy_validation_error
long_text = "یک زراعت کار، " + ("توضیح " * 20) + "معاف از پرداخت مالیات است."
with pytest.raises(ValidationError) as exc_info:
BusinessCreateRequest(
name="تست",
business_type=BusinessType.INDIVIDUAL,
business_field=BusinessField.OTHER,
default_currency_id=1,
economic_id=long_text,
)
msg = format_legacy_validation_error(exc_info.value, stage="ایجاد کسب‌وکار")
assert "شناسه اقتصادی" in msg
assert "50" in msg
assert "حسابیکس قبلی" in msg
assert "errors.pydantic.dev" not in msg
assert "string_too_long" not in msg
def test_format_legacy_import_exception_uses_api_error_message():
from app.core.responses import ApiError
from app.services.legacy_import.errors import format_legacy_import_exception
exc = ApiError(
"LEGACY_BUSINESS_DATA_INVALID",
"پیام واضح برای کاربر",
http_status=400,
)
assert format_legacy_import_exception(exc) == "پیام واضح برای کاربر"
def test_legacy_response_indicates_accpro_required():
from app.services.legacy_import.client import legacy_response_indicates_accpro_required
assert legacy_response_indicates_accpro_required(
'{"result":0,"message":"این قابلیت فقط برای کاربران افزونه accpro در دسترس است."}'
)
assert legacy_response_indicates_accpro_required("افزونه حسابداری پیشرفته فعال نیست")
assert not legacy_response_indicates_accpro_required('{"result":0,"message":"خطای دیگر"}')
assert not legacy_response_indicates_accpro_required("")
def test_legacy_api_unauthorized_does_not_return_http_401():
"""401 از سرور قدیم نباید باعث logout کاربر در پنل جدید شود."""
from unittest.mock import MagicMock
import httpx
import pytest
from app.core.responses import ApiError
from app.services.legacy_import.client import LegacyApiClient
client = LegacyApiClient("https://app.hesabix.ir", "test-api-key-12345")
response = MagicMock(spec=httpx.Response)
response.status_code = 401
response.text = "Unauthorized"
response.request = MagicMock()
response.request.url = "https://app.hesabix.ir/api/business/list"
with pytest.raises(ApiError) as exc_info:
client._raise_for_status(response, context="business/list")
assert exc_info.value.status_code == 400
assert exc_info.value.detail["error"]["code"] == "LEGACY_API_UNAUTHORIZED"
assert "کلید API نسخه قدیم" in exc_info.value.detail["error"]["message"]
def test_map_legacy_person_types_defaults():
assert map_legacy_person_types([]) == ["مشتری"]
assert "مشتری" in map_legacy_person_types([1])
assert "بازاریاب" in map_legacy_person_types([1, 2])
def test_parse_legacy_jalali_date():
d = parse_legacy_date("1404/07/10")
assert d.year >= 2024
def test_parse_legacy_archive_minimal():
buf = BytesIO()
manifest = {
"version": "1.0",
"sourceBusinessId": 1,
"sourceBusinessName": "Test Biz",
}
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("manifest.json", json.dumps(manifest))
zf.writestr("data/business.json", json.dumps([{"id": 1, "name": "Test Biz"}]))
zf.writestr("data/persons.json", json.dumps([]))
zf.writestr("data/commodities.json", json.dumps([]))
zf.writestr("data/hesabdari_docs.json", json.dumps([]))
zf.writestr("data/hesabdari_rows.json", json.dumps([]))
archive = parse_legacy_archive(buf.getvalue())
assert archive.source_business_id == 1
assert archive.source_business_name == "Test Biz"
assert archive.counts()["persons"] == 0
class _StubChart:
def resolve_account_id(self, ref_id, *, is_income=False, **kwargs):
return 501 if ref_id == 98 else 502
def test_normalize_api_document_rows():
api_rows = [
{
"ref": {"id": 98, "tableType": "calc"},
"bd": 25,
"des": "هزینه",
},
{
"ref": {"id": 5, "tableType": "bank"},
"bs": 25,
"bankAccount": 36,
},
]
rows = normalize_api_document_rows(api_rows)
assert rows[0]["ref_id"] == 98
assert rows[0]["bd"] == 25
assert rows[1]["bank_id"] == 36
assert rows[1]["bs"] == 25
def test_build_expense_income_payload_cost():
"""نمونه سند cost: bd روی calc، bs روی بانک."""
id_map = LegacyIdMap()
id_map.bank_accounts[36] = 1001
archive_rows = [
{"ref_id": 98, "bd": 25, "des": "خرید"},
{"ref_id": 5, "bank_id": 36, "bs": 25},
{"person_id": 10, "bd": 0, "bs": 0},
]
items, counterparties = build_expense_income_payload(
"cost",
archive_rows,
chart=_StubChart(),
id_map=id_map,
doc_amount=25,
)
assert len(items) == 1
assert items[0]["account_id"] == 501
assert items[0]["amount"] == 25.0
assert len(counterparties) == 1
assert counterparties[0]["transaction_type"] == "bank"
assert counterparties[0]["bank_id"] == 1001
assert counterparties[0]["amount"] == 25.0
def test_build_expense_income_payload_income():
id_map = LegacyIdMap()
id_map.bank_accounts[12] = 2002
rows = [
{"ref_id": 40, "bs": 100},
{"bank_id": 12, "bd": 100},
]
items, counterparties = build_expense_income_payload(
"income",
rows,
chart=_StubChart(),
id_map=id_map,
)
assert items[0]["amount"] == 100.0
assert counterparties[0]["bank_id"] == 2002
def test_build_invoice_lines_extra_info_pricing():
"""قیمت واحد و جمع سطر باید در extra_info باشند (قرارداد create_invoice)."""
from app.services.legacy_import.document_importer import LegacyDocumentImporter
from app.services.legacy_import.id_map import LegacyIdMap, LegacyImportStats
id_map = LegacyIdMap()
id_map.products[769] = 5001
stats = LegacyImportStats()
importer = LegacyDocumentImporter(None, 1, 1, 1, id_map, stats)
rows = [
{
"commodity_id": 769,
"commdityCount": 20,
"bs": "69400000",
"bd": "0",
"discount": "0",
"tax": "0",
}
]
lines = importer._build_invoice_lines(rows)
assert len(lines) == 1
info = lines[0]["extra_info"]
assert info["unit_price"] == 3470000.0
assert info["line_total"] == 69400000.0
assert "unit_price" not in lines[0] or lines[0].get("unit_price") is None
def test_extract_invoice_header_discount():
from app.services.legacy_import.document_importer import LegacyDocumentImporter
from app.services.legacy_import.id_map import LegacyIdMap, LegacyImportStats
importer = LegacyDocumentImporter(None, 1, 1, 1, LegacyIdMap(), LegacyImportStats())
rows = [
{"bd": "5000", "bs": "0", "des": "تخفیف فاکتور"},
{"commodity_id": 1, "bs": "1000", "bd": "0", "commdityCount": 1},
]
assert float(importer._extract_invoice_header_discount(rows)) == 5000.0
def test_invoice_payload_requires_person_in_extra_info():
"""create_invoice reads person_id from extra_info, not root payload."""
from app.services.legacy_import.document_importer import LegacyDocumentImporter
from app.services.legacy_import.id_map import LegacyIdMap, LegacyImportStats
from app.services.legacy_import.mappers import parse_legacy_date
id_map = LegacyIdMap()
id_map.persons[33] = 9001
id_map.products[769] = 5001
stats = LegacyImportStats()
doc = {"id": 132, "code": "1129", "type": "sell", "date": "1404/05/22"}
rows = [
{
"commodity_id": 769,
"commdityCount": 2,
"bs": "100000",
"person_id": 33,
}
]
importer = LegacyDocumentImporter(None, 1, 1, 1, id_map, stats)
person_id = importer._resolve_person_for_doc(doc, rows)
lines = importer._build_invoice_lines(rows)
payload = {
"invoice_type": "invoice_sales",
"document_date": parse_legacy_date(doc["date"]).isoformat(),
"currency_id": 1,
"lines": lines,
"extra_info": {
"person_id": int(person_id),
"legacy_import": True,
},
}
assert payload["extra_info"]["person_id"] == 9001
assert payload["lines"][0]["extra_info"]["unit_price"] == 50000.0
def test_parse_legacy_invoice_code_patterns():
from app.services.legacy_import.invoice_settlement import parse_legacy_invoice_code
assert parse_legacy_invoice_code("بابت تسویه فاکتور 1192") == "1192"
assert parse_legacy_invoice_code("بابت تسویه فاکتور فروش 1182") == "1182"
assert parse_legacy_invoice_code("پرداخت وجه فاکتور شماره 1000") == "1000"
assert parse_legacy_invoice_code("دریافت وجه فاکتور شماره ۱٬۱۲۹") == "1129"
assert parse_legacy_invoice_code("بابت فاکتور فروش بارمان شیمی") is None
def test_extract_linked_invoice_legacy_code_from_rows():
from app.services.legacy_import.invoice_settlement import extract_linked_invoice_legacy_code
doc = {"des": ""}
rows = [{"des": "بابت تسویه فاکتور فروش 1188"}]
assert extract_linked_invoice_legacy_code(doc, rows) == "1188"
def test_apply_related_docs_to_link_map():
from app.services.legacy_import.invoice_settlement import apply_related_docs_to_link_map
link_map: dict[str, str] = {}
apply_related_docs_to_link_map(
link_map,
"2034",
[
{"type": "sell_receive", "code": "2035", "des": "بابت دریافت فاکتور فروش"},
{"type": "cost", "code": "9999", "des": "ignored"},
],
)
assert link_map == {"2035": "2034"}
def test_resolve_linked_invoice_legacy_code_prefers_related_docs():
from app.services.legacy_import.invoice_settlement import resolve_linked_invoice_legacy_code
doc = {"des": "توضیح قدیمی بدون کد فاکتور"}
rows = [{"des": "توضیح تغییر یافته"}]
code = resolve_linked_invoice_legacy_code(
receipt_payment_code="2035",
related_docs_link_map={"2035": "2034"},
doc=doc,
rows=rows,
)
assert code == "2034"
def test_resolve_linked_invoice_legacy_code_falls_back_to_description():
from app.services.legacy_import.invoice_settlement import resolve_linked_invoice_legacy_code
code = resolve_linked_invoice_legacy_code(
receipt_payment_code="1194",
related_docs_link_map={},
doc={"des": "بابت تسویه فاکتور 1192"},
rows=[],
)
assert code == "1192"
def test_receipt_payment_person_lines_get_invoice_id():
"""sell_receive/buy_send must attach invoice_id to person_lines for settlement."""
from app.services.legacy_import.document_importer import LegacyDocumentImporter
from app.services.legacy_import.id_map import LegacyIdMap, LegacyImportStats
id_map = LegacyIdMap()
id_map.invoice_codes["1192"] = 7001
id_map.persons[33] = 9001
id_map.bank_accounts[36] = 1001
stats = LegacyImportStats()
importer = LegacyDocumentImporter(None, 1, 1, 1, id_map, stats)
doc = {
"id": 197,
"code": "1194",
"type": "sell_receive",
"date": "1404/06/15",
"des": "بابت تسویه فاکتور 1192",
}
rows = [
{"person_id": 33, "bs": "5000000", "des": "بابت تسویه فاکتور 1192"},
{"bank_id": 36, "bd": "5000000", "des": "بابت تسویه فاکتور 1192"},
]
from app.services.legacy_import.document_rows import build_receipt_payment_lines
from app.services.legacy_import.invoice_settlement import (
INVOICE_LINKED_RECEIPT_PAYMENT_TYPES,
resolve_linked_invoice_legacy_code,
)
person_lines, account_lines = build_receipt_payment_lines(rows, id_map=id_map)
legacy_type = str(doc.get("type") or "").strip()
assert legacy_type in INVOICE_LINKED_RECEIPT_PAYMENT_TYPES
linked_code = resolve_linked_invoice_legacy_code(
receipt_payment_code=doc.get("code"),
related_docs_link_map={"1194": "1192"},
doc=doc,
rows=rows,
)
assert linked_code == "1192"
linked_id = id_map.invoice_codes.get(linked_code)
assert linked_id == 7001
for pl in person_lines:
pl.setdefault("extra_info", {})
pl["extra_info"]["invoice_id"] = int(linked_id)
assert person_lines[0]["extra_info"]["invoice_id"] == 7001