diff --git a/backend/alembic/versions/eec3c13599e7_add_order_status_client.py b/backend/alembic/versions/eec3c13599e7_add_order_status_client.py new file mode 100644 index 0000000..435b879 --- /dev/null +++ b/backend/alembic/versions/eec3c13599e7_add_order_status_client.py @@ -0,0 +1,30 @@ +"""add_order_status_client + +Revision ID: eec3c13599e7 +Revises: fbbfad4cd8f3 +Create Date: 2026-03-13 17:34:26.366597 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eec3c13599e7' +down_revision: Union[str, None] = 'fbbfad4cd8f3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('orders', sa.Column('status_client', sa.String(length=20), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('orders', 'status_client') + # ### end Alembic commands ### diff --git a/backend/app/client_portal/__init__.py b/backend/app/client_portal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/client_portal/router.py b/backend/app/client_portal/router.py new file mode 100644 index 0000000..94f291d --- /dev/null +++ b/backend/app/client_portal/router.py @@ -0,0 +1,94 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select, text +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.models.order import Order +from app.db.models.order_line import OrderLine +from app.db.models.tenant import Tenant +from app.db.models.vehicle import Vehicle +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/p/{token}") +async def get_deviz_public(token: str, db: AsyncSession = Depends(get_db)): + r = await db.execute(select(Order).where(Order.token_client == token)) + order = r.scalar_one_or_none() + if not order: + raise HTTPException(status_code=404, detail="Deviz not found") + + r = await db.execute(select(Tenant).where(Tenant.id == order.tenant_id)) + tenant = r.scalar_one() + + r = await db.execute(select(Vehicle).where(Vehicle.id == order.vehicle_id)) + vehicle = r.scalar_one_or_none() + + r = await db.execute( + select(OrderLine).where(OrderLine.order_id == order.id) + ) + lines = r.scalars().all() + + return { + "order": { + "id": order.id, + "status": order.status, + "data_comanda": order.data_comanda, + "total_manopera": order.total_manopera, + "total_materiale": order.total_materiale, + "total_general": order.total_general, + "nr_auto": vehicle.nr_inmatriculare if vehicle else "", + "observatii": order.observatii, + }, + "tenant": { + "nume": tenant.nume, + "telefon": tenant.telefon, + }, + "lines": [ + { + "tip": l.tip, + "descriere": l.descriere, + "ore": l.ore, + "pret_ora": l.pret_ora, + "cantitate": l.cantitate, + "pret_unitar": l.pret_unitar, + "um": l.um, + "total": l.total, + } + for l in lines + ], + } + + +@router.post("/p/{token}/accept") +async def accept_deviz(token: str, db: AsyncSession = Depends(get_db)): + r = await db.execute(select(Order).where(Order.token_client == token)) + order = r.scalar_one_or_none() + if not order: + raise HTTPException(status_code=404, detail="Deviz not found") + await db.execute( + text( + "UPDATE orders SET status_client='ACCEPTAT', updated_at=datetime('now') " + "WHERE token_client=:t" + ), + {"t": token}, + ) + await db.commit() + return {"ok": True} + + +@router.post("/p/{token}/reject") +async def reject_deviz(token: str, db: AsyncSession = Depends(get_db)): + r = await db.execute(select(Order).where(Order.token_client == token)) + order = r.scalar_one_or_none() + if not order: + raise HTTPException(status_code=404, detail="Deviz not found") + await db.execute( + text( + "UPDATE orders SET status_client='RESPINS', updated_at=datetime('now') " + "WHERE token_client=:t" + ), + {"t": token}, + ) + await db.commit() + return {"ok": True} diff --git a/backend/app/db/models/order.py b/backend/app/db/models/order.py index 79ac23b..c88f534 100644 --- a/backend/app/db/models/order.py +++ b/backend/app/db/models/order.py @@ -17,3 +17,4 @@ class Order(Base, UUIDMixin, TenantMixin, TimestampMixin): total_materiale: Mapped[float] = mapped_column(Float, default=0) total_general: Mapped[float] = mapped_column(Float, default=0) token_client: Mapped[str | None] = mapped_column(String(36)) + status_client: Mapped[str | None] = mapped_column(String(20)) diff --git a/backend/app/invoices/__init__.py b/backend/app/invoices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/invoices/router.py b/backend/app/invoices/router.py new file mode 100644 index 0000000..447e505 --- /dev/null +++ b/backend/app/invoices/router.py @@ -0,0 +1,111 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import Response +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.models.invoice import Invoice +from app.db.models.order import Order +from app.db.models.order_line import OrderLine +from app.db.models.tenant import Tenant +from app.db.models.vehicle import Vehicle +from app.db.session import get_db +from app.deps import get_tenant_id +from app.invoices import service +from app.pdf.service import generate_factura + +router = APIRouter() + + +@router.post("") +async def create_invoice( + data: dict, + tenant_id: str = Depends(get_tenant_id), + db: AsyncSession = Depends(get_db), +): + order_id = data.get("order_id") + if not order_id: + raise HTTPException(status_code=422, detail="order_id required") + try: + invoice = await service.create_invoice(db, tenant_id, order_id) + return {"id": invoice.id, "nr_factura": invoice.nr_factura} + except ValueError as e: + raise HTTPException(status_code=422, detail=str(e)) + + +@router.get("/{invoice_id}/pdf") +async def get_invoice_pdf( + invoice_id: str, + tenant_id: str = Depends(get_tenant_id), + db: AsyncSession = Depends(get_db), +): + r = await db.execute( + select(Invoice).where( + Invoice.id == invoice_id, Invoice.tenant_id == tenant_id + ) + ) + invoice = r.scalar_one_or_none() + if not invoice: + raise HTTPException(status_code=404, detail="Invoice not found") + + r = await db.execute(select(Order).where(Order.id == invoice.order_id)) + order = r.scalar_one() + + r = await db.execute(select(Vehicle).where(Vehicle.id == order.vehicle_id)) + vehicle = r.scalar_one_or_none() + + r = await db.execute(select(Tenant).where(Tenant.id == tenant_id)) + tenant = r.scalar_one() + + r = await db.execute( + select(OrderLine).where(OrderLine.order_id == order.id) + ) + lines = r.scalars().all() + + order_data = { + "id": order.id, + "nr_auto": vehicle.nr_inmatriculare if vehicle else "", + "client_nume": vehicle.client_nume if vehicle else "", + "client_cui": vehicle.client_cui if vehicle else "", + "client_adresa": vehicle.client_adresa if vehicle else "", + "total_manopera": order.total_manopera, + "total_materiale": order.total_materiale, + "total_general": order.total_general, + } + + invoice_data = { + "nr_factura": invoice.nr_factura, + "data_factura": invoice.data_factura, + "total": invoice.total, + } + + tenant_data = { + "nume": tenant.nume, + "cui": tenant.cui, + "reg_com": tenant.reg_com, + "adresa": tenant.adresa, + "iban": tenant.iban, + "banca": tenant.banca, + } + + lines_data = [ + { + "tip": l.tip, + "descriere": l.descriere, + "ore": l.ore, + "pret_ora": l.pret_ora, + "cantitate": l.cantitate, + "pret_unitar": l.pret_unitar, + "um": l.um, + "total": l.total, + } + for l in lines + ] + + pdf_bytes = generate_factura(invoice_data, order_data, lines_data, tenant_data) + return Response( + content=pdf_bytes, + media_type="application/pdf", + headers={ + "Content-Disposition": f'inline; filename="factura-{invoice.nr_factura}.pdf"' + }, + ) diff --git a/backend/app/invoices/service.py b/backend/app/invoices/service.py new file mode 100644 index 0000000..57ac2ad --- /dev/null +++ b/backend/app/invoices/service.py @@ -0,0 +1,49 @@ +from datetime import UTC, datetime + +from sqlalchemy import select, text +from sqlalchemy.ext.asyncio import AsyncSession + +from app.db.base import uuid7 +from app.db.models.invoice import Invoice +from app.db.models.order import Order + + +async def create_invoice( + db: AsyncSession, tenant_id: str, order_id: str +) -> Invoice: + r = await db.execute( + select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id) + ) + order = r.scalar_one_or_none() + if not order: + raise ValueError("Order not found") + if order.status != "VALIDAT": + raise ValueError("Order must be VALIDAT to create invoice") + + # Generate next invoice number for this tenant + r = await db.execute( + text( + "SELECT COUNT(*) as cnt FROM invoices WHERE tenant_id = :tid" + ), + {"tid": tenant_id}, + ) + count = r.scalar() + 1 + now = datetime.now(UTC) + nr_factura = f"F-{now.year}-{count:04d}" + + invoice = Invoice( + id=uuid7(), + tenant_id=tenant_id, + order_id=order_id, + nr_factura=nr_factura, + data_factura=now.isoformat().split("T")[0], + total=order.total_general, + ) + db.add(invoice) + + # Update order status to FACTURAT + order.status = "FACTURAT" + order.updated_at = now.isoformat() + await db.commit() + await db.refresh(invoice) + return invoice diff --git a/backend/app/main.py b/backend/app/main.py index 35b9f7a..4bb5275 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,9 +4,11 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.auth.router import router as auth_router +from app.client_portal.router import router as portal_router from app.config import settings from app.db.base import Base from app.db.session import engine +from app.invoices.router import router as invoices_router from app.orders.router import router as orders_router from app.sync.router import router as sync_router from app.vehicles.router import router as vehicles_router @@ -34,6 +36,8 @@ app.include_router(auth_router, prefix="/api/auth") app.include_router(sync_router, prefix="/api/sync") app.include_router(orders_router, prefix="/api/orders") app.include_router(vehicles_router, prefix="/api/vehicles") +app.include_router(invoices_router, prefix="/api/invoices") +app.include_router(portal_router, prefix="/api") @app.get("/api/health") diff --git a/backend/app/orders/router.py b/backend/app/orders/router.py index 156d53d..0763a20 100644 --- a/backend/app/orders/router.py +++ b/backend/app/orders/router.py @@ -1,9 +1,16 @@ from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import Response +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.db.models.order import Order +from app.db.models.order_line import OrderLine +from app.db.models.tenant import Tenant +from app.db.models.vehicle import Vehicle from app.db.session import get_db from app.deps import get_tenant_id from app.orders import schemas, service +from app.pdf.service import generate_deviz router = APIRouter() @@ -81,3 +88,70 @@ async def validate_order( return {"status": order.status} except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) + + +@router.get("/{order_id}/pdf/deviz") +async def get_deviz_pdf( + order_id: str, + tenant_id: str = Depends(get_tenant_id), + db: AsyncSession = Depends(get_db), +): + r = await db.execute( + select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id) + ) + order = r.scalar_one_or_none() + if not order: + raise HTTPException(status_code=404, detail="Order not found") + + r = await db.execute(select(Vehicle).where(Vehicle.id == order.vehicle_id)) + vehicle = r.scalar_one_or_none() + + r = await db.execute(select(Tenant).where(Tenant.id == tenant_id)) + tenant = r.scalar_one() + + r = await db.execute( + select(OrderLine).where(OrderLine.order_id == order.id) + ) + lines = r.scalars().all() + + order_data = { + "id": order.id, + "data_comanda": order.data_comanda, + "nr_auto": vehicle.nr_inmatriculare if vehicle else "", + "client_nume": vehicle.client_nume if vehicle else "", + "marca_denumire": "", + "model_denumire": "", + "total_manopera": order.total_manopera, + "total_materiale": order.total_materiale, + "total_general": order.total_general, + } + + tenant_data = { + "nume": tenant.nume, + "cui": tenant.cui, + "adresa": tenant.adresa, + "telefon": tenant.telefon, + } + + lines_data = [ + { + "tip": l.tip, + "descriere": l.descriere, + "ore": l.ore, + "pret_ora": l.pret_ora, + "cantitate": l.cantitate, + "pret_unitar": l.pret_unitar, + "um": l.um, + "total": l.total, + } + for l in lines + ] + + pdf_bytes = generate_deviz(order_data, lines_data, tenant_data) + return Response( + content=pdf_bytes, + media_type="application/pdf", + headers={ + "Content-Disposition": f'inline; filename="deviz-{order.id[:8]}.pdf"' + }, + ) diff --git a/backend/app/pdf/__init__.py b/backend/app/pdf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/pdf/service.py b/backend/app/pdf/service.py new file mode 100644 index 0000000..927eaab --- /dev/null +++ b/backend/app/pdf/service.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader +from weasyprint import HTML + +TEMPLATES = Path(__file__).parent / "templates" + + +def generate_deviz(order: dict, lines: list, tenant: dict) -> bytes: + env = Environment(loader=FileSystemLoader(str(TEMPLATES))) + html = env.get_template("deviz.html").render( + order=order, + tenant=tenant, + manopera=[l for l in lines if l.get("tip") == "manopera"], + materiale=[l for l in lines if l.get("tip") == "material"], + ) + return HTML(string=html).write_pdf() + + +def generate_factura( + invoice: dict, order: dict, lines: list, tenant: dict +) -> bytes: + env = Environment(loader=FileSystemLoader(str(TEMPLATES))) + html = env.get_template("factura.html").render( + invoice=invoice, + order=order, + tenant=tenant, + lines=lines, + ) + return HTML(string=html).write_pdf() diff --git a/backend/app/pdf/templates/deviz.html b/backend/app/pdf/templates/deviz.html new file mode 100644 index 0000000..132850b --- /dev/null +++ b/backend/app/pdf/templates/deviz.html @@ -0,0 +1,67 @@ + + + + + +
+
+ {{ tenant.nume }}
+ {% if tenant.cui %}CUI: {{ tenant.cui }}
{% endif %} + {% if tenant.adresa %}{{ tenant.adresa }}
{% endif %} + {% if tenant.telefon %}Tel: {{ tenant.telefon }}{% endif %} +
+
+

DEVIZ Nr. {{ order.nr_comanda or order.id[:8]|upper }}

+
Data: {{ order.data_comanda }}
+
Auto: {{ order.nr_auto }}
+
{{ order.marca_denumire or '' }} {{ order.model_denumire or '' }}
+
Client: {{ order.client_nume or '' }}
+
+
+ +{% if manopera %} +

Operatii manopera

+ + + {% for l in manopera %} + + + + + + {% endfor %} +
DescriereOrePret/ora (RON)Total (RON)
{{ l.descriere }}{{ l.ore }}{{ "%.2f"|format(l.pret_ora or 0) }}{{ "%.2f"|format(l.total or 0) }}
+{% endif %} + +{% if materiale %} +

Materiale

+ + + {% for l in materiale %} + + + + + + {% endfor %} +
DescriereUMCant.Pret unit. (RON)Total (RON)
{{ l.descriere }}{{ l.um }}{{ l.cantitate }}{{ "%.2f"|format(l.pret_unitar or 0) }}{{ "%.2f"|format(l.total or 0) }}
+{% endif %} + +
+
Manopera: {{ "%.2f"|format(order.total_manopera or 0) }} RON
+
Materiale: {{ "%.2f"|format(order.total_materiale or 0) }} RON
+
TOTAL: {{ "%.2f"|format(order.total_general or 0) }} RON
+
+ diff --git a/backend/app/pdf/templates/factura.html b/backend/app/pdf/templates/factura.html new file mode 100644 index 0000000..c722326 --- /dev/null +++ b/backend/app/pdf/templates/factura.html @@ -0,0 +1,65 @@ + + + + + +
+
+ FURNIZOR
+ {{ tenant.nume }}
+ {% if tenant.cui %}CUI: {{ tenant.cui }}
{% endif %} + {% if tenant.reg_com %}Reg. Com.: {{ tenant.reg_com }}
{% endif %} + {% if tenant.adresa %}{{ tenant.adresa }}
{% endif %} + {% if tenant.iban %}IBAN: {{ tenant.iban }}
{% endif %} + {% if tenant.banca %}Banca: {{ tenant.banca }}{% endif %} +
+
+

FACTURA Nr. {{ invoice.nr_factura }}

+
Data: {{ invoice.data_factura }}
+
+
+ +
+ CLIENT
+ {{ order.client_nume or 'N/A' }}
+ {% if order.client_cui %}CUI: {{ order.client_cui }}
{% endif %} + {% if order.client_adresa %}{{ order.client_adresa }}{% endif %} +
+ +
+ Auto: {{ order.nr_auto }} | Deviz: {{ order.id[:8]|upper }} +
+ + + + {% for l in lines %} + + + + + + + + + {% endfor %} +
#DescriereUMCant.Pret unit. (RON)Total (RON)
{{ loop.index }}{{ l.descriere }}{{ l.um or ('ore' if l.tip == 'manopera' else 'buc') }}{{ l.ore if l.tip == 'manopera' else l.cantitate }}{{ "%.2f"|format(l.pret_ora if l.tip == 'manopera' else l.pret_unitar) }}{{ "%.2f"|format(l.total or 0) }}
+ +
+
Manopera: {{ "%.2f"|format(order.total_manopera or 0) }} RON
+
Materiale: {{ "%.2f"|format(order.total_materiale or 0) }} RON
+
TOTAL: {{ "%.2f"|format(invoice.total or 0) }} RON
+
+ diff --git a/backend/app/sms/__init__.py b/backend/app/sms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/sms/service.py b/backend/app/sms/service.py new file mode 100644 index 0000000..d93237e --- /dev/null +++ b/backend/app/sms/service.py @@ -0,0 +1,18 @@ +import httpx + +from app.config import settings + + +async def send_deviz_sms( + telefon: str, token_client: str, tenant_name: str, base_url: str +): + if not settings.SMSAPI_TOKEN: + return # skip in dev/test + url = f"{base_url}/p/{token_client}" + msg = f"{tenant_name}: Devizul tau e gata. Vizualizeaza: {url}" + async with httpx.AsyncClient() as c: + await c.post( + "https://api.smsapi.ro/sms.do", + headers={"Authorization": f"Bearer {settings.SMSAPI_TOKEN}"}, + data={"to": telefon, "message": msg, "from": "ROAAUTO"}, + ) diff --git a/backend/tests/test_pdf.py b/backend/tests/test_pdf.py new file mode 100644 index 0000000..cfb64d4 --- /dev/null +++ b/backend/tests/test_pdf.py @@ -0,0 +1,142 @@ +import uuid +from datetime import UTC, datetime + +import pytest +from httpx import ASGITransport, AsyncClient + +from app.main import app + + +@pytest.mark.asyncio +async def test_pdf_deviz_returns_pdf(client, auth_headers): + me = await client.get("/api/auth/me", headers=auth_headers) + tenant_id = me.json()["tenant_id"] + + vid = str(uuid.uuid4()) + now = datetime.now(UTC).isoformat() + await client.post( + "/api/sync/push", + headers=auth_headers, + json={ + "operations": [ + { + "table": "vehicles", + "id": vid, + "operation": "INSERT", + "data": { + "id": vid, + "tenant_id": tenant_id, + "nr_inmatriculare": "CT99PDF", + "client_nume": "PDF Test", + "created_at": now, + "updated_at": now, + }, + "timestamp": now, + } + ] + }, + ) + + r = await client.post( + "/api/orders", + headers=auth_headers, + json={"vehicle_id": vid}, + ) + order_id = r.json()["id"] + + await client.post( + f"/api/orders/{order_id}/lines", + headers=auth_headers, + json={ + "tip": "manopera", + "descriere": "Diagnosticare", + "ore": 0.5, + "pret_ora": 100, + }, + ) + + r = await client.get( + f"/api/orders/{order_id}/pdf/deviz", + headers=auth_headers, + ) + assert r.status_code == 200 + assert r.headers["content-type"] == "application/pdf" + assert r.content[:4] == b"%PDF" + + +@pytest.mark.asyncio +async def test_invoice_workflow(client, auth_headers): + me = await client.get("/api/auth/me", headers=auth_headers) + tenant_id = me.json()["tenant_id"] + + vid = str(uuid.uuid4()) + now = datetime.now(UTC).isoformat() + await client.post( + "/api/sync/push", + headers=auth_headers, + json={ + "operations": [ + { + "table": "vehicles", + "id": vid, + "operation": "INSERT", + "data": { + "id": vid, + "tenant_id": tenant_id, + "nr_inmatriculare": "B01INV", + "client_nume": "Factura Test", + "created_at": now, + "updated_at": now, + }, + "timestamp": now, + } + ] + }, + ) + + # Create order + line + validate + r = await client.post( + "/api/orders", + headers=auth_headers, + json={"vehicle_id": vid}, + ) + order_id = r.json()["id"] + + await client.post( + f"/api/orders/{order_id}/lines", + headers=auth_headers, + json={ + "tip": "material", + "descriere": "Filtru aer", + "cantitate": 1, + "pret_unitar": 80, + "um": "buc", + }, + ) + + await client.post( + f"/api/orders/{order_id}/validate", + headers=auth_headers, + ) + + # Create invoice + r = await client.post( + "/api/invoices", + headers=auth_headers, + json={"order_id": order_id}, + ) + assert r.status_code == 200 + data = r.json() + assert "id" in data + assert "nr_factura" in data + assert data["nr_factura"].startswith("F-") + + # Get invoice PDF + invoice_id = data["id"] + r = await client.get( + f"/api/invoices/{invoice_id}/pdf", + headers=auth_headers, + ) + assert r.status_code == 200 + assert r.headers["content-type"] == "application/pdf" + assert r.content[:4] == b"%PDF" diff --git a/backend/tests/test_portal.py b/backend/tests/test_portal.py new file mode 100644 index 0000000..d627c33 --- /dev/null +++ b/backend/tests/test_portal.py @@ -0,0 +1,101 @@ +import uuid +from datetime import UTC, datetime + +import pytest +from httpx import ASGITransport, AsyncClient + +from app.main import app + + +async def _setup_order_with_lines(client, auth_headers): + """Create vehicle + order + lines, return order details.""" + me = await client.get("/api/auth/me", headers=auth_headers) + tenant_id = me.json()["tenant_id"] + + vid = str(uuid.uuid4()) + now = datetime.now(UTC).isoformat() + await client.post( + "/api/sync/push", + headers=auth_headers, + json={ + "operations": [ + { + "table": "vehicles", + "id": vid, + "operation": "INSERT", + "data": { + "id": vid, + "tenant_id": tenant_id, + "nr_inmatriculare": "B99PDF", + "client_nume": "Ion Popescu", + "created_at": now, + "updated_at": now, + }, + "timestamp": now, + } + ] + }, + ) + + r = await client.post( + "/api/orders", + headers=auth_headers, + json={"vehicle_id": vid}, + ) + order_id = r.json()["id"] + + await client.post( + f"/api/orders/{order_id}/lines", + headers=auth_headers, + json={ + "tip": "manopera", + "descriere": "Schimb ulei", + "ore": 1, + "pret_ora": 150, + }, + ) + + # Get order to find token_client + r = await client.get(f"/api/orders/{order_id}", headers=auth_headers) + return r.json() + + +@pytest.mark.asyncio +async def test_portal_get_deviz(client, auth_headers): + order = await _setup_order_with_lines(client, auth_headers) + token = order["token_client"] + + # Portal access is public (no auth headers) + r = await client.get(f"/api/p/{token}") + assert r.status_code == 200 + data = r.json() + assert "order" in data + assert "tenant" in data + assert "lines" in data + assert data["order"]["nr_auto"] == "B99PDF" + + +@pytest.mark.asyncio +async def test_portal_accept(client, auth_headers): + order = await _setup_order_with_lines(client, auth_headers) + token = order["token_client"] + + r = await client.post(f"/api/p/{token}/accept") + assert r.status_code == 200 + assert r.json()["ok"] is True + + +@pytest.mark.asyncio +async def test_portal_reject(client, auth_headers): + order = await _setup_order_with_lines(client, auth_headers) + token = order["token_client"] + + r = await client.post(f"/api/p/{token}/reject") + assert r.status_code == 200 + assert r.json()["ok"] is True + + +@pytest.mark.asyncio +async def test_portal_invalid_token(client): + r = await client.get("/api/p/invalid-token") + assert r.status_code == 404