Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/public_share_links.py
2026-09-03 00:02:08 +00:00

743 lines
26 KiB
Python
Executable file

import base64
import io
import re
from datetime import datetime
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, Body
from pydantic import BaseModel, Field
from fastapi.responses import RedirectResponse
from fastapi.responses import Response, StreamingResponse
from sqlalchemy.orm import Session
from adapters.db.models.person import Person
from adapters.db.session import get_db
from app.core.responses import ApiError, success_response, format_datetime_fields
from app.core.settings import get_settings
from app.core.calendar import CalendarConverter
from app.services.invoice_adjustments_service import payable_total_from_totals_dict
from app.services.pdf.template_renderer import render_template
from app.services.person_share_link_service import (
get_public_invoice_details,
resolve_public_payload_by_code,
)
from app.services.document_share_link_service import (
resolve_public_payload_by_code as resolve_invoice_document_share_by_code,
)
from app.services.file_storage_service import FileStorageService
from app.services.share_link_logo_service import (
business_logo_file_id_for_invoice_share,
business_logo_file_id_for_person_share,
)
from app.services.system_settings_service import get_share_link_settings
from decimal import Decimal
from app.services.public_invoice_share_payment_service import start_public_invoice_share_payment
router = APIRouter(tags=["public-share-links"])
def _strip_trailing_public_segment(url: str) -> str:
"""حذف پسوند /public از پایهٔ URL تا مسیر نهایی دوبل نشود (/public/public/...)."""
u = (url or "").strip().rstrip("/")
if u.lower().endswith("/public"):
u = u[: -len("/public")].rstrip("/")
return u
def _flutter_public_page_url(request: Request, settings_public_app: str, env_public_app: str, subpath: str) -> str:
"""
URL کامل صفحهٔ عمومی Flutter (مثلاً /public/invoice-link/{code}).
اولویت با همان scheme+host درخواست است تا لینک کوتاه روی hsxn به arc نپرد
(مگر اینکه واقعاً فقط روی دامنهٔ دیگری UI سرو شود — آن زمان باید nginx همان دامنه /public را داشته باشد
یا لینک کوتاه روی دامنهٔ UI صادر شود).
"""
same_origin = f"{request.url.scheme}://{request.url.netloc}".rstrip("/")
configured = _strip_trailing_public_segment(settings_public_app)
env_base = _strip_trailing_public_segment(env_public_app)
base = same_origin or configured or env_base
base = base.rstrip("/")
return f"{base}/public/{subpath.lstrip('/')}"
@router.get(
"/api/v1/public/person-links/{code}",
summary="دریافت اطلاعات عمومی کارت حساب بدون احراز هویت",
)
async def get_public_person_link(
code: str,
request: Request,
db: Session = Depends(get_db),
):
try:
payload = resolve_public_payload_by_code(db, code)
except ApiError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
return success_response(
data=payload,
request=request,
message="اطلاعات کارت حساب دریافت شد",
)
@router.get(
"/api/v1/public/person-links/{code}/business-logo",
summary="لوگوی کسب‌وکار برای لینک اشتراک کارت حساب (بدون شمارش بازدید)",
)
async def get_public_person_share_business_logo(
code: str,
db: Session = Depends(get_db),
):
file_uuid = business_logo_file_id_for_person_share(db, code)
if not file_uuid:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
storage = FileStorageService(db)
try:
file_data = await storage.download_file(file_uuid)
except Exception:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
content = file_data.get("content") or b""
if not content:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
filename = file_data.get("filename") or "logo"
return StreamingResponse(
io.BytesIO(content),
media_type=file_data.get("mime_type") or "image/png",
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
@router.get(
"/api/v1/public/person-links/{code}/invoices/{document_id}",
summary="دریافت جزئیات فاکتور از طریق لینک اشتراک",
)
async def get_public_invoice_details_endpoint(
code: str,
document_id: int,
request: Request,
db: Session = Depends(get_db),
):
try:
details = get_public_invoice_details(db, code, document_id)
except ApiError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
return success_response(
data=details,
request=request,
message="جزئیات فاکتور دریافت شد",
)
@router.get(
"/p/{code}",
summary="انتقال به صفحه عمومی Flutter",
)
async def redirect_public_person_link(
code: str,
request: Request,
db: Session = Depends(get_db),
):
settings = get_share_link_settings(db)
configured = (settings.get("public_app_url") or "").strip()
env_app = (get_settings().share_link_public_app_url or "").strip()
target_url = _flutter_public_page_url(request, configured, env_app, f"person-link/{code}")
return RedirectResponse(url=target_url, status_code=307)
@router.get(
"/api/v1/public/invoice-links/{code}",
summary="نمایش عمومی فاکتور از طریق کد لینک (بدون احراز هویت)",
)
async def get_public_invoice_document_link(
code: str,
request: Request,
db: Session = Depends(get_db),
):
try:
payload = resolve_invoice_document_share_by_code(db, code)
except ApiError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
business_id = None
try:
business_id = int(((payload or {}).get("business") or {}).get("id"))
except Exception:
business_id = None
formatted_payload = format_datetime_fields(payload, request, business_id=business_id)
return success_response(
data=formatted_payload,
request=request,
message="اطلاعات فاکتور دریافت شد",
)
class PublicInvoicePayStartBody(BaseModel):
amount: float = Field(..., gt=0, description="مبلغ پرداخت به ریال")
@router.post(
"/api/v1/public/invoice-links/{code}/pay/start",
summary="شروع پرداخت آنلاین از لینک عمومی فاکتور",
)
async def post_public_invoice_pay_start(
code: str,
request: Request,
body: PublicInvoicePayStartBody,
db: Session = Depends(get_db),
):
try:
data = start_public_invoice_share_payment(
db,
share_code=code,
amount=Decimal(str(body.amount)),
)
except ApiError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
return success_response(
data=data,
request=request,
message="PUBLIC_INVOICE_PAYMENT_STARTED",
)
@router.get(
"/api/v1/public/invoice-links/{code}/business-logo",
summary="لوگوی کسب‌وکار برای لینک اشتراک فاکتور (بدون شمارش بازدید)",
)
async def get_public_invoice_share_business_logo(
code: str,
db: Session = Depends(get_db),
):
file_uuid = business_logo_file_id_for_invoice_share(db, code)
if not file_uuid:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
storage = FileStorageService(db)
try:
file_data = await storage.download_file(file_uuid)
except Exception:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
content = file_data.get("content") or b""
if not content:
raise HTTPException(status_code=404, detail="لوگو یافت نشد")
filename = file_data.get("filename") or "logo"
return StreamingResponse(
io.BytesIO(content),
media_type=file_data.get("mime_type") or "image/png",
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
@router.get(
"/api/v1/public/invoice-links/{code}/pdf",
summary="خروجی PDF عمومی فاکتور از طریق کد لینک (بدون احراز هویت)",
)
async def get_public_invoice_document_pdf(
code: str,
request: Request,
db: Session = Depends(get_db),
):
try:
payload = resolve_invoice_document_share_by_code(db, code)
except ApiError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
invoice = (payload or {}).get("invoice") or {}
business = (payload or {}).get("business") or {}
installments = (payload or {}).get("installments") or {}
lines = invoice.get("product_lines") or []
extra = invoice.get("extra_info") or {}
totals = extra.get("totals") if isinstance(extra, dict) else {}
totals = totals if isinstance(totals, dict) else {}
def _num(v) -> float:
try:
return float(v or 0)
except Exception:
return 0.0
def _line_unit_display(row: dict) -> str:
unit = str(row.get("product_main_unit") or "").strip()
if unit:
return unit
return "-"
is_fa = str(request.headers.get("Accept-Language") or "").lower().startswith("fa")
calendar_header = (request.headers.get("X-Calendar-Type") or "").strip().lower()
calendar_type = calendar_header if calendar_header in {"jalali", "gregorian"} else ("jalali" if is_fa else "gregorian")
def _fmt_date(raw_value):
if raw_value is None:
return "-"
try:
dt = datetime.fromisoformat(str(raw_value).replace("Z", "+00:00"))
fd = CalendarConverter.format_datetime(dt, calendar_type)
return fd.get("date_only") or fd.get("formatted") or str(raw_value)
except Exception:
return str(raw_value)
def _status_label(v: str) -> str:
status = str(v or "").strip().lower()
if is_fa:
return {
"paid": "پرداخت‌شده",
"partial": "پرداخت ناقص",
"overdue": "سررسید گذشته",
"pending": "در انتظار",
}.get(status, status or "در انتظار")
return {
"paid": "Paid",
"partial": "Partial",
"overdue": "Overdue",
"pending": "Pending",
}.get(status, status or "Pending")
def _invoice_type_label(doc_type: str) -> str:
key = str(doc_type or "").strip().lower()
if is_fa:
return {
"invoice_sales": "فروش",
"invoice_sales_return": "برگشت از فروش",
"invoice_purchase": "خرید",
"invoice_purchase_return": "برگشت از خرید",
"invoice_direct_consumption": "مصرف مستقیم",
"invoice_production": "تولید",
"invoice_waste": "ضایعات",
}.get(key, doc_type or "-")
return {
"invoice_sales": "Sales",
"invoice_sales_return": "Sales Return",
"invoice_purchase": "Purchase",
"invoice_purchase_return": "Purchase Return",
"invoice_direct_consumption": "Direct Consumption",
"invoice_production": "Production",
"invoice_waste": "Waste",
}.get(key, doc_type or "-")
def _normalize_line(row: dict) -> dict:
q = _num(row.get("quantity"))
from app.services.print_tax_discount_display import (
enrich_line_amount_fields,
line_amounts_before_global_discount,
parse_discount_meta,
should_restore_line_amounts_before_global,
)
info = row.get("extra_info") if isinstance(row.get("extra_info"), dict) else {}
unit_price = _num(row.get("unit_price") if row.get("unit_price") is not None else info.get("unit_price"))
line_discount = _num(
row.get("line_discount") if row.get("line_discount") is not None else info.get("line_discount")
)
tax_amount = _num(
row.get("tax_amount") if row.get("tax_amount") is not None else info.get("tax_amount")
)
line_total = _num(
row.get("line_total") if row.get("line_total") is not None else info.get("line_total")
)
discount_type, discount_value = parse_discount_meta(info)
if discount_type is None:
discount_type, discount_value = parse_discount_meta(row)
gd = (extra or {}).get("global_discount") if isinstance(extra, dict) else None
if should_restore_line_amounts_before_global(gd if isinstance(gd, dict) else None):
restored = line_amounts_before_global_discount(
quantity=q,
unit_price=unit_price,
line_discount=line_discount,
tax_rate=info.get("tax_rate", row.get("tax_rate")),
)
tax_amount = restored["tax_amount"]
line_total = restored["line_total"]
return enrich_line_amount_fields(
{
"product_code": row.get("product_code"),
"product_name": row.get("product_name"),
"description": row.get("description"),
"quantity": q,
"quantity_display": str(int(q)) if float(q).is_integer() else f"{q:.3f}".rstrip("0").rstrip("."),
"unit_display": _line_unit_display(row),
"unit_price": unit_price,
"discount": line_discount,
"discount_type": discount_type,
"discount_value": discount_value,
"tax_amount": tax_amount,
"line_total": line_total,
"attributes_display": "",
},
is_fa=is_fa,
)
normalized_lines = [_normalize_line(row) for row in lines if isinstance(row, dict)]
has_line_discount = any((_num(x.get("discount")) != 0) for x in normalized_lines)
has_line_tax = any((_num(x.get("tax_amount")) != 0) for x in normalized_lines)
person_info = {}
try:
person_id = (extra or {}).get("person_id")
if person_id is not None:
person = (
db.query(Person)
.filter(
Person.id == int(person_id),
Person.business_id == int(invoice.get("business_id") or business.get("id") or 0),
)
.first()
)
if person is not None:
display_name = None
try:
first = (getattr(person, "first_name", None) or "").strip()
last = (getattr(person, "last_name", None) or "").strip()
full_name = f"{first} {last}".strip()
display_name = (
full_name
or getattr(person, "company_name", None)
or getattr(person, "alias_name", None)
or "-"
)
except Exception:
display_name = getattr(person, "alias_name", None) or "-"
le_type = getattr(person, "legal_entity_type", None) or "natural"
person_info = {
"id": getattr(person, "id", None),
"name": display_name,
"name_prefix": getattr(person, "name_prefix", None),
"legal_entity_type": le_type,
"legal_entity_type_label": "حقوقی" if le_type == "legal" else "حقیقی",
"national_id": getattr(person, "national_id", None),
"registration_number": getattr(person, "registration_number", None),
"economic_id": getattr(person, "economic_id", None),
"address": getattr(person, "address", None),
"postal_code": getattr(person, "postal_code", None),
"mobile": getattr(person, "mobile", None),
"phone": getattr(person, "phone", None),
}
except Exception:
person_info = {}
doc_type = str(invoice.get("document_type") or "")
if doc_type in ("invoice_purchase", "invoice_purchase_return"):
seller_info = person_info or {
"name": business.get("name"),
"address": business.get("address"),
"phone": business.get("phone"),
"mobile": business.get("mobile"),
}
buyer_info = {
"name": business.get("name"),
"address": business.get("address"),
"phone": business.get("phone"),
"mobile": business.get("mobile"),
}
else:
seller_info = {
"name": business.get("name"),
"address": business.get("address"),
"phone": business.get("phone"),
"mobile": business.get("mobile"),
}
buyer_info = person_info
invoice_view = dict(invoice)
invoice_view["title"] = "فاکتور" if is_fa else "Invoice"
invoice_view["issue_date"] = _fmt_date(invoice.get("document_date"))
invoice_view["invoice_type_name"] = _invoice_type_label(str(invoice.get("document_type") or ""))
invoice_view["subtotal"] = _num(totals.get("gross"))
invoice_view["discount_total"] = _num(totals.get("discount"))
invoice_view["tax_total"] = _num(totals.get("tax"))
invoice_view["payable_total"] = float(payable_total_from_totals_dict(totals))
invoice_view["amount_before_discount_and_tax"] = _num(totals.get("gross"))
invoice_view["amount_without_tax"] = _num(totals.get("gross")) - _num(totals.get("discount"))
from app.services.business_print_settings_resolver import load_print_settings
from app.services.print_tax_discount_display import (
build_global_discount_print_info,
build_invoice_tax_discount_display_flags,
)
business_id = invoice.get("business_id") or business.get("id")
print_settings = load_print_settings(
db,
int(business_id or 0),
str(invoice.get("document_type") or ""),
)
tax_discount_display_flags = build_invoice_tax_discount_display_flags(
print_settings,
has_line_discount=has_line_discount,
has_line_tax=has_line_tax,
discount_total=invoice_view.get("discount_total"),
tax_total=invoice_view.get("tax_total"),
amount_without_tax=invoice_view.get("amount_without_tax"),
subtotal=invoice_view.get("subtotal"),
)
line_discount_total = sum(_num(x.get("discount")) for x in normalized_lines)
global_discount_print = build_global_discount_print_info(
extra if isinstance(extra, dict) else {},
line_discount_total=line_discount_total,
is_fa=is_fa,
)
invoice_view["line_discount_total"] = global_discount_print["line_discount_total"]
invoice_view["global_discount_amount"] = global_discount_print["global_discount_amount"]
invoice_view["global_discount_type"] = global_discount_print["global_discount_type"]
invoice_view["global_discount_value"] = global_discount_print["global_discount_value"]
invoice_view["global_discount_display"] = global_discount_print["global_discount_display"]
invoice_view["show_discount_breakdown"] = global_discount_print["show_discount_breakdown"]
invoice_view["discount_summary_label"] = global_discount_print["discount_summary_label"]
invoice_view["discount_line_label"] = global_discount_print["discount_line_label"]
invoice_view["discount_global_label"] = global_discount_print["discount_global_label"]
invoice_view["discount_summary_display"] = global_discount_print["discount_summary_display"]
invoice_view["has_global_discount"] = global_discount_print["has_global_discount"]
invoice_adjustments_rows: list = []
adjustments_net_signed = 0.0
adjustments_tax_signed = 0.0
adjustments_additions_total = 0.0
adjustments_deductions_total = 0.0
final_payable_total = invoice_view["payable_total"]
try:
raw_adjustments = extra.get("invoice_adjustments") if isinstance(extra, dict) else None
if isinstance(raw_adjustments, list) and raw_adjustments:
from adapters.db.models.account import Account as _Account
acc_ids: list = []
for row in raw_adjustments:
if isinstance(row, dict) and row.get("account_id") is not None:
try:
acc_ids.append(int(row["account_id"]))
except Exception:
continue
acc_name_map: dict = {}
acc_code_map: dict = {}
if acc_ids:
try:
for acc in db.query(_Account).filter(_Account.id.in_(set(acc_ids))).all():
acc_name_map[int(acc.id)] = acc.name or ""
acc_code_map[int(acc.id)] = getattr(acc, "code", None)
except Exception:
pass
for row in raw_adjustments:
if not isinstance(row, dict):
continue
kind = str(row.get("kind") or "").strip().lower()
if kind not in ("addition", "deduction"):
continue
amt = _num(row.get("amount"))
tr = _num(row.get("tax_rate"))
tax_amt = _num(row.get("tax_amount"))
total = _num(row.get("total")) if row.get("total") is not None else (amt + tax_amt)
acc_id_int = None
try:
if row.get("account_id") is not None:
acc_id_int = int(row["account_id"])
except Exception:
acc_id_int = None
# اگر payload عمومی already enrich شده باشد، نام/کد حساب همراه ردیف می‌آید.
acc_name = (
row.get("account_name")
or (acc_name_map.get(acc_id_int) if acc_id_int is not None else None)
)
acc_code = (
row.get("account_code")
or (acc_code_map.get(acc_id_int) if acc_id_int is not None else None)
)
kind_label = (
("اضافه" if is_fa else "Addition")
if kind == "addition"
else ("کسر" if is_fa else "Deduction")
)
sign = 1 if kind == "addition" else -1
adjustments_net_signed += amt * sign
adjustments_tax_signed += tax_amt * sign
if kind == "addition":
adjustments_additions_total += total
else:
adjustments_deductions_total += total
desc_val = row.get("description")
desc_str = desc_val.strip() if isinstance(desc_val, str) else (desc_val or "")
invoice_adjustments_rows.append({
"kind": kind,
"kind_label": kind_label,
"amount": amt,
"tax_rate": tr,
"tax_amount": tax_amt,
"total": total,
"signed_amount": amt * sign,
"signed_tax": tax_amt * sign,
"signed_total": total * sign,
"account_id": acc_id_int,
"account_name": acc_name,
"account_code": acc_code,
"description": desc_str,
})
else:
adjustments_net_signed = _num((totals or {}).get("adjustments_net"))
adjustments_tax_signed = _num((totals or {}).get("adjustments_tax"))
adjustments_net_signed = round(adjustments_net_signed, 2)
adjustments_tax_signed = round(adjustments_tax_signed, 2)
adjustments_additions_total = round(adjustments_additions_total, 2)
adjustments_deductions_total = round(adjustments_deductions_total, 2)
final_payable_total = round(float(payable_total_from_totals_dict(totals)), 2)
except Exception:
invoice_adjustments_rows = []
adjustments_net_signed = 0.0
adjustments_tax_signed = 0.0
adjustments_additions_total = 0.0
adjustments_deductions_total = 0.0
final_payable_total = invoice_view.get("payable_total")
invoice_view["invoice_adjustments"] = invoice_adjustments_rows
invoice_view["adjustments_net"] = adjustments_net_signed
invoice_view["adjustments_tax"] = adjustments_tax_signed
invoice_view["adjustments_additions_total"] = adjustments_additions_total
invoice_view["adjustments_deductions_total"] = adjustments_deductions_total
invoice_view["final_payable_total"] = final_payable_total
storage = FileStorageService(db)
async def _load_logo_data_uri(logo_uuid: Optional[UUID]) -> Optional[str]:
if not logo_uuid:
return None
try:
file_data = await storage.download_file(logo_uuid)
except Exception:
return None
content_bytes = file_data.get("content") or b""
if not content_bytes:
return None
mime = file_data.get("mime_type") or "image/png"
b64 = base64.b64encode(content_bytes).decode("ascii")
return f"data:{mime};base64,{b64}"
logo_uuid = business_logo_file_id_for_invoice_share(db, code)
business_logo_data_uri = await _load_logo_data_uri(logo_uuid)
installment_plan = None
if installments.get("has_installments"):
installment_plan = {
"meta": {
"invoice_code": invoice.get("code"),
},
"data": {
"schedule": [
{
**it,
"due_date_display": _fmt_date(it.get("due_date")),
"status": _status_label(str(it.get("status") or "")),
}
for it in (installments.get("schedule") or [])
if isinstance(it, dict)
],
},
}
template_context = {
"title_text": "فاکتور" if is_fa else "Invoice",
"is_fa": is_fa,
"generated_at": datetime.utcnow(),
"business_name": business.get("name") or "",
"business": {
"name": business.get("name"),
"address": business.get("address"),
"phone": business.get("phone"),
"mobile": business.get("mobile"),
},
"seller": seller_info,
"buyer": buyer_info,
"invoice": invoice_view,
"lines": normalized_lines,
"has_line_discount": tax_discount_display_flags["has_line_discount"],
"has_line_tax": tax_discount_display_flags["has_line_tax"],
"show_line_discount_column": tax_discount_display_flags["show_line_discount_column"],
"show_line_tax_column": tax_discount_display_flags["show_line_tax_column"],
"show_line_amount_before_discount_column": tax_discount_display_flags[
"show_line_amount_before_discount_column"
],
"show_line_amount_before_tax_column": tax_discount_display_flags[
"show_line_amount_before_tax_column"
],
"show_summary_discount": tax_discount_display_flags["show_summary_discount"],
"show_summary_tax": tax_discount_display_flags["show_summary_tax"],
"show_summary_amount_without_tax": tax_discount_display_flags[
"show_summary_amount_without_tax"
],
"payments": [],
"installment_plan": installment_plan,
"business_logo_data_uri": business_logo_data_uri,
"business_stamp_data_uri": None,
"owner_signature_data_uri": None,
"show_invoice_verify_qr": False,
"invoice_verify_qr_data_uri": None,
"show_footer_print_time": True,
"show_footer_preparer": False,
"invoice_footer_note": None,
"customer_balance_info": {},
"show_seller_signature_area": True,
"show_buyer_signature_area": True,
"stamp_scale_percent": 100,
"signature_scale_percent": 100,
"stamp_max_width_px": 160,
"signature_max_width_px": 140,
"paper_size": None,
"orientation": "landscape",
"footer_text": "",
"is_multi_currency": False,
"base_currency": invoice.get("base_currency")
if isinstance(invoice.get("base_currency"), dict)
else {},
}
try:
from app.services.fx_rate_provider_service import business_is_multi_currency
bid = business.get("id") or invoice.get("business_id")
if bid is not None:
template_context["is_multi_currency"] = bool(business_is_multi_currency(db, int(bid)))
except Exception:
fx_t = invoice.get("fx_totals") if isinstance(invoice.get("fx_totals"), dict) else {}
template_context["is_multi_currency"] = bool(fx_t.get("show_dual"))
html_content = render_template("pdf/invoices/detail.html", template_context)
from weasyprint import HTML
from weasyprint.text.fonts import FontConfiguration
pdf_bytes = HTML(string=html_content).write_pdf(font_config=FontConfiguration())
def _slugify(text: str) -> str:
return re.sub(r"[^A-Za-z0-9_-]+", "_", (text or "")).strip("_") or "invoice"
filename = f"public_invoice_{_slugify(str(invoice.get('code') or code))}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(pdf_bytes)),
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.get(
"/i/{code}",
summary="انتقال به صفحهٔ عمومی فاکتور در Flutter",
)
async def redirect_public_invoice_link(
code: str,
request: Request,
db: Session = Depends(get_db),
):
settings = get_share_link_settings(db)
configured = (settings.get("public_app_url") or "").strip()
env_app = (get_settings().share_link_public_app_url or "").strip()
target_url = _flutter_public_page_url(request, configured, env_app, f"invoice-link/{code}")
return RedirectResponse(url=target_url, status_code=307)
from adapters.api.v1.public.crm_chat_public import router as _crm_chat_public_router # noqa: E402
router.include_router(_crm_chat_public_router)