Initial commit from monorepo

This commit is contained in:
Ruslan Bakiev
2026-01-07 09:16:11 +07:00
commit 27c04a4759
28 changed files with 2270 additions and 0 deletions

1
orders_app/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Orders Django app

2
orders_app/admin.py Normal file
View File

@@ -0,0 +1,2 @@
# No Django admin needed - all data managed in Odoo ERP
# Orders backend is a GraphQL proxy, not a data storage

5
orders_app/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrdersAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'orders_app'

66
orders_app/auth.py Normal file
View File

@@ -0,0 +1,66 @@
import logging
from typing import Iterable, Optional
import jwt
from django.conf import settings
from jwt import InvalidTokenError, PyJWKClient
logger = logging.getLogger(__name__)
class LogtoTokenValidator:
"""Validate JWTs issued by Logto using the published JWKS."""
def __init__(self, jwks_url: str, issuer: str):
self._issuer = issuer
self._jwks_client = PyJWKClient(jwks_url)
def decode(self, token: str, audience: Optional[str] = None) -> dict:
"""Decode and verify a JWT, enforcing issuer and optional audience."""
try:
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
header_alg = jwt.get_unverified_header(token).get("alg")
return jwt.decode(
token,
signing_key.key,
algorithms=[header_alg] if header_alg else None,
issuer=self._issuer,
audience=audience,
options={"verify_aud": audience is not None},
)
except InvalidTokenError as exc:
logger.warning("Failed to validate Logto token: %s", exc)
raise
def get_bearer_token(request) -> str:
"""Extract Bearer token from Authorization header."""
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header.startswith("Bearer "):
raise InvalidTokenError("Missing Bearer token")
token = auth_header.split(" ", 1)[1]
if not token or token == "undefined":
raise InvalidTokenError("Empty Bearer token")
return token
def scopes_from_payload(payload: dict) -> list[str]:
"""Split scope string (if present) into a list."""
scope_value = payload.get("scope")
if not scope_value:
return []
if isinstance(scope_value, str):
return scope_value.split()
if isinstance(scope_value, Iterable):
return list(scope_value)
return []
validator = LogtoTokenValidator(
getattr(settings, "LOGTO_JWKS_URL", "https://auth.optovia.ru/oidc/jwks"),
getattr(settings, "LOGTO_ISSUER", "https://auth.optovia.ru/oidc"),
)

View File

@@ -0,0 +1,67 @@
"""
GraphQL middleware for JWT authentication.
Each class is bound to a specific GraphQL endpoint (public/user/team).
"""
from django.conf import settings
from graphql import GraphQLError
from jwt import InvalidTokenError
from .auth import get_bearer_token, scopes_from_payload, validator
def _is_introspection(info) -> bool:
"""Возвращает True для любых introspection резолвов."""
field = getattr(info, "field_name", "")
parent = getattr(getattr(info, "parent_type", None), "name", "")
return field.startswith("__") or parent.startswith("__")
class PublicNoAuthMiddleware:
"""Public endpoint - no authentication required."""
def resolve(self, next, root, info, **kwargs):
return next(root, info, **kwargs)
class UserJWTMiddleware:
"""User endpoint - requires ID token."""
def resolve(self, next, root, info, **kwargs):
request = info.context
if _is_introspection(info):
return next(root, info, **kwargs)
try:
token = get_bearer_token(request)
payload = validator.decode(token)
request.user_id = payload.get('sub')
except InvalidTokenError as exc:
raise GraphQLError("Unauthorized") from exc
return next(root, info, **kwargs)
class TeamJWTMiddleware:
"""Team endpoint - requires Access token for orders audience."""
def resolve(self, next, root, info, **kwargs):
request = info.context
if _is_introspection(info):
return next(root, info, **kwargs)
try:
token = get_bearer_token(request)
payload = validator.decode(
token,
audience=getattr(settings, 'LOGTO_ORDERS_AUDIENCE', None),
)
request.user_id = payload.get('sub')
request.team_uuid = payload.get('team_uuid') # Custom claim from Logto
request.scopes = scopes_from_payload(payload)
if not request.team_uuid or 'teams:member' not in request.scopes:
raise GraphQLError("Unauthorized")
except InvalidTokenError as exc:
raise GraphQLError("Unauthorized") from exc
return next(root, info, **kwargs)

