Add offer workflow with Terminus and graph sync
This commit is contained in:
127
backends/exchange/exchange/schemas/m2m_schema.py
Normal file
127
backends/exchange/exchange/schemas/m2m_schema.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
M2M (Machine-to-Machine) GraphQL schema for Exchange.
|
||||||
|
Used by internal services (Temporal workflows, etc.) without user authentication.
|
||||||
|
"""
|
||||||
|
import graphene
|
||||||
|
import logging
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from offers.models import Offer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OfferType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Offer
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class M2MQuery(graphene.ObjectType):
|
||||||
|
offer = graphene.Field(OfferType, offerUuid=graphene.String(required=True))
|
||||||
|
|
||||||
|
def resolve_offer(self, info, offerUuid):
|
||||||
|
try:
|
||||||
|
return Offer.objects.get(uuid=offerUuid)
|
||||||
|
except Offer.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CreateOfferFromWorkflowInput(graphene.InputObjectType):
|
||||||
|
offerUuid = graphene.String(required=True)
|
||||||
|
teamUuid = graphene.String(required=True)
|
||||||
|
productUuid = graphene.String(required=True)
|
||||||
|
productName = graphene.String(required=True)
|
||||||
|
categoryName = graphene.String()
|
||||||
|
locationUuid = graphene.String()
|
||||||
|
locationName = graphene.String()
|
||||||
|
locationCountry = graphene.String()
|
||||||
|
locationCountryCode = graphene.String()
|
||||||
|
locationLatitude = graphene.Float()
|
||||||
|
locationLongitude = graphene.Float()
|
||||||
|
quantity = graphene.Decimal(required=True)
|
||||||
|
unit = graphene.String()
|
||||||
|
pricePerUnit = graphene.Decimal()
|
||||||
|
currency = graphene.String()
|
||||||
|
description = graphene.String()
|
||||||
|
validUntil = graphene.Date()
|
||||||
|
terminusSchemaId = graphene.String()
|
||||||
|
terminusDocumentId = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateOfferFromWorkflow(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
input = CreateOfferFromWorkflowInput(required=True)
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
message = graphene.String()
|
||||||
|
offer = graphene.Field(OfferType)
|
||||||
|
|
||||||
|
def mutate(self, info, input):
|
||||||
|
try:
|
||||||
|
offer = Offer.objects.filter(uuid=input.offerUuid).first()
|
||||||
|
if offer:
|
||||||
|
logger.info("Offer %s already exists, returning existing", input.offerUuid)
|
||||||
|
return CreateOfferFromWorkflow(success=True, message="Offer exists", offer=offer)
|
||||||
|
|
||||||
|
offer = Offer.objects.create(
|
||||||
|
uuid=input.offerUuid,
|
||||||
|
team_uuid=input.teamUuid,
|
||||||
|
product_uuid=input.productUuid,
|
||||||
|
product_name=input.productName,
|
||||||
|
category_name=input.categoryName or '',
|
||||||
|
location_uuid=input.locationUuid or '',
|
||||||
|
location_name=input.locationName or '',
|
||||||
|
location_country=input.locationCountry or '',
|
||||||
|
location_country_code=input.locationCountryCode or '',
|
||||||
|
location_latitude=input.locationLatitude,
|
||||||
|
location_longitude=input.locationLongitude,
|
||||||
|
quantity=input.quantity,
|
||||||
|
unit=input.unit or 'ton',
|
||||||
|
price_per_unit=input.pricePerUnit,
|
||||||
|
currency=input.currency or 'USD',
|
||||||
|
description=input.description or '',
|
||||||
|
valid_until=input.validUntil,
|
||||||
|
terminus_schema_id=input.terminusSchemaId or '',
|
||||||
|
terminus_document_id=input.terminusDocumentId or '',
|
||||||
|
workflow_status='pending',
|
||||||
|
)
|
||||||
|
logger.info("Created offer %s via workflow", offer.uuid)
|
||||||
|
return CreateOfferFromWorkflow(success=True, message="Offer created", offer=offer)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Failed to create offer %s", input.offerUuid)
|
||||||
|
return CreateOfferFromWorkflow(success=False, message=str(exc), offer=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateOfferWorkflowStatusInput(graphene.InputObjectType):
|
||||||
|
offerUuid = graphene.String(required=True)
|
||||||
|
status = graphene.String(required=True) # pending | active | error
|
||||||
|
errorMessage = graphene.String()
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateOfferWorkflowStatus(graphene.Mutation):
|
||||||
|
class Arguments:
|
||||||
|
input = UpdateOfferWorkflowStatusInput(required=True)
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
message = graphene.String()
|
||||||
|
offer = graphene.Field(OfferType)
|
||||||
|
|
||||||
|
def mutate(self, info, input):
|
||||||
|
try:
|
||||||
|
offer = Offer.objects.get(uuid=input.offerUuid)
|
||||||
|
offer.workflow_status = input.status
|
||||||
|
if input.errorMessage is not None:
|
||||||
|
offer.workflow_error = input.errorMessage
|
||||||
|
offer.save(update_fields=["workflow_status", "workflow_error", "updated_at"])
|
||||||
|
logger.info("Offer %s workflow_status updated to %s", input.offerUuid, input.status)
|
||||||
|
return UpdateOfferWorkflowStatus(success=True, message="Status updated", offer=offer)
|
||||||
|
except Offer.DoesNotExist:
|
||||||
|
return UpdateOfferWorkflowStatus(success=False, message="Offer not found", offer=None)
|
||||||
|
|
||||||
|
|
||||||
|
class M2MMutation(graphene.ObjectType):
|
||||||
|
createOfferFromWorkflow = CreateOfferFromWorkflow.Field()
|
||||||
|
updateOfferWorkflowStatus = UpdateOfferWorkflowStatus.Field()
|
||||||
|
|
||||||
|
|
||||||
|
m2m_schema = graphene.Schema(query=M2MQuery, mutation=M2MMutation)
|
||||||
@@ -46,6 +46,8 @@ class OfferInput(graphene.InputObjectType):
|
|||||||
# Прочее
|
# Прочее
|
||||||
description = graphene.String()
|
description = graphene.String()
|
||||||
valid_until = graphene.Date()
|
valid_until = graphene.Date()
|
||||||
|
terminus_schema_id = graphene.String()
|
||||||
|
terminus_payload = graphene.JSONString()
|
||||||
|
|
||||||
|
|
||||||
class TeamQuery(graphene.ObjectType):
|
class TeamQuery(graphene.ObjectType):
|
||||||
@@ -93,35 +95,46 @@ class CreateOffer(graphene.Mutation):
|
|||||||
class Arguments:
|
class Arguments:
|
||||||
input = OfferInput(required=True)
|
input = OfferInput(required=True)
|
||||||
|
|
||||||
offer = graphene.Field(OfferType)
|
success = graphene.Boolean()
|
||||||
|
message = graphene.String()
|
||||||
|
workflowId = graphene.String()
|
||||||
|
offerUuid = graphene.String()
|
||||||
|
|
||||||
@require_scopes("teams:member")
|
@require_scopes("teams:member")
|
||||||
def mutate(self, info, input):
|
def mutate(self, info, input):
|
||||||
offer = Offer(
|
from ..temporal_client import start_offer_workflow
|
||||||
uuid=str(uuid_lib.uuid4()),
|
|
||||||
team_uuid=input.team_uuid,
|
try:
|
||||||
# Товар
|
offer_uuid = str(uuid_lib.uuid4())
|
||||||
product_uuid=input.product_uuid,
|
workflow_id, _ = start_offer_workflow(
|
||||||
product_name=input.product_name,
|
offer_uuid=offer_uuid,
|
||||||
category_name=input.category_name or '',
|
team_uuid=input.team_uuid,
|
||||||
# Локация
|
product_uuid=input.product_uuid,
|
||||||
location_uuid=input.location_uuid or '',
|
product_name=input.product_name,
|
||||||
location_name=input.location_name or '',
|
category_name=input.category_name,
|
||||||
location_country=input.location_country or '',
|
location_uuid=input.location_uuid,
|
||||||
location_country_code=input.location_country_code or '',
|
location_name=input.location_name,
|
||||||
location_latitude=input.location_latitude,
|
location_country=input.location_country,
|
||||||
location_longitude=input.location_longitude,
|
location_country_code=input.location_country_code,
|
||||||
# Цена и количество
|
location_latitude=input.location_latitude,
|
||||||
quantity=input.quantity,
|
location_longitude=input.location_longitude,
|
||||||
unit=input.unit or 'ton',
|
quantity=input.quantity,
|
||||||
price_per_unit=input.price_per_unit,
|
unit=input.unit,
|
||||||
currency=input.currency or 'USD',
|
price_per_unit=input.price_per_unit,
|
||||||
# Прочее
|
currency=input.currency,
|
||||||
description=input.description or '',
|
description=input.description,
|
||||||
valid_until=input.valid_until,
|
valid_until=input.valid_until,
|
||||||
)
|
terminus_schema_id=getattr(input, "terminus_schema_id", None),
|
||||||
offer.save()
|
terminus_payload=getattr(input, "terminus_payload", None),
|
||||||
return CreateOffer(offer=offer)
|
)
|
||||||
|
return CreateOffer(
|
||||||
|
success=True,
|
||||||
|
message="Offer workflow started",
|
||||||
|
workflowId=workflow_id,
|
||||||
|
offerUuid=offer_uuid,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return CreateOffer(success=False, message=str(exc), workflowId=None, offerUuid=None)
|
||||||
|
|
||||||
|
|
||||||
class UpdateOffer(graphene.Mutation):
|
class UpdateOffer(graphene.Mutation):
|
||||||
@@ -154,6 +167,8 @@ class UpdateOffer(graphene.Mutation):
|
|||||||
offer.currency = input.currency or 'USD'
|
offer.currency = input.currency or 'USD'
|
||||||
offer.description = input.description or ''
|
offer.description = input.description or ''
|
||||||
offer.valid_until = input.valid_until
|
offer.valid_until = input.valid_until
|
||||||
|
if input.terminus_schema_id is not None:
|
||||||
|
offer.terminus_schema_id = input.terminus_schema_id
|
||||||
offer.save()
|
offer.save()
|
||||||
|
|
||||||
return UpdateOffer(offer=offer)
|
return UpdateOffer(offer=offer)
|
||||||
|
|||||||
79
backends/exchange/exchange/temporal_client.py
Normal file
79
backends/exchange/exchange/temporal_client.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from temporalio.client import Client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TEMPORAL_ADDRESS = os.getenv("TEMPORAL_ADDRESS", "temporal:7233")
|
||||||
|
TEMPORAL_NAMESPACE = os.getenv("TEMPORAL_NAMESPACE", "default")
|
||||||
|
TEMPORAL_TASK_QUEUE = os.getenv("TEMPORAL_TASK_QUEUE", "platform-worker")
|
||||||
|
|
||||||
|
|
||||||
|
async def _start_offer_workflow_async(payload: dict) -> Tuple[str, str]:
|
||||||
|
client = await Client.connect(TEMPORAL_ADDRESS, namespace=TEMPORAL_NAMESPACE)
|
||||||
|
|
||||||
|
workflow_id = f"offer-{payload['offer_uuid']}"
|
||||||
|
|
||||||
|
handle = await client.start_workflow(
|
||||||
|
"create_offer",
|
||||||
|
payload,
|
||||||
|
id=workflow_id,
|
||||||
|
task_queue=TEMPORAL_TASK_QUEUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Started offer workflow %s", workflow_id)
|
||||||
|
return handle.id, handle.result_run_id
|
||||||
|
|
||||||
|
|
||||||
|
def start_offer_workflow(
|
||||||
|
*,
|
||||||
|
offer_uuid: str,
|
||||||
|
team_uuid: str,
|
||||||
|
product_uuid: str,
|
||||||
|
product_name: str,
|
||||||
|
category_name: str | None = None,
|
||||||
|
location_uuid: str | None = None,
|
||||||
|
location_name: str | None = None,
|
||||||
|
location_country: str | None = None,
|
||||||
|
location_country_code: str | None = None,
|
||||||
|
location_latitude: float | None = None,
|
||||||
|
location_longitude: float | None = None,
|
||||||
|
quantity=None,
|
||||||
|
unit: str | None = None,
|
||||||
|
price_per_unit=None,
|
||||||
|
currency: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
valid_until=None,
|
||||||
|
terminus_schema_id: str | None = None,
|
||||||
|
terminus_payload: dict | None = None,
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
payload = {
|
||||||
|
"offer_uuid": offer_uuid,
|
||||||
|
"team_uuid": team_uuid,
|
||||||
|
"product_uuid": product_uuid,
|
||||||
|
"product_name": product_name,
|
||||||
|
"category_name": category_name,
|
||||||
|
"location_uuid": location_uuid,
|
||||||
|
"location_name": location_name,
|
||||||
|
"location_country": location_country,
|
||||||
|
"location_country_code": location_country_code,
|
||||||
|
"location_latitude": location_latitude,
|
||||||
|
"location_longitude": location_longitude,
|
||||||
|
"quantity": str(quantity) if quantity is not None else None,
|
||||||
|
"unit": unit,
|
||||||
|
"price_per_unit": str(price_per_unit) if price_per_unit is not None else None,
|
||||||
|
"currency": currency,
|
||||||
|
"description": description,
|
||||||
|
"valid_until": valid_until.isoformat() if hasattr(valid_until, "isoformat") else valid_until,
|
||||||
|
"terminus_schema_id": terminus_schema_id,
|
||||||
|
"terminus_payload": terminus_payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return asyncio.run(_start_offer_workflow_async(payload))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to start offer workflow %s", offer_uuid)
|
||||||
|
raise
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from .views import PublicGraphQLView, UserGraphQLView, TeamGraphQLView
|
from .views import PublicGraphQLView, UserGraphQLView, TeamGraphQLView, M2MGraphQLView
|
||||||
from .schemas.public_schema import public_schema
|
from .schemas.public_schema import public_schema
|
||||||
from .schemas.user_schema import user_schema
|
from .schemas.user_schema import user_schema
|
||||||
from .schemas.team_schema import team_schema
|
from .schemas.team_schema import team_schema
|
||||||
|
from .schemas.m2m_schema import m2m_schema
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('graphql/public/', csrf_exempt(PublicGraphQLView.as_view(graphiql=True, schema=public_schema))),
|
path('graphql/public/', csrf_exempt(PublicGraphQLView.as_view(graphiql=True, schema=public_schema))),
|
||||||
path('graphql/user/', csrf_exempt(UserGraphQLView.as_view(graphiql=True, schema=user_schema))),
|
path('graphql/user/', csrf_exempt(UserGraphQLView.as_view(graphiql=True, schema=user_schema))),
|
||||||
path('graphql/team/', csrf_exempt(TeamGraphQLView.as_view(graphiql=True, schema=team_schema))),
|
path('graphql/team/', csrf_exempt(TeamGraphQLView.as_view(graphiql=True, schema=team_schema))),
|
||||||
|
path('graphql/m2m/', csrf_exempt(M2MGraphQLView.as_view(graphiql=True, schema=m2m_schema))),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,3 +19,8 @@ class UserGraphQLView(GraphQLView):
|
|||||||
class TeamGraphQLView(GraphQLView):
|
class TeamGraphQLView(GraphQLView):
|
||||||
"""Team endpoint - requires Organization Access Token."""
|
"""Team endpoint - requires Organization Access Token."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class M2MGraphQLView(GraphQLView):
|
||||||
|
"""M2M endpoint - internal services only."""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.9 on 2025-12-30 03:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('offers', '0003_offer_workflow_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='offer',
|
||||||
|
name='terminus_document_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='offer',
|
||||||
|
name='terminus_schema_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='offer',
|
||||||
|
name='workflow_error',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -24,6 +24,7 @@ class Offer(models.Model):
|
|||||||
choices=WORKFLOW_STATUS_CHOICES,
|
choices=WORKFLOW_STATUS_CHOICES,
|
||||||
default='pending',
|
default='pending',
|
||||||
)
|
)
|
||||||
|
workflow_error = models.TextField(blank=True, default='')
|
||||||
|
|
||||||
# Локация отгрузки
|
# Локация отгрузки
|
||||||
location_uuid = models.CharField(max_length=100, blank=True, default='')
|
location_uuid = models.CharField(max_length=100, blank=True, default='')
|
||||||
@@ -46,6 +47,8 @@ class Offer(models.Model):
|
|||||||
|
|
||||||
# Описание (опционально)
|
# Описание (опционально)
|
||||||
description = models.TextField(blank=True, default='')
|
description = models.TextField(blank=True, default='')
|
||||||
|
terminus_schema_id = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
terminus_document_id = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
|
||||||
# Срок действия
|
# Срок действия
|
||||||
valid_until = models.DateField(null=True, blank=True)
|
valid_until = models.DateField(null=True, blank=True)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from .odoo import add_kyc_to_odoo, set_buyer_logto_org
|
from .odoo import add_kyc_to_odoo, set_buyer_logto_org
|
||||||
from .teams import set_logto_org_id
|
from .teams import set_logto_org_id
|
||||||
|
from .exchange import create_offer_in_exchange, update_offer_workflow_status
|
||||||
|
from .terminus import save_offer_description
|
||||||
|
from .offers import trigger_prefect_offer_sync
|
||||||
from .logto import create_logto_org
|
from .logto import create_logto_org
|
||||||
from .address import (
|
from .address import (
|
||||||
create_address_in_django,
|
create_address_in_django,
|
||||||
@@ -14,6 +17,10 @@ __all__ = [
|
|||||||
"set_buyer_logto_org",
|
"set_buyer_logto_org",
|
||||||
"set_logto_org_id",
|
"set_logto_org_id",
|
||||||
"create_logto_org",
|
"create_logto_org",
|
||||||
|
"create_offer_in_exchange",
|
||||||
|
"update_offer_workflow_status",
|
||||||
|
"save_offer_description",
|
||||||
|
"trigger_prefect_offer_sync",
|
||||||
"create_address_in_django",
|
"create_address_in_django",
|
||||||
"update_address_status",
|
"update_address_status",
|
||||||
"trigger_prefect_sync",
|
"trigger_prefect_sync",
|
||||||
|
|||||||
113
temporal/platform_worker/platform_worker/activities/exchange.py
Normal file
113
temporal/platform_worker/platform_worker/activities/exchange.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"""Activities for Exchange service via GraphQL M2M endpoint."""
|
||||||
|
import logging
|
||||||
|
import httpx
|
||||||
|
from temporalio import activity
|
||||||
|
|
||||||
|
from optovia_workflows.types import (
|
||||||
|
CreateOfferInExchangeInput,
|
||||||
|
CreateOfferInExchangeResult,
|
||||||
|
UpdateOfferWorkflowStatusInput,
|
||||||
|
UpdateOfferWorkflowStatusResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CREATE_OFFER_MUTATION = """
|
||||||
|
mutation CreateOfferFromWorkflow($input: CreateOfferFromWorkflowInput!) {
|
||||||
|
createOfferFromWorkflow(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
offer {
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
UPDATE_OFFER_STATUS_MUTATION = """
|
||||||
|
mutation UpdateOfferWorkflowStatus($input: UpdateOfferWorkflowStatusInput!) {
|
||||||
|
updateOfferWorkflowStatus(input: $input) {
|
||||||
|
success
|
||||||
|
message
|
||||||
|
offer {
|
||||||
|
uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="create_offer_in_exchange")
|
||||||
|
def create_offer_in_exchange(data: CreateOfferInExchangeInput) -> CreateOfferInExchangeResult:
|
||||||
|
url = f"{settings.EXCHANGE_API_URL}/graphql/m2m/"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"offerUuid": data.offer_uuid,
|
||||||
|
"teamUuid": data.team_uuid,
|
||||||
|
"productUuid": data.product_uuid,
|
||||||
|
"productName": data.product_name,
|
||||||
|
"categoryName": data.category_name,
|
||||||
|
"locationUuid": data.location_uuid,
|
||||||
|
"locationName": data.location_name,
|
||||||
|
"locationCountry": data.location_country,
|
||||||
|
"locationCountryCode": data.location_country_code,
|
||||||
|
"locationLatitude": data.location_latitude,
|
||||||
|
"locationLongitude": data.location_longitude,
|
||||||
|
"quantity": data.quantity,
|
||||||
|
"unit": data.unit,
|
||||||
|
"pricePerUnit": data.price_per_unit,
|
||||||
|
"currency": data.currency,
|
||||||
|
"description": data.description,
|
||||||
|
"validUntil": data.valid_until,
|
||||||
|
"terminusSchemaId": data.terminus_schema_id,
|
||||||
|
"terminusDocumentId": data.terminus_document_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
with httpx.Client(timeout=settings.EXCHANGE_TIMEOUT) as client:
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
json={"query": CREATE_OFFER_MUTATION, "variables": {"input": payload}},
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
if "errors" in result:
|
||||||
|
return CreateOfferInExchangeResult(success=False, offer_uuid=data.offer_uuid, message=str(result["errors"]))
|
||||||
|
mutation_result = result.get("data", {}).get("createOfferFromWorkflow", {})
|
||||||
|
if not mutation_result.get("success"):
|
||||||
|
return CreateOfferInExchangeResult(
|
||||||
|
success=False,
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
message=mutation_result.get("message", "Unknown error"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CreateOfferInExchangeResult(success=True, offer_uuid=data.offer_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="update_offer_workflow_status")
|
||||||
|
def update_offer_workflow_status(data: UpdateOfferWorkflowStatusInput) -> UpdateOfferWorkflowStatusResult:
|
||||||
|
url = f"{settings.EXCHANGE_API_URL}/graphql/m2m/"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"offerUuid": data.offer_uuid,
|
||||||
|
"status": data.status,
|
||||||
|
"errorMessage": data.error_message,
|
||||||
|
}
|
||||||
|
|
||||||
|
with httpx.Client(timeout=settings.EXCHANGE_TIMEOUT) as client:
|
||||||
|
response = client.post(
|
||||||
|
url,
|
||||||
|
json={"query": UPDATE_OFFER_STATUS_MUTATION, "variables": {"input": payload}},
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
if "errors" in result:
|
||||||
|
return UpdateOfferWorkflowStatusResult(success=False, message=str(result["errors"]))
|
||||||
|
mutation_result = result.get("data", {}).get("updateOfferWorkflowStatus", {})
|
||||||
|
if not mutation_result.get("success"):
|
||||||
|
return UpdateOfferWorkflowStatusResult(success=False, message=mutation_result.get("message", "Unknown error"))
|
||||||
|
|
||||||
|
return UpdateOfferWorkflowStatusResult(success=True, message="OK")
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Activities to sync offers into graph via Prefect."""
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import httpx
|
||||||
|
from temporalio import activity
|
||||||
|
|
||||||
|
from optovia_workflows.types import TriggerPrefectOfferSyncInput, TriggerPrefectOfferSyncResult
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="trigger_prefect_offer_sync")
|
||||||
|
def trigger_prefect_offer_sync(data: TriggerPrefectOfferSyncInput) -> TriggerPrefectOfferSyncResult:
|
||||||
|
"""
|
||||||
|
Trigger Prefect sync-offer flow to create offer node and edge to location.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deployment_url = f"{settings.PREFECT_API_URL}/deployments/name/sync-offer/sync-offer-deployment"
|
||||||
|
|
||||||
|
with httpx.Client(timeout=30) as client:
|
||||||
|
response = client.get(deployment_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
deployment_id = response.json()["id"]
|
||||||
|
|
||||||
|
node_data = {
|
||||||
|
"uuid": data.offer_uuid,
|
||||||
|
"node_type": "offer",
|
||||||
|
"location_uuid": data.location_uuid,
|
||||||
|
"location_latitude": data.location_latitude,
|
||||||
|
"location_longitude": data.location_longitude,
|
||||||
|
"team_uuid": data.team_uuid,
|
||||||
|
"product_uuid": data.product_uuid,
|
||||||
|
"price_per_unit": data.price_per_unit,
|
||||||
|
"currency": data.currency,
|
||||||
|
"quantity": data.quantity,
|
||||||
|
"unit": data.unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
run_url = f"{settings.PREFECT_API_URL}/deployments/{deployment_id}/create_flow_run"
|
||||||
|
response = client.post(run_url, json={"parameters": {"offer_data": node_data}})
|
||||||
|
response.raise_for_status()
|
||||||
|
flow_run_id = response.json()["id"]
|
||||||
|
|
||||||
|
logger.info("Created Prefect offer flow run %s", flow_run_id)
|
||||||
|
|
||||||
|
poll_interval = 60
|
||||||
|
max_polls = 20
|
||||||
|
polls = 0
|
||||||
|
|
||||||
|
while polls < max_polls:
|
||||||
|
with httpx.Client(timeout=30) as client:
|
||||||
|
flow_run_url = f"{settings.PREFECT_API_URL}/flow_runs/{flow_run_id}"
|
||||||
|
response = client.get(flow_run_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
flow_run = response.json()
|
||||||
|
|
||||||
|
state_type = flow_run.get("state", {}).get("type")
|
||||||
|
state_name = flow_run.get("state", {}).get("name", "")
|
||||||
|
if state_type == "COMPLETED":
|
||||||
|
return TriggerPrefectOfferSyncResult(success=True, flow_run_id=flow_run_id)
|
||||||
|
if state_type in ("FAILED", "CANCELLED", "CRASHED"):
|
||||||
|
return TriggerPrefectOfferSyncResult(
|
||||||
|
success=False,
|
||||||
|
flow_run_id=flow_run_id,
|
||||||
|
message=f"Flow run {state_type}: {state_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
polls += 1
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
return TriggerPrefectOfferSyncResult(
|
||||||
|
success=False,
|
||||||
|
flow_run_id=flow_run_id,
|
||||||
|
message=f"Timeout waiting for flow run {flow_run_id}",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Failed to trigger Prefect offer sync")
|
||||||
|
return TriggerPrefectOfferSyncResult(success=False, message=str(exc))
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Activities for TerminusDB document storage."""
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import httpx
|
||||||
|
from temporalio import activity
|
||||||
|
|
||||||
|
from optovia_workflows.types import SaveOfferDescriptionInput, SaveOfferDescriptionResult
|
||||||
|
from ..config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _basic_auth_header() -> str:
|
||||||
|
if settings.TERMINUS_BASIC_USER and settings.TERMINUS_BASIC_PASS:
|
||||||
|
token = f"{settings.TERMINUS_BASIC_USER}:{settings.TERMINUS_BASIC_PASS}".encode("utf-8")
|
||||||
|
return "Basic " + base64.b64encode(token).decode("utf-8")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="save_offer_description")
|
||||||
|
def save_offer_description(data: SaveOfferDescriptionInput) -> SaveOfferDescriptionResult:
|
||||||
|
if not settings.TERMINUS_DOCUMENT_URL:
|
||||||
|
return SaveOfferDescriptionResult(success=False, message="Terminus URL not configured")
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
auth_header = _basic_auth_header()
|
||||||
|
if auth_header:
|
||||||
|
headers["Authorization"] = auth_header
|
||||||
|
|
||||||
|
document = {
|
||||||
|
"@type": data.schema_id,
|
||||||
|
"@id": data.offer_uuid,
|
||||||
|
"offer_uuid": data.offer_uuid,
|
||||||
|
**data.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with httpx.Client(timeout=settings.TERMINUS_TIMEOUT) as client:
|
||||||
|
response = client.post(
|
||||||
|
settings.TERMINUS_DOCUMENT_URL,
|
||||||
|
json=document,
|
||||||
|
headers=headers,
|
||||||
|
params={"graph_type": "instance"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return SaveOfferDescriptionResult(success=True, document_id=data.offer_uuid)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Failed to save offer description to Terminus")
|
||||||
|
return SaveOfferDescriptionResult(success=False, message=str(exc))
|
||||||
@@ -65,6 +65,10 @@ class Settings(BaseSettings):
|
|||||||
TEAMS_API_URL: str = "http://teams:8000"
|
TEAMS_API_URL: str = "http://teams:8000"
|
||||||
TEAMS_TIMEOUT: float = 15.0
|
TEAMS_TIMEOUT: float = 15.0
|
||||||
|
|
||||||
|
# Exchange service
|
||||||
|
EXCHANGE_API_URL: str = "http://exchange:8000"
|
||||||
|
EXCHANGE_TIMEOUT: float = 15.0
|
||||||
|
|
||||||
# Logto
|
# Logto
|
||||||
LOGTO_API_URL: str = "http://logto:3001"
|
LOGTO_API_URL: str = "http://logto:3001"
|
||||||
LOGTO_M2M_APP_ID: str = ""
|
LOGTO_M2M_APP_ID: str = ""
|
||||||
@@ -80,5 +84,11 @@ class Settings(BaseSettings):
|
|||||||
# Prefect (loaded from Infisical /shared)
|
# Prefect (loaded from Infisical /shared)
|
||||||
PREFECT_API_URL: str
|
PREFECT_API_URL: str
|
||||||
|
|
||||||
|
# TerminusDB
|
||||||
|
TERMINUS_DOCUMENT_URL: str = ""
|
||||||
|
TERMINUS_BASIC_USER: str = ""
|
||||||
|
TERMINUS_BASIC_PASS: str = ""
|
||||||
|
TERMINUS_TIMEOUT: float = 15.0
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
from temporalio.client import Client
|
from temporalio.client import Client
|
||||||
from temporalio.worker import Worker
|
from temporalio.worker import Worker
|
||||||
|
|
||||||
from optovia_workflows import KycApplicationWorkflow, AddressWorkflow
|
from optovia_workflows import KycApplicationWorkflow, AddressWorkflow, OfferWorkflow
|
||||||
|
|
||||||
from . import activities
|
from . import activities
|
||||||
from .config import settings
|
from .config import settings
|
||||||
@@ -65,12 +65,16 @@ async def run_worker() -> None:
|
|||||||
worker = Worker(
|
worker = Worker(
|
||||||
client,
|
client,
|
||||||
task_queue=settings.TASK_QUEUE,
|
task_queue=settings.TASK_QUEUE,
|
||||||
workflows=[KycApplicationWorkflow, AddressWorkflow],
|
workflows=[KycApplicationWorkflow, AddressWorkflow, OfferWorkflow],
|
||||||
activities=[
|
activities=[
|
||||||
activities.add_kyc_to_odoo,
|
activities.add_kyc_to_odoo,
|
||||||
activities.set_buyer_logto_org,
|
activities.set_buyer_logto_org,
|
||||||
activities.create_logto_org,
|
activities.create_logto_org,
|
||||||
activities.set_logto_org_id,
|
activities.set_logto_org_id,
|
||||||
|
activities.create_offer_in_exchange,
|
||||||
|
activities.update_offer_workflow_status,
|
||||||
|
activities.save_offer_description,
|
||||||
|
activities.trigger_prefect_offer_sync,
|
||||||
activities.create_address_in_django,
|
activities.create_address_in_django,
|
||||||
activities.update_address_status,
|
activities.update_address_status,
|
||||||
activities.trigger_prefect_sync,
|
activities.trigger_prefect_sync,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from .kyc import KycApplicationWorkflow
|
from .kyc import KycApplicationWorkflow
|
||||||
from .address import AddressWorkflow
|
from .address import AddressWorkflow
|
||||||
|
from .offer import OfferWorkflow
|
||||||
from .types import (
|
from .types import (
|
||||||
KycApprovalData,
|
KycApprovalData,
|
||||||
KycData,
|
KycData,
|
||||||
@@ -9,6 +10,8 @@ from .types import (
|
|||||||
KycWorkflowResult,
|
KycWorkflowResult,
|
||||||
AddressData,
|
AddressData,
|
||||||
AddressWorkflowResult,
|
AddressWorkflowResult,
|
||||||
|
OfferData,
|
||||||
|
OfferWorkflowResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -20,4 +23,7 @@ __all__ = [
|
|||||||
"AddressWorkflow",
|
"AddressWorkflow",
|
||||||
"AddressData",
|
"AddressData",
|
||||||
"AddressWorkflowResult",
|
"AddressWorkflowResult",
|
||||||
|
"OfferWorkflow",
|
||||||
|
"OfferData",
|
||||||
|
"OfferWorkflowResult",
|
||||||
]
|
]
|
||||||
|
|||||||
5
temporal/workflows/optovia_workflows/offer/__init__.py
Normal file
5
temporal/workflows/optovia_workflows/offer/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Offer workflow module."""
|
||||||
|
|
||||||
|
from .workflow import OfferWorkflow
|
||||||
|
|
||||||
|
__all__ = ["OfferWorkflow"]
|
||||||
174
temporal/workflows/optovia_workflows/offer/workflow.py
Normal file
174
temporal/workflows/optovia_workflows/offer/workflow.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Offer Workflow.
|
||||||
|
|
||||||
|
Creates an offer in Exchange, stores description in Terminus, and
|
||||||
|
syncs offer node/edge in graph via Prefect.
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from temporalio import workflow
|
||||||
|
from temporalio.common import RetryPolicy
|
||||||
|
|
||||||
|
with workflow.unsafe.imports_passed_through():
|
||||||
|
from ..types import (
|
||||||
|
OfferData,
|
||||||
|
OfferWorkflowResult,
|
||||||
|
CreateOfferInExchangeInput,
|
||||||
|
CreateOfferInExchangeResult,
|
||||||
|
SaveOfferDescriptionInput,
|
||||||
|
SaveOfferDescriptionResult,
|
||||||
|
TriggerPrefectOfferSyncInput,
|
||||||
|
TriggerPrefectOfferSyncResult,
|
||||||
|
UpdateOfferWorkflowStatusInput,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
EXTERNAL_API_RETRY = RetryPolicy(
|
||||||
|
maximum_attempts=5,
|
||||||
|
initial_interval=timedelta(seconds=2),
|
||||||
|
backoff_coefficient=2.0,
|
||||||
|
maximum_interval=timedelta(seconds=30),
|
||||||
|
)
|
||||||
|
|
||||||
|
INTERNAL_API_RETRY = RetryPolicy(
|
||||||
|
maximum_attempts=3,
|
||||||
|
initial_interval=timedelta(seconds=1),
|
||||||
|
backoff_coefficient=2.0,
|
||||||
|
maximum_interval=timedelta(seconds=10),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@workflow.defn(name="create_offer")
|
||||||
|
class OfferWorkflow:
|
||||||
|
@workflow.run
|
||||||
|
async def run(self, data: OfferData) -> OfferWorkflowResult:
|
||||||
|
workflow.logger.info("Starting offer workflow %s", data.offer_uuid)
|
||||||
|
|
||||||
|
create_result: CreateOfferInExchangeResult = await workflow.execute_activity(
|
||||||
|
"create_offer_in_exchange",
|
||||||
|
CreateOfferInExchangeInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
team_uuid=data.team_uuid,
|
||||||
|
product_uuid=data.product_uuid,
|
||||||
|
product_name=data.product_name,
|
||||||
|
category_name=data.category_name,
|
||||||
|
location_uuid=data.location_uuid,
|
||||||
|
location_name=data.location_name,
|
||||||
|
location_country=data.location_country,
|
||||||
|
location_country_code=data.location_country_code,
|
||||||
|
location_latitude=data.location_latitude,
|
||||||
|
location_longitude=data.location_longitude,
|
||||||
|
quantity=data.quantity,
|
||||||
|
unit=data.unit,
|
||||||
|
price_per_unit=data.price_per_unit,
|
||||||
|
currency=data.currency,
|
||||||
|
description=data.description,
|
||||||
|
valid_until=data.valid_until,
|
||||||
|
terminus_schema_id=data.terminus_schema_id,
|
||||||
|
terminus_document_id=data.offer_uuid,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=INTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = create_result.success if hasattr(create_result, "success") else create_result["success"]
|
||||||
|
if not success:
|
||||||
|
message = create_result.message if hasattr(create_result, "message") else create_result.get("message", "")
|
||||||
|
workflow.logger.error("Failed to create offer in Exchange: %s", message)
|
||||||
|
return OfferWorkflowResult(status="failed", offer_uuid=data.offer_uuid, message=message)
|
||||||
|
|
||||||
|
# Save description to Terminus
|
||||||
|
if data.terminus_schema_id and data.terminus_payload:
|
||||||
|
save_result: SaveOfferDescriptionResult = await workflow.execute_activity(
|
||||||
|
"save_offer_description",
|
||||||
|
SaveOfferDescriptionInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
schema_id=data.terminus_schema_id,
|
||||||
|
payload=data.terminus_payload,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=EXTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
save_success = save_result.success if hasattr(save_result, "success") else save_result.get("success", False)
|
||||||
|
if not save_success:
|
||||||
|
message = save_result.message if hasattr(save_result, "message") else save_result.get("message", "")
|
||||||
|
await workflow.execute_activity(
|
||||||
|
"update_offer_workflow_status",
|
||||||
|
UpdateOfferWorkflowStatusInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
status="error",
|
||||||
|
error_message=message,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=INTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
return OfferWorkflowResult(status="failed", offer_uuid=data.offer_uuid, message=message)
|
||||||
|
else:
|
||||||
|
workflow.logger.warning("No Terminus payload for offer %s", data.offer_uuid)
|
||||||
|
|
||||||
|
# Sync offer node + edge in graph
|
||||||
|
if data.location_uuid and data.location_latitude is not None and data.location_longitude is not None:
|
||||||
|
sync_result: TriggerPrefectOfferSyncResult = await workflow.execute_activity(
|
||||||
|
"trigger_prefect_offer_sync",
|
||||||
|
TriggerPrefectOfferSyncInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
location_uuid=data.location_uuid,
|
||||||
|
location_latitude=data.location_latitude,
|
||||||
|
location_longitude=data.location_longitude,
|
||||||
|
team_uuid=data.team_uuid,
|
||||||
|
product_uuid=data.product_uuid,
|
||||||
|
price_per_unit=data.price_per_unit,
|
||||||
|
currency=data.currency,
|
||||||
|
quantity=data.quantity,
|
||||||
|
unit=data.unit,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(minutes=20),
|
||||||
|
start_to_close_timeout=timedelta(minutes=20),
|
||||||
|
retry_policy=EXTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
sync_success = sync_result.success if hasattr(sync_result, "success") else sync_result.get("success", False)
|
||||||
|
if not sync_success:
|
||||||
|
message = sync_result.message if hasattr(sync_result, "message") else sync_result.get("message", "")
|
||||||
|
await workflow.execute_activity(
|
||||||
|
"update_offer_workflow_status",
|
||||||
|
UpdateOfferWorkflowStatusInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
status="error",
|
||||||
|
error_message=message,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=INTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
return OfferWorkflowResult(status="failed", offer_uuid=data.offer_uuid, message=message)
|
||||||
|
else:
|
||||||
|
message = "Offer location coordinates are missing"
|
||||||
|
await workflow.execute_activity(
|
||||||
|
"update_offer_workflow_status",
|
||||||
|
UpdateOfferWorkflowStatusInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
status="error",
|
||||||
|
error_message=message,
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=INTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
return OfferWorkflowResult(status="failed", offer_uuid=data.offer_uuid, message=message)
|
||||||
|
|
||||||
|
await workflow.execute_activity(
|
||||||
|
"update_offer_workflow_status",
|
||||||
|
UpdateOfferWorkflowStatusInput(
|
||||||
|
offer_uuid=data.offer_uuid,
|
||||||
|
status="active",
|
||||||
|
),
|
||||||
|
schedule_to_close_timeout=timedelta(seconds=30),
|
||||||
|
start_to_close_timeout=timedelta(seconds=15),
|
||||||
|
retry_policy=INTERNAL_API_RETRY,
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.logger.info("Offer workflow completed: %s", data.offer_uuid)
|
||||||
|
return OfferWorkflowResult(status="completed", offer_uuid=data.offer_uuid)
|
||||||
@@ -123,6 +123,139 @@ class SetLogtoOrgIdResult:
|
|||||||
updated: bool
|
updated: bool
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Offer Workflow Types
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OfferData:
|
||||||
|
"""Input data for offer creation workflow."""
|
||||||
|
|
||||||
|
offer_uuid: str
|
||||||
|
team_uuid: str
|
||||||
|
product_uuid: str
|
||||||
|
product_name: str
|
||||||
|
category_name: str | None = None
|
||||||
|
location_uuid: str | None = None
|
||||||
|
location_name: str | None = None
|
||||||
|
location_country: str | None = None
|
||||||
|
location_country_code: str | None = None
|
||||||
|
location_latitude: float | None = None
|
||||||
|
location_longitude: float | None = None
|
||||||
|
quantity: str | None = None
|
||||||
|
unit: str | None = None
|
||||||
|
price_per_unit: str | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
valid_until: str | None = None
|
||||||
|
terminus_schema_id: str | None = None
|
||||||
|
terminus_payload: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OfferWorkflowResult:
|
||||||
|
"""Output of offer workflow."""
|
||||||
|
|
||||||
|
status: str # completed | failed
|
||||||
|
offer_uuid: str
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CreateOfferInExchangeInput:
|
||||||
|
"""Input for create_offer_in_exchange activity."""
|
||||||
|
|
||||||
|
offer_uuid: str
|
||||||
|
team_uuid: str
|
||||||
|
product_uuid: str
|
||||||
|
product_name: str
|
||||||
|
category_name: str | None = None
|
||||||
|
location_uuid: str | None = None
|
||||||
|
location_name: str | None = None
|
||||||
|
location_country: str | None = None
|
||||||
|
location_country_code: str | None = None
|
||||||
|
location_latitude: float | None = None
|
||||||
|
location_longitude: float | None = None
|
||||||
|
quantity: str | None = None
|
||||||
|
unit: str | None = None
|
||||||
|
price_per_unit: str | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
valid_until: str | None = None
|
||||||
|
terminus_schema_id: str | None = None
|
||||||
|
terminus_document_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CreateOfferInExchangeResult:
|
||||||
|
"""Result of create_offer_in_exchange activity."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
offer_uuid: str
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UpdateOfferWorkflowStatusInput:
|
||||||
|
"""Input for update_offer_workflow_status activity."""
|
||||||
|
|
||||||
|
offer_uuid: str
|
||||||
|
status: str # pending | active | error
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UpdateOfferWorkflowStatusResult:
|
||||||
|
"""Result of update_offer_workflow_status activity."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SaveOfferDescriptionInput:
|
||||||
|
"""Input for save_offer_description activity."""
|
||||||
|
|
||||||
|
offer_uuid: str
|
||||||
|
schema_id: str
|
||||||
|
payload: dict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SaveOfferDescriptionResult:
|
||||||
|
"""Result of save_offer_description activity."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
document_id: str = ""
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TriggerPrefectOfferSyncInput:
|
||||||
|
"""Input for trigger_prefect_offer_sync activity."""
|
||||||
|
|
||||||
|
offer_uuid: str
|
||||||
|
location_uuid: str
|
||||||
|
location_latitude: float
|
||||||
|
location_longitude: float
|
||||||
|
team_uuid: str
|
||||||
|
product_uuid: str
|
||||||
|
price_per_unit: str | None = None
|
||||||
|
currency: str | None = None
|
||||||
|
quantity: str | None = None
|
||||||
|
unit: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TriggerPrefectOfferSyncResult:
|
||||||
|
"""Result of trigger_prefect_offer_sync activity."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
flow_run_id: str = ""
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# KYC Django Activity Types
|
# KYC Django Activity Types
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -16,11 +16,15 @@ export type Scalars = {
|
|||||||
Date: { input: any; output: any; }
|
Date: { input: any; output: any; }
|
||||||
DateTime: { input: string; output: string; }
|
DateTime: { input: string; output: string; }
|
||||||
Decimal: { input: any; output: any; }
|
Decimal: { input: any; output: any; }
|
||||||
|
JSONString: { input: any; output: any; }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateOffer = {
|
export type CreateOffer = {
|
||||||
__typename?: 'CreateOffer';
|
__typename?: 'CreateOffer';
|
||||||
offer?: Maybe<OfferType>;
|
message?: Maybe<Scalars['String']['output']>;
|
||||||
|
offerUuid?: Maybe<Scalars['String']['output']>;
|
||||||
|
success?: Maybe<Scalars['Boolean']['output']>;
|
||||||
|
workflowId?: Maybe<Scalars['String']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateRequest = {
|
export type CreateRequest = {
|
||||||
@@ -48,6 +52,8 @@ export type OfferInput = {
|
|||||||
productUuid: Scalars['String']['input'];
|
productUuid: Scalars['String']['input'];
|
||||||
quantity: Scalars['Decimal']['input'];
|
quantity: Scalars['Decimal']['input'];
|
||||||
teamUuid: Scalars['String']['input'];
|
teamUuid: Scalars['String']['input'];
|
||||||
|
terminusPayload?: InputMaybe<Scalars['JSONString']['input']>;
|
||||||
|
terminusSchemaId?: InputMaybe<Scalars['String']['input']>;
|
||||||
unit?: InputMaybe<Scalars['String']['input']>;
|
unit?: InputMaybe<Scalars['String']['input']>;
|
||||||
validUntil?: InputMaybe<Scalars['Date']['input']>;
|
validUntil?: InputMaybe<Scalars['Date']['input']>;
|
||||||
};
|
};
|
||||||
@@ -189,7 +195,7 @@ export type CreateOfferMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type CreateOfferMutation = { __typename?: 'TeamMutation', createOffer?: { __typename?: 'CreateOffer', offer?: { __typename?: 'OfferType', uuid: string, teamUuid: string, status: OffersOfferStatusChoices, locationUuid: string, locationName: string, locationCountry: string, locationCountryCode: string, locationLatitude?: number | null, locationLongitude?: number | null, productUuid: string, productName: string, categoryName: string, quantity: any, unit: string, pricePerUnit?: any | null, currency: string, description: string, validUntil?: any | null, createdAt: string, updatedAt: string } | null } | null };
|
export type CreateOfferMutation = { __typename?: 'TeamMutation', createOffer?: { __typename?: 'CreateOffer', success?: boolean | null, message?: string | null, workflowId?: string | null, offerUuid?: string | null } | null };
|
||||||
|
|
||||||
export type CreateRequestMutationVariables = Exact<{
|
export type CreateRequestMutationVariables = Exact<{
|
||||||
input: RequestInput;
|
input: RequestInput;
|
||||||
@@ -206,6 +212,6 @@ export type GetRequestsQueryVariables = Exact<{
|
|||||||
export type GetRequestsQuery = { __typename?: 'TeamQuery', getRequests?: Array<{ __typename?: 'RequestType', uuid: string, productUuid: string, quantity: any, sourceLocationUuid: string, userId: string } | null> | null };
|
export type GetRequestsQuery = { __typename?: 'TeamQuery', getRequests?: Array<{ __typename?: 'RequestType', uuid: string, productUuid: string, quantity: any, sourceLocationUuid: string, userId: string } | null> | null };
|
||||||
|
|
||||||
|
|
||||||
export const CreateOfferDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOffer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OfferInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOffer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"offer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"teamUuid"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"locationUuid"}},{"kind":"Field","name":{"kind":"Name","value":"locationName"}},{"kind":"Field","name":{"kind":"Name","value":"locationCountry"}},{"kind":"Field","name":{"kind":"Name","value":"locationCountryCode"}},{"kind":"Field","name":{"kind":"Name","value":"locationLatitude"}},{"kind":"Field","name":{"kind":"Name","value":"locationLongitude"}},{"kind":"Field","name":{"kind":"Name","value":"productUuid"}},{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"categoryName"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unit"}},{"kind":"Field","name":{"kind":"Name","value":"pricePerUnit"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"validUntil"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode<CreateOfferMutation, CreateOfferMutationVariables>;
|
export const CreateOfferDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOffer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OfferInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOffer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"workflowId"}},{"kind":"Field","name":{"kind":"Name","value":"offerUuid"}}]}}]}}]} as unknown as DocumentNode<CreateOfferMutation, CreateOfferMutationVariables>;
|
||||||
export const CreateRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RequestInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"request"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"productUuid"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLocationUuid"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]}}]}}]} as unknown as DocumentNode<CreateRequestMutation, CreateRequestMutationVariables>;
|
export const CreateRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RequestInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"request"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"productUuid"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLocationUuid"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]}}]}}]} as unknown as DocumentNode<CreateRequestMutation, CreateRequestMutationVariables>;
|
||||||
export const GetRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRequests"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"productUuid"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLocationUuid"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]}}]} as unknown as DocumentNode<GetRequestsQuery, GetRequestsQueryVariables>;
|
export const GetRequestsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRequests"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getRequests"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uuid"}},{"kind":"Field","name":{"kind":"Name","value":"productUuid"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"sourceLocationUuid"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]}}]} as unknown as DocumentNode<GetRequestsQuery, GetRequestsQueryVariables>;
|
||||||
|
|||||||
@@ -52,6 +52,22 @@
|
|||||||
<Text v-if="schemaDescription" tone="muted">{{ schemaDescription }}</Text>
|
<Text v-if="schemaDescription" tone="muted">{{ schemaDescription }}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="2">
|
||||||
|
<Text weight="semibold">{{ t('clientOfferForm.labels.location') }}</Text>
|
||||||
|
<select v-model="selectedAddressUuid" class="select select-bordered w-full">
|
||||||
|
<option v-if="!addresses.length" :value="null">
|
||||||
|
{{ t('clientOfferForm.labels.location_empty') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="address in addresses"
|
||||||
|
:key="address.uuid"
|
||||||
|
:value="address.uuid"
|
||||||
|
>
|
||||||
|
{{ address.name }} — {{ address.address }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<hr class="border-base-300" />
|
<hr class="border-base-300" />
|
||||||
|
|
||||||
<!-- FormKit dynamic form -->
|
<!-- FormKit dynamic form -->
|
||||||
@@ -100,6 +116,8 @@
|
|||||||
import { FormKitSchema } from '@formkit/vue'
|
import { FormKitSchema } from '@formkit/vue'
|
||||||
import type { FormKitSchemaNode } from '@formkit/core'
|
import type { FormKitSchemaNode } from '@formkit/core'
|
||||||
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
||||||
|
import { CreateOfferDocument } from '~/composables/graphql/team/exchange-generated'
|
||||||
|
import { GetTeamAddressesDocument } from '~/composables/graphql/team/teams-generated'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['auth-oidc'],
|
middleware: ['auth-oidc'],
|
||||||
@@ -114,6 +132,7 @@ const localePath = useLocalePath()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { execute } = useGraphQL()
|
const { execute } = useGraphQL()
|
||||||
const { getSchema, getEnums, schemaToFormKit } = useTerminus()
|
const { getSchema, getEnums, schemaToFormKit } = useTerminus()
|
||||||
|
const { activeTeamId } = useActiveTeam()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
@@ -126,9 +145,24 @@ const productName = ref<string>('')
|
|||||||
const schemaId = ref<string | null>(null)
|
const schemaId = ref<string | null>(null)
|
||||||
const schemaDescription = ref<string | null>(null)
|
const schemaDescription = ref<string | null>(null)
|
||||||
const formkitSchema = ref<FormKitSchemaNode[]>([])
|
const formkitSchema = ref<FormKitSchemaNode[]>([])
|
||||||
|
const addresses = ref<any[]>([])
|
||||||
|
const selectedAddressUuid = ref<string | null>(null)
|
||||||
|
|
||||||
const isDev = process.dev
|
const isDev = process.dev
|
||||||
|
|
||||||
|
const loadAddresses = async () => {
|
||||||
|
try {
|
||||||
|
const result = await execute(GetTeamAddressesDocument, {}, 'team', 'teams')
|
||||||
|
addresses.value = result.teamAddresses || []
|
||||||
|
const defaultAddress = addresses.value.find((address: any) => address.isDefault)
|
||||||
|
selectedAddressUuid.value = defaultAddress?.uuid || addresses.value[0]?.uuid || null
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load addresses:', err)
|
||||||
|
addresses.value = []
|
||||||
|
selectedAddressUuid.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -166,6 +200,7 @@ const loadData = async () => {
|
|||||||
// 3. Load enums and convert to FormKit schema
|
// 3. Load enums and convert to FormKit schema
|
||||||
const enums = await getEnums()
|
const enums = await getEnums()
|
||||||
formkitSchema.value = schemaToFormKit(terminusClass, enums)
|
formkitSchema.value = schemaToFormKit(terminusClass, enums)
|
||||||
|
await loadAddresses()
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
hasError.value = true
|
hasError.value = true
|
||||||
@@ -181,17 +216,41 @@ const handleSubmit = async (data: Record<string, unknown>) => {
|
|||||||
try {
|
try {
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
|
|
||||||
console.log('Form data:', data)
|
if (!activeTeamId.value) {
|
||||||
|
throw new Error(t('clientOfferForm.error.load'))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Save data to TerminusDB
|
const selectedAddress = addresses.value.find((address: any) => address.uuid === selectedAddressUuid.value)
|
||||||
// const result = await saveToTerminus(schemaId.value, data)
|
if (!selectedAddress) {
|
||||||
|
throw new Error(t('clientOfferForm.error.save'))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Create OfferLine with product_uuid and characteristics
|
const input = {
|
||||||
|
teamUuid: activeTeamId.value,
|
||||||
|
productUuid: productUuid.value,
|
||||||
|
productName: productName.value,
|
||||||
|
categoryName: undefined,
|
||||||
|
locationUuid: selectedAddress.uuid,
|
||||||
|
locationName: selectedAddress.name,
|
||||||
|
locationCountry: '',
|
||||||
|
locationCountryCode: selectedAddress.countryCode || '',
|
||||||
|
locationLatitude: selectedAddress.latitude,
|
||||||
|
locationLongitude: selectedAddress.longitude,
|
||||||
|
quantity: data.quantity || 0,
|
||||||
|
unit: data.unit || 'ton',
|
||||||
|
pricePerUnit: data.price_per_unit || data.pricePerUnit || null,
|
||||||
|
currency: data.currency || 'USD',
|
||||||
|
description: data.description || '',
|
||||||
|
validUntil: data.valid_until || data.validUntil || null,
|
||||||
|
terminusSchemaId: schemaId.value,
|
||||||
|
terminusPayload: data,
|
||||||
|
}
|
||||||
|
|
||||||
// Temporary success alert
|
const result = await execute(CreateOfferDocument, { input }, 'team', 'exchange')
|
||||||
alert(t('clientOfferForm.success.saved', { payload: JSON.stringify(data, null, 2) }))
|
if (!result.createOffer?.success) {
|
||||||
|
throw new Error(result.createOffer?.message || t('clientOfferForm.error.save'))
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to offers list
|
|
||||||
await navigateTo(localePath('/clientarea/offers'))
|
await navigateTo(localePath('/clientarea/offers'))
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,30 +1,8 @@
|
|||||||
mutation CreateOffer($input: OfferInput!) {
|
mutation CreateOffer($input: OfferInput!) {
|
||||||
createOffer(input: $input) {
|
createOffer(input: $input) {
|
||||||
offer {
|
success
|
||||||
uuid
|
message
|
||||||
teamUuid
|
workflowId
|
||||||
status
|
offerUuid
|
||||||
# Location
|
|
||||||
locationUuid
|
|
||||||
locationName
|
|
||||||
locationCountry
|
|
||||||
locationCountryCode
|
|
||||||
locationLatitude
|
|
||||||
locationLongitude
|
|
||||||
# Product
|
|
||||||
productUuid
|
|
||||||
productName
|
|
||||||
categoryName
|
|
||||||
# Price
|
|
||||||
quantity
|
|
||||||
unit
|
|
||||||
pricePerUnit
|
|
||||||
currency
|
|
||||||
# Misc
|
|
||||||
description
|
|
||||||
validUntil
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
"productNotFound": "Product with UUID {uuid} not found",
|
"productNotFound": "Product with UUID {uuid} not found",
|
||||||
"schemaNotFound": "Schema \"{schema}\" not found in TerminusDB"
|
"schemaNotFound": "Schema \"{schema}\" not found in TerminusDB"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"location": "Location",
|
||||||
|
"location_empty": "No locations available"
|
||||||
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"saved": "Data saved!\\n\\n{payload}"
|
"saved": "Data saved!\\n\\n{payload}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
"productNotFound": "Продукт с UUID {uuid} не найден",
|
"productNotFound": "Продукт с UUID {uuid} не найден",
|
||||||
"schemaNotFound": "Схема \"{schema}\" не найдена в TerminusDB"
|
"schemaNotFound": "Схема \"{schema}\" не найдена в TerminusDB"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"location": "Локация",
|
||||||
|
"location_empty": "Локации отсутствуют"
|
||||||
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"saved": "Данные сохранены!\\n\\n{payload}"
|
"saved": "Данные сохранены!\\n\\n{payload}"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user