63
orders_app/middleware.py Normal file
View File

@@ -0,0 +1,63 @@
import json
import logging
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from jwt import InvalidTokenError
from .auth import get_bearer_token, scopes_from_payload, validator
logger = logging.getLogger(__name__)
class LogtoJWTMiddleware(MiddlewareMixin):
"""
JWT middleware для проверки токенов от Logto
"""
def __init__(self, get_response=None):
super().__init__(get_response)
# Audience validated only for non-introspection API calls
self.audience = getattr(settings, "LOGTO_ORDERS_AUDIENCE", None)
def _is_introspection_query(self, request):
"""Проверяет, является ли запрос introspection (для GraphQL codegen)"""
if request.method != 'POST':
return False
try:
body = json.loads(request.body.decode('utf-8'))
query = body.get('query', '')
return '__schema' in query or '__type' in query
except Exception:
return False
def process_request(self, request):
"""Обрабатывает Team Access Token для Orders API"""
# Пропускаем проверку для admin панели и статики
if request.path.startswith('/admin/') or request.path.startswith('/static/'):
return None
# Пропускаем introspection запросы (для GraphQL codegen)
if self._is_introspection_query(request):
return None
try:
token = get_bearer_token(request)
payload = validator.decode(token, audience=self.audience)
# Проверяем scope
scopes = scopes_from_payload(payload)
if 'teams:member' not in scopes:
return None
# Извлекаем данные пользователя и команды
request.user_id = payload.get('sub')
request.team_uuid = payload.get('team_uuid')
request.scopes = scopes
except InvalidTokenError:
# Любая ошибка = нет доступа (тихо)
return None
return None

View File

2
orders_app/models.py Normal file
View File

@@ -0,0 +1,2 @@
# Orders backend - GraphQL proxy for Odoo
# No Django models needed - all data comes from Odoo FastAPI endpoints

View File

@@ -0,0 +1,2 @@
# Odoo models removed - backend now uses direct Odoo FastAPI calls
# No need to duplicate Odoo tables as Django models

70
orders_app/permissions.py Normal file
View File

@@ -0,0 +1,70 @@
"""
Декоратор для проверки scopes в JWT токене.
Используется для защиты GraphQL резолверов.
"""
from functools import wraps
from graphql import GraphQLError
def require_scopes(*scopes: str):
"""
Декоратор для проверки наличия scopes в JWT токене.
Использование:
@require_scopes("read:orders")
def resolve_getTeamOrders(self, info):
...
"""
def decorator(func):
# Сохраняем scopes в метаданных для возможности сбора всех scopes
if not hasattr(func, '_required_scopes'):
func._required_scopes = []
func._required_scopes.extend(scopes)
@wraps(func)
def wrapper(self, info, *args, **kwargs):
# Получаем scopes из контекста (должны быть добавлены в middleware)
user_scopes = set(getattr(info.context, 'scopes', []) or [])
missing = set(scopes) - user_scopes
if missing:
raise GraphQLError(f"Missing required scopes: {', '.join(missing)}")
return func(self, info, *args, **kwargs)
# Переносим метаданные на wrapper
wrapper._required_scopes = func._required_scopes
return wrapper
return decorator
def collect_scopes_from_schema(schema) -> set:
"""
Собирает все scopes из схемы для синхронизации с Logto.
Использование:
from .schema import schema
scopes = collect_scopes_from_schema(schema)
# {'read:orders'}
"""
scopes = set()
# Query resolvers
if hasattr(schema, 'query') and schema.query:
query_type = schema.query
for field_name in dir(query_type):
if field_name.startswith('resolve_'):
resolver = getattr(query_type, field_name, None)
if resolver and hasattr(resolver, '_required_scopes'):
scopes.update(resolver._required_scopes)
# Mutation resolvers
if hasattr(schema, 'mutation') and schema.mutation:
mutation_type = schema.mutation
for field_name, field in mutation_type._meta.fields.items():
if hasattr(field, 'type') and hasattr(field.type, 'mutate'):
mutate = field.type.mutate
if hasattr(mutate, '_required_scopes'):
scopes.update(mutate._required_scopes)
return scopes

View File

View File

@@ -0,0 +1,12 @@
import graphene
class PublicQuery(graphene.ObjectType):
"""Public schema - no authentication required"""
_placeholder = graphene.String(description="Placeholder field")
def resolve__placeholder(self, info):
return None
public_schema = graphene.Schema(query=PublicQuery)

View File

@@ -0,0 +1,291 @@
import graphene
import requests
from django.conf import settings
from ..permissions import require_scopes
class OrderType(graphene.ObjectType):
uuid = graphene.String()
name = graphene.String()
teamUuid = graphene.String()
userId = graphene.String()
status = graphene.String()
totalAmount = graphene.Float()
currency = graphene.String()
sourceLocationUuid = graphene.String()
sourceLocationName = graphene.String()
destinationLocationUuid = graphene.String()
destinationLocationName = graphene.String()
createdAt = graphene.String()
updatedAt = graphene.String()
notes = graphene.String()
orderLines = graphene.List(lambda: OrderLineType)
stages = graphene.List(lambda: StageType)
class OrderLineType(graphene.ObjectType):
uuid = graphene.String()
productUuid = graphene.String()
productName = graphene.String()
quantity = graphene.Float()
unit = graphene.String()
priceUnit = graphene.Float()
subtotal = graphene.Float()
currency = graphene.String()
notes = graphene.String()
class CompanyType(graphene.ObjectType):
uuid = graphene.String()
name = graphene.String()
taxId = graphene.String()
country = graphene.String()
countryCode = graphene.String()
active = graphene.Boolean()
class TripType(graphene.ObjectType):
uuid = graphene.String()
name = graphene.String()
sequence = graphene.Int()
company = graphene.Field(CompanyType)
plannedLoadingDate = graphene.String()
actualLoadingDate = graphene.String()
realLoadingDate = graphene.String()
plannedUnloadingDate = graphene.String()
actualUnloadingDate = graphene.String()
plannedWeight = graphene.Float()
weightAtLoading = graphene.Float()
weightAtUnloading = graphene.Float()
class StageType(graphene.ObjectType):
uuid = graphene.String()
name = graphene.String()
sequence = graphene.Int()
stageType = graphene.String()
transportType = graphene.String()
sourceLocationName = graphene.String()
sourceLatitude = graphene.Float()
sourceLongitude = graphene.Float()
destinationLocationName = graphene.String()
destinationLatitude = graphene.Float()
destinationLongitude = graphene.Float()
locationName = graphene.String()
locationLatitude = graphene.Float()
locationLongitude = graphene.Float()
selectedCompany = graphene.Field(CompanyType)
trips = graphene.List(TripType)
class TeamQuery(graphene.ObjectType):
"""Team schema - Team Access Token authentication"""
getTeamOrders = graphene.List(OrderType)
getOrder = graphene.Field(OrderType, orderUuid=graphene.String(required=True))
@require_scopes("teams:member")
def resolve_getTeamOrders(self, info):
user_id = getattr(info.context, 'user_id', None)
team_uuid = getattr(info.context, 'team_uuid', None)
if not user_id:
raise Exception("User not authenticated")
if not team_uuid:
return []
try:
url = f"http://{settings.ODOO_INTERNAL_URL}/fastapi/orders/orders/team/{team_uuid}"
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
result = []
for order_data in data:
order = OrderType(
uuid=order_data.get('uuid'),
name=order_data.get('name'),
teamUuid=order_data.get('team_uuid'),
userId=order_data.get('user_id'),
status='active',
totalAmount=order_data.get('total_amount'),
currency=order_data.get('currency'),
sourceLocationUuid=order_data.get('source_location_uuid'),
sourceLocationName=order_data.get('source_location_name'),
destinationLocationUuid=order_data.get('destination_location_uuid'),
destinationLocationName=order_data.get('destination_location_name'),
createdAt=order_data.get('created_at'),
updatedAt=order_data.get('updated_at'),
notes=order_data.get('notes', ''),
orderLines=[
OrderLineType(
uuid=line.get('uuid'),
productUuid=line.get('product_uuid'),
productName=line.get('product_name'),
quantity=line.get('quantity'),
unit=line.get('unit'),
priceUnit=line.get('price_unit'),
subtotal=line.get('subtotal'),
currency=line.get('currency'),
notes=line.get('notes', '')
) for line in order_data.get('order_lines', [])
],
stages=[
StageType(
uuid=stage.get('uuid'),
name=stage.get('name'),
sequence=stage.get('sequence'),
stageType=stage.get('stage_type'),
transportType=stage.get('transport_type'),
sourceLocationName=stage.get('source_location_name'),
destinationLocationName=stage.get('destination_location_name'),
locationName=stage.get('location_name'),
selectedCompany=CompanyType(
uuid=stage.get('selected_company', {}).get('uuid', ''),
name=stage.get('selected_company', {}).get('name', ''),
taxId=stage.get('selected_company', {}).get('tax_id', ''),
country=stage.get('selected_company', {}).get('country', ''),
countryCode=stage.get('selected_company', {}).get('country_code', ''),
active=stage.get('selected_company', {}).get('active', True)
) if stage.get('selected_company') else None,
trips=[
TripType(
uuid=trip.get('uuid'),
name=trip.get('name'),
sequence=trip.get('sequence'),
company=CompanyType(
uuid=trip.get('company', {}).get('uuid', ''),
name=trip.get('company', {}).get('name', ''),
taxId=trip.get('company', {}).get('tax_id', ''),
country=trip.get('company', {}).get('country', ''),
countryCode=trip.get('company', {}).get('country_code', ''),
active=trip.get('company', {}).get('active', True)
) if trip.get('company') else None,
plannedLoadingDate=trip.get('planned_loading_date'),
actualLoadingDate=trip.get('actual_loading_date'),
realLoadingDate=trip.get('real_loading_date'),
plannedUnloadingDate=trip.get('planned_unloading_date'),
actualUnloadingDate=trip.get('actual_unloading_date'),
plannedWeight=trip.get('planned_weight'),
weightAtLoading=trip.get('weight_at_loading'),
weightAtUnloading=trip.get('weight_at_unloading')
) for trip in stage.get('trips', [])
]
) for stage in order_data.get('stages', [])
]
)
result.append(order)
return result
else:
return []
except Exception as e:
print(f"Error calling Odoo: {e}")
return []
@require_scopes("teams:member")
def resolve_getOrder(self, info, orderUuid):
user_id = getattr(info.context, 'user_id', None)
team_uuid = getattr(info.context, 'team_uuid', None)
if not user_id or not team_uuid:
raise Exception("User not authenticated")
try:
url = f"http://{settings.ODOO_INTERNAL_URL}/fastapi/orders/orders/{orderUuid}"
response = requests.get(url, timeout=10)
if response.status_code == 200:
order_data = response.json()
if order_data.get('team_uuid') != team_uuid:
raise Exception("Access denied: order belongs to different team")
order = OrderType(
uuid=order_data.get('uuid'),
name=order_data.get('name'),
teamUuid=order_data.get('team_uuid'),
userId=order_data.get('user_id'),
status='active',
totalAmount=order_data.get('total_amount'),
currency=order_data.get('currency'),
sourceLocationUuid=order_data.get('source_location_uuid'),
sourceLocationName=order_data.get('source_location_name'),
destinationLocationUuid=order_data.get('destination_location_uuid'),
destinationLocationName=order_data.get('destination_location_name'),
createdAt=order_data.get('created_at'),
updatedAt=order_data.get('updated_at'),
notes=order_data.get('notes', ''),
orderLines=[
OrderLineType(
uuid=line.get('uuid'),
productUuid=line.get('product_uuid'),
productName=line.get('product_name'),
quantity=line.get('quantity'),
unit=line.get('unit'),
priceUnit=line.get('price_unit'),
subtotal=line.get('subtotal'),
currency=line.get('currency'),
notes=line.get('notes', '')
) for line in order_data.get('order_lines', [])
],
stages=[
StageType(
uuid=stage.get('uuid'),
name=stage.get('name'),
sequence=stage.get('sequence'),
stageType=stage.get('stage_type'),
transportType=stage.get('transport_type'),
sourceLocationName=stage.get('source_location_name'),
sourceLatitude=stage.get('source_latitude'),
sourceLongitude=stage.get('source_longitude'),
destinationLocationName=stage.get('destination_location_name'),
destinationLatitude=stage.get('destination_latitude'),
destinationLongitude=stage.get('destination_longitude'),
locationName=stage.get('location_name'),
locationLatitude=stage.get('location_latitude'),
locationLongitude=stage.get('location_longitude'),
selectedCompany=CompanyType(
uuid=stage.get('selected_company', {}).get('uuid', ''),
name=stage.get('selected_company', {}).get('name', ''),
taxId=stage.get('selected_company', {}).get('tax_id', ''),
country=stage.get('selected_company', {}).get('country', ''),
countryCode=stage.get('selected_company', {}).get('country_code', ''),
active=stage.get('selected_company', {}).get('active', True)
) if stage.get('selected_company') else None,
trips=[
TripType(
uuid=trip.get('uuid'),
name=trip.get('name'),
sequence=trip.get('sequence'),
company=CompanyType(
uuid=trip.get('company', {}).get('uuid', ''),
name=trip.get('company', {}).get('name', ''),
taxId=trip.get('company', {}).get('tax_id', ''),
country=trip.get('company', {}).get('country', ''),
countryCode=trip.get('company', {}).get('country_code', ''),
active=trip.get('company', {}).get('active', True)
) if trip.get('company') else None,
plannedLoadingDate=trip.get('planned_loading_date'),
actualLoadingDate=trip.get('actual_loading_date'),
realLoadingDate=trip.get('real_loading_date'),
plannedUnloadingDate=trip.get('planned_unloading_date'),
actualUnloadingDate=trip.get('actual_unloading_date'),
plannedWeight=trip.get('planned_weight'),
weightAtLoading=trip.get('weight_at_loading'),
weightAtUnloading=trip.get('weight_at_unloading')
) for trip in stage.get('trips', [])
]
) for stage in order_data.get('stages', [])
]
)
return order
else:
return None
except Exception as e:
print(f"Exception: {e}")
return None
team_schema = graphene.Schema(query=TeamQuery)

View File

@@ -0,0 +1,12 @@
import graphene
class UserQuery(graphene.ObjectType):
"""User schema - ID token authentication"""
_placeholder = graphene.String(description="Placeholder field")
def resolve__placeholder(self, info):
return None
user_schema = graphene.Schema(query=UserQuery)

169
orders_app/services.py Normal file
View File

@@ -0,0 +1,169 @@
import requests
from django.conf import settings
from .models import Order, OrderLine, Stage, Trip
class OdooService:
def __init__(self):
self.base_url = f"http://{settings.ODOO_INTERNAL_URL}"
def get_odoo_orders(self, team_uuid):
"""Получить заказы из Odoo API"""
try:
# Прямой вызов Odoo FastAPI
url = f"{self.base_url}/fastapi/orders/api/v1/orders/team/{team_uuid}"
print(f"Calling Odoo API: {url}")
response = requests.get(url, timeout=10)
print(f"Odoo response: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Odoo data: {len(data)} orders")
return data
else:
print(f"Odoo error: {response.text}")
return []
except Exception as e:
print(f"Error fetching from Odoo: {e}")
return []
def get_odoo_order(self, order_uuid):
"""Получить заказ из Odoo API"""
try:
url = f"{self.base_url}/fastapi/orders/api/v1/orders/{order_uuid}"
response = requests.get(url, timeout=10)
if response.status_code == 200:
return response.json()
return None
except Exception as e:
print(f"Error fetching order from Odoo: {e}")
return None
def sync_team_orders(self, team_uuid):
"""Синхронизировать заказы команды с Odoo"""
odoo_orders = self.get_odoo_orders(team_uuid)
django_orders = []
for odoo_order in odoo_orders:
# Создаем или обновляем заказ в Django
order, created = Order.objects.get_or_create(
uuid=odoo_order['uuid'],
defaults={
'name': odoo_order['name'],
'team_uuid': odoo_order['teamUuid'],
'user_id': odoo_order['userId'],
'source_location_uuid': odoo_order['sourceLocationUuid'],
'source_location_name': odoo_order['sourceLocationName'],
'destination_location_uuid': odoo_order['destinationLocationUuid'],
'destination_location_name': odoo_order['destinationLocationName'],
'status': odoo_order['status'],
'total_amount': odoo_order['totalAmount'],
'currency': odoo_order['currency'],
'notes': odoo_order.get('notes', ''),
}
)
# Синхронизируем order lines
self.sync_order_lines(order, odoo_order.get('orderLines', []))
# Синхронизируем stages
self.sync_stages(order, odoo_order.get('stages', []))
django_orders.append(order)
return django_orders
def sync_order(self, order_uuid):
"""Синхронизировать один заказ с Odoo"""
odoo_order = self.get_odoo_order(order_uuid)
if not odoo_order:
return None
# Создаем или обновляем заказ
order, created = Order.objects.get_or_create(
uuid=odoo_order['uuid'],
defaults={
'name': odoo_order['name'],
'team_uuid': odoo_order['teamUuid'],
'user_id': odoo_order['userId'],
'source_location_uuid': odoo_order['sourceLocationUuid'],
'source_location_name': odoo_order['sourceLocationName'],
'destination_location_uuid': odoo_order['destinationLocationUuid'],
'destination_location_name': odoo_order['destinationLocationName'],
'status': odoo_order['status'],
'total_amount': odoo_order['totalAmount'],
'currency': odoo_order['currency'],
'notes': odoo_order.get('notes', ''),
}
)
# Синхронизируем связанные данные
self.sync_order_lines(order, odoo_order.get('orderLines', []))
self.sync_stages(order, odoo_order.get('stages', []))
return order
def sync_order_lines(self, order, odoo_lines):
"""Синхронизировать строки заказа"""
# Удаляем старые
order.order_lines.all().delete()
# Создаем новые
for line_data in odoo_lines:
OrderLine.objects.create(
uuid=line_data['uuid'],
order=order,
product_uuid=line_data['productUuid'],
product_name=line_data['productName'],
quantity=line_data['quantity'],
unit=line_data['unit'],
price_unit=line_data['priceUnit'],
subtotal=line_data['subtotal'],
currency=line_data.get('currency', 'RUB'),
notes=line_data.get('notes', ''),
)
def sync_stages(self, order, odoo_stages):
"""Синхронизировать этапы заказа"""
# Удаляем старые
order.stages.all().delete()
# Создаем новые
for stage_data in odoo_stages:
stage = Stage.objects.create(
uuid=stage_data['uuid'],
order=order,
name=stage_data['name'],
sequence=stage_data['sequence'],
stage_type=stage_data['stageType'],
transport_type=stage_data.get('transportType', ''),
source_location_name=stage_data.get('sourceLocationName', ''),
destination_location_name=stage_data.get('destinationLocationName', ''),
location_name=stage_data.get('locationName', ''),
selected_company_uuid=stage_data.get('selectedCompany', {}).get('uuid', '') if stage_data.get('selectedCompany') else '',
selected_company_name=stage_data.get('selectedCompany', {}).get('name', '') if stage_data.get('selectedCompany') else '',
)
# Синхронизируем trips
self.sync_trips(stage, stage_data.get('trips', []))
def sync_trips(self, stage, odoo_trips):
"""Синхронизировать рейсы этапа"""
for trip_data in odoo_trips:
Trip.objects.create(
uuid=trip_data['uuid'],
stage=stage,
name=trip_data['name'],
sequence=trip_data['sequence'],
company_uuid=trip_data.get('company', {}).get('uuid', '') if trip_data.get('company') else '',
company_name=trip_data.get('company', {}).get('name', '') if trip_data.get('company') else '',
planned_weight=trip_data.get('plannedWeight'),
weight_at_loading=trip_data.get('weightAtLoading'),
weight_at_unloading=trip_data.get('weightAtUnloading'),
planned_loading_date=trip_data.get('plannedLoadingDate'),
actual_loading_date=trip_data.get('actualLoadingDate'),
real_loading_date=trip_data.get('realLoadingDate'),
planned_unloading_date=trip_data.get('plannedUnloadingDate'),
actual_unloading_date=trip_data.get('actualUnloadingDate'),
notes=trip_data.get('notes', ''),
)

119
orders_app/tests.py Normal file
View File

@@ -0,0 +1,119 @@
from django.test import TestCase
from graphene.test import Client
from orders_app.schema import schema
class OrdersGraphQLTestCase(TestCase):
def setUp(self):
self.client = Client(schema)
def test_get_team_orders(self):
"""Тест getTeamOrders"""
query = '''
{
getTeamOrders(teamUuid: "4993148a-cd2b-4987-b3f2-0f92b7479885") {
uuid
name
totalAmount
sourceLocationName
destinationLocationName
orderLines {
productName
quantity
unit
}
stages {
name
stageType
transportType
}
}
}
'''
result = self.client.execute(query)
print(f"\n=== TEAM ORDERS TEST ===")
print(f"Result: {result}")
if result.get('errors'):
print(f"ERRORS: {result['errors']}")
if result.get('data'):
orders = result['data']['getTeamOrders']
print(f"Found {len(orders)} orders")
for order in orders:
print(f"Order: {order.get('name')} - {order.get('totalAmount')}")
if order.get('stages'):
print(f" Stages: {len(order['stages'])}")
def test_get_order_single(self):
"""Тест getOrder для конкретного заказа"""
query = '''
{
getOrder(orderUuid: "order-coffee-kenya-2024") {
uuid
name
totalAmount
sourceLocationName
destinationLocationName
orderLines {
productName
quantity
unit
}
stages {
name
stageType
transportType
trips {
name
company {
name
}
plannedWeight
weightAtLoading
}
}
}
}
'''
result = self.client.execute(query)
print(f"\n=== SINGLE ORDER TEST ===")
print(f"Result: {result}")
if result.get('errors'):
print(f"ERRORS: {result['errors']}")
if result.get('data') and result['data']['getOrder']:
order = result['data']['getOrder']
print(f"Order: {order.get('name')}")
print(f"Amount: {order.get('totalAmount')}")
print(f"Route: {order.get('sourceLocationName')}{order.get('destinationLocationName')}")
if order.get('orderLines'):
print(f"Order lines: {len(order['orderLines'])}")
for line in order['orderLines']:
print(f" - {line.get('productName')}: {line.get('quantity')} {line.get('unit')}")
if order.get('stages'):
print(f"Stages: {len(order['stages'])}")
for stage in order['stages']:
print(f" - {stage.get('name')} ({stage.get('stageType')})")
if stage.get('trips'):
print(f" Trips: {len(stage['trips'])}")
def test_schema_types(self):
"""Тест доступных типов в схеме"""
query = '{ __schema { types { name } } }'
result = self.client.execute(query)
print(f"\n=== SCHEMA TYPES ===")
if result.get('data'):
types = [t['name'] for t in result['data']['__schema']['types']]
print(f"Available types: {types}")
# Проверяем что есть нужные типы
required_types = ['OrderType', 'StageType', 'TripType', 'CompanyType']
for req_type in required_types:
if req_type in types:
print(f"{req_type} - OK")
else:
print(f"{req_type} - MISSING")

68
orders_app/views.py Normal file
View File

@@ -0,0 +1,68 @@
"""
Views for Orders API.
Authentication is handled by GRAPHENE MIDDLEWARE in settings.py
"""
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from jwt import InvalidTokenError
from graphene_django.views import GraphQLView
from .graphql_middleware import PublicNoAuthMiddleware, TeamJWTMiddleware, UserJWTMiddleware
from .auth import get_bearer_token, scopes_from_payload, validator
@csrf_exempt
def test_jwt_orders(request):
"""Test endpoint for Orders API JWT validation"""
try:
token = get_bearer_token(request)
except InvalidTokenError as exc:
return JsonResponse({"status": "error", "error": str(exc)}, status=403)
response = {"token_length": len(token), "token_preview": f"{token[:32]}...{token[-32:]}"}
try:
payload = validator.decode(token, audience=getattr(settings, "LOGTO_ORDERS_AUDIENCE", None))
response.update(
{
"status": "ok",
"payload": payload,
"user_id": payload.get("sub"),
"team_uuid": payload.get("team_uuid"),
"audience": payload.get("aud"),
"scopes": scopes_from_payload(payload),
}
)
return JsonResponse(response, json_dumps_params={'indent': 2})
except InvalidTokenError as exc:
response["status"] = "invalid"
response["error"] = str(exc)
return JsonResponse(response, status=403, json_dumps_params={'indent': 2})
class PublicGraphQLView(GraphQLView):
"""Public endpoint - no authentication required."""
def __init__(self, *args, **kwargs):
kwargs['middleware'] = [PublicNoAuthMiddleware()]
super().__init__(*args, **kwargs)
class UserGraphQLView(GraphQLView):
"""User endpoint - requires ID Token."""
def __init__(self, *args, **kwargs):
kwargs['middleware'] = [UserJWTMiddleware()]
super().__init__(*args, **kwargs)
class TeamGraphQLView(GraphQLView):
"""Team endpoint - requires Team Access Token."""
def __init__(self, *args, **kwargs):
kwargs['middleware'] = [TeamJWTMiddleware()]
super().__init__(*args, **kwargs)