Improve the demo feature of the project
This commit is contained in:
@@ -27,8 +27,7 @@ COPY --from=shared /shared /app/shared
|
||||
# Copy application code
|
||||
COPY services/orders/ .
|
||||
|
||||
# Copy scripts directory
|
||||
COPY scripts/ /app/scripts/
|
||||
|
||||
|
||||
# Add shared libraries to Python path
|
||||
ENV PYTHONPATH="/app:/app/shared:${PYTHONPATH:-}"
|
||||
|
||||
@@ -108,40 +108,6 @@ async def create_customer(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
response_model=CustomerResponse
|
||||
)
|
||||
async def get_customer(
|
||||
tenant_id: UUID = Path(...),
|
||||
customer_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get customer details by ID"""
|
||||
try:
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
return CustomerResponse.from_orm(customer)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting customer",
|
||||
customer_id=str(customer_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve customer"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("customers"),
|
||||
response_model=List[CustomerResponse]
|
||||
@@ -176,6 +142,40 @@ async def get_customers(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
response_model=CustomerResponse
|
||||
)
|
||||
async def get_customer(
|
||||
tenant_id: UUID = Path(...),
|
||||
customer_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get customer details by ID"""
|
||||
try:
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
return CustomerResponse.from_orm(customer)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting customer",
|
||||
customer_id=str(customer_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve customer"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
route_builder.build_resource_detail_route("customers", "customer_id"),
|
||||
response_model=CustomerResponse
|
||||
|
||||
352
services/orders/app/api/internal_demo.py
Normal file
352
services/orders/app/api/internal_demo.py
Normal file
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Internal Demo Cloning API for Orders Service
|
||||
Service-to-service endpoint for cloning order and procurement data
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
import uuid
|
||||
from datetime import datetime, timezone, timedelta, date
|
||||
from typing import Optional
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.order import CustomerOrder, OrderItem
|
||||
from app.models.procurement import ProcurementPlan, ProcurementRequirement
|
||||
from app.models.customer import Customer
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
|
||||
# Internal API key for service-to-service auth
|
||||
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
|
||||
|
||||
|
||||
def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)):
|
||||
"""Verify internal API key for service-to-service communication"""
|
||||
if x_internal_api_key != INTERNAL_API_KEY:
|
||||
logger.warning("Unauthorized internal API access attempted")
|
||||
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/clone")
|
||||
async def clone_demo_data(
|
||||
base_tenant_id: str,
|
||||
virtual_tenant_id: str,
|
||||
demo_account_type: str,
|
||||
session_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(verify_internal_api_key)
|
||||
):
|
||||
"""
|
||||
Clone orders service data for a virtual demo tenant
|
||||
|
||||
Clones:
|
||||
- Customers
|
||||
- Customer orders with line items
|
||||
- Procurement plans with requirements
|
||||
- Adjusts dates to recent timeframe
|
||||
|
||||
Args:
|
||||
base_tenant_id: Template tenant UUID to clone from
|
||||
virtual_tenant_id: Target virtual tenant UUID
|
||||
demo_account_type: Type of demo account
|
||||
session_id: Originating session ID for tracing
|
||||
|
||||
Returns:
|
||||
Cloning status and record counts
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(
|
||||
"Starting orders data cloning",
|
||||
base_tenant_id=base_tenant_id,
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
demo_account_type=demo_account_type,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate UUIDs
|
||||
base_uuid = uuid.UUID(base_tenant_id)
|
||||
virtual_uuid = uuid.UUID(virtual_tenant_id)
|
||||
|
||||
# Track cloning statistics
|
||||
stats = {
|
||||
"customers": 0,
|
||||
"customer_orders": 0,
|
||||
"order_line_items": 0,
|
||||
"procurement_plans": 0,
|
||||
"procurement_requirements": 0
|
||||
}
|
||||
|
||||
# Customer ID mapping (old -> new)
|
||||
customer_id_map = {}
|
||||
|
||||
# Clone Customers
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.tenant_id == base_uuid)
|
||||
)
|
||||
base_customers = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"Found customers to clone",
|
||||
count=len(base_customers),
|
||||
base_tenant=str(base_uuid)
|
||||
)
|
||||
|
||||
for customer in base_customers:
|
||||
new_customer_id = uuid.uuid4()
|
||||
customer_id_map[customer.id] = new_customer_id
|
||||
|
||||
new_customer = Customer(
|
||||
id=new_customer_id,
|
||||
tenant_id=virtual_uuid,
|
||||
customer_name=customer.customer_name,
|
||||
customer_type=customer.customer_type,
|
||||
business_name=customer.business_name,
|
||||
contact_person=customer.contact_person,
|
||||
email=customer.email,
|
||||
phone=customer.phone,
|
||||
address=customer.address,
|
||||
tax_id=customer.tax_id,
|
||||
credit_limit=customer.credit_limit,
|
||||
payment_terms=customer.payment_terms,
|
||||
discount_percentage=customer.discount_percentage,
|
||||
is_active=customer.is_active,
|
||||
notes=customer.notes,
|
||||
tags=customer.tags,
|
||||
metadata_=customer.metadata_,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_customer)
|
||||
stats["customers"] += 1
|
||||
|
||||
# Clone Customer Orders with Line Items
|
||||
result = await db.execute(
|
||||
select(CustomerOrder).where(CustomerOrder.tenant_id == base_uuid)
|
||||
)
|
||||
base_orders = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"Found customer orders to clone",
|
||||
count=len(base_orders),
|
||||
base_tenant=str(base_uuid)
|
||||
)
|
||||
|
||||
# Calculate date offset
|
||||
if base_orders:
|
||||
max_date = max(order.order_date for order in base_orders)
|
||||
today = datetime.now(timezone.utc)
|
||||
date_offset = today - max_date
|
||||
else:
|
||||
date_offset = timedelta(days=0)
|
||||
|
||||
order_id_map = {}
|
||||
|
||||
for order in base_orders:
|
||||
new_order_id = uuid.uuid4()
|
||||
order_id_map[order.id] = new_order_id
|
||||
|
||||
new_order = CustomerOrder(
|
||||
id=new_order_id,
|
||||
tenant_id=virtual_uuid,
|
||||
order_number=f"ORD-{uuid.uuid4().hex[:8].upper()}", # New order number
|
||||
customer_id=customer_id_map.get(order.customer_id, order.customer_id),
|
||||
status=order.status,
|
||||
order_type=order.order_type,
|
||||
priority=order.priority,
|
||||
order_date=order.order_date + date_offset if order.order_date else None,
|
||||
requested_delivery_date=order.requested_delivery_date + date_offset if order.requested_delivery_date else None,
|
||||
confirmed_delivery_date=order.confirmed_delivery_date + date_offset if order.confirmed_delivery_date else None,
|
||||
actual_delivery_date=order.actual_delivery_date + date_offset if order.actual_delivery_date else None,
|
||||
delivery_method=order.delivery_method,
|
||||
delivery_address=order.delivery_address,
|
||||
delivery_instructions=order.delivery_instructions,
|
||||
delivery_window_start=order.delivery_window_start + date_offset if order.delivery_window_start else None,
|
||||
delivery_window_end=order.delivery_window_end + date_offset if order.delivery_window_end else None,
|
||||
subtotal=order.subtotal,
|
||||
tax_amount=order.tax_amount,
|
||||
discount_amount=order.discount_amount,
|
||||
delivery_fee=order.delivery_fee,
|
||||
total_amount=order.total_amount,
|
||||
payment_status=order.payment_status,
|
||||
payment_method=order.payment_method,
|
||||
notes=order.notes,
|
||||
internal_notes=order.internal_notes,
|
||||
tags=order.tags,
|
||||
metadata_=order.metadata_,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_order)
|
||||
stats["customer_orders"] += 1
|
||||
|
||||
# Clone Order Items
|
||||
for old_order_id, new_order_id in order_id_map.items():
|
||||
result = await db.execute(
|
||||
select(OrderItem).where(OrderItem.order_id == old_order_id)
|
||||
)
|
||||
order_items = result.scalars().all()
|
||||
|
||||
for item in order_items:
|
||||
new_item = OrderItem(
|
||||
id=uuid.uuid4(),
|
||||
order_id=new_order_id,
|
||||
product_id=item.product_id, # Keep product reference
|
||||
quantity=item.quantity,
|
||||
unit_price=item.unit_price,
|
||||
subtotal=item.subtotal,
|
||||
discount_amount=item.discount_amount,
|
||||
tax_amount=item.tax_amount,
|
||||
total_amount=item.total_amount,
|
||||
notes=item.notes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_item)
|
||||
stats["order_line_items"] += 1
|
||||
|
||||
# Clone Procurement Plans with Requirements
|
||||
result = await db.execute(
|
||||
select(ProcurementPlan).where(ProcurementPlan.tenant_id == base_uuid)
|
||||
)
|
||||
base_plans = result.scalars().all()
|
||||
|
||||
logger.info(
|
||||
"Found procurement plans to clone",
|
||||
count=len(base_plans),
|
||||
base_tenant=str(base_uuid)
|
||||
)
|
||||
|
||||
# Calculate date offset for procurement
|
||||
if base_plans:
|
||||
max_plan_date = max(plan.plan_date for plan in base_plans)
|
||||
today_date = date.today()
|
||||
days_diff = (today_date - max_plan_date).days
|
||||
plan_date_offset = timedelta(days=days_diff)
|
||||
else:
|
||||
plan_date_offset = timedelta(days=0)
|
||||
|
||||
plan_id_map = {}
|
||||
|
||||
for plan in base_plans:
|
||||
new_plan_id = uuid.uuid4()
|
||||
plan_id_map[plan.id] = new_plan_id
|
||||
|
||||
new_plan = ProcurementPlan(
|
||||
id=new_plan_id,
|
||||
tenant_id=virtual_uuid,
|
||||
plan_number=f"PROC-{uuid.uuid4().hex[:8].upper()}",
|
||||
plan_date=plan.plan_date + plan_date_offset.days if plan.plan_date else None,
|
||||
plan_period_start=plan.plan_period_start + plan_date_offset.days if plan.plan_period_start else None,
|
||||
plan_period_end=plan.plan_period_end + plan_date_offset.days if plan.plan_period_end else None,
|
||||
planning_horizon_days=plan.planning_horizon_days,
|
||||
status=plan.status,
|
||||
plan_type=plan.plan_type,
|
||||
priority=plan.priority,
|
||||
business_model=plan.business_model,
|
||||
procurement_strategy=plan.procurement_strategy,
|
||||
total_requirements=plan.total_requirements,
|
||||
total_estimated_cost=plan.total_estimated_cost,
|
||||
total_approved_cost=plan.total_approved_cost,
|
||||
cost_variance=plan.cost_variance,
|
||||
notes=plan.notes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_plan)
|
||||
stats["procurement_plans"] += 1
|
||||
|
||||
# Clone Procurement Requirements
|
||||
for old_plan_id, new_plan_id in plan_id_map.items():
|
||||
result = await db.execute(
|
||||
select(ProcurementRequirement).where(ProcurementRequirement.procurement_plan_id == old_plan_id)
|
||||
)
|
||||
requirements = result.scalars().all()
|
||||
|
||||
for req in requirements:
|
||||
new_req = ProcurementRequirement(
|
||||
id=uuid.uuid4(),
|
||||
procurement_plan_id=new_plan_id,
|
||||
ingredient_id=req.ingredient_id, # Keep ingredient reference
|
||||
required_quantity=req.required_quantity,
|
||||
unit_of_measure=req.unit_of_measure,
|
||||
estimated_unit_cost=req.estimated_unit_cost,
|
||||
estimated_total_cost=req.estimated_total_cost,
|
||||
required_by_date=req.required_by_date + plan_date_offset.days if req.required_by_date else None,
|
||||
priority=req.priority,
|
||||
source=req.source,
|
||||
notes=req.notes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
db.add(new_req)
|
||||
stats["procurement_requirements"] += 1
|
||||
|
||||
# Commit all changes
|
||||
await db.commit()
|
||||
|
||||
total_records = sum(stats.values())
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
logger.info(
|
||||
"Orders data cloning completed",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
total_records=total_records,
|
||||
stats=stats,
|
||||
duration_ms=duration_ms
|
||||
)
|
||||
|
||||
return {
|
||||
"service": "orders",
|
||||
"status": "completed",
|
||||
"records_cloned": total_records,
|
||||
"duration_ms": duration_ms,
|
||||
"details": stats
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
logger.error("Invalid UUID format", error=str(e))
|
||||
raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to clone orders data",
|
||||
error=str(e),
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Rollback on error
|
||||
await db.rollback()
|
||||
|
||||
return {
|
||||
"service": "orders",
|
||||
"status": "failed",
|
||||
"records_cloned": 0,
|
||||
"duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000),
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/clone/health")
|
||||
async def clone_health_check(_: bool = Depends(verify_internal_api_key)):
|
||||
"""
|
||||
Health check for internal cloning endpoint
|
||||
Used by orchestrator to verify service availability
|
||||
"""
|
||||
return {
|
||||
"service": "orders",
|
||||
"clone_endpoint": "available",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
@@ -76,19 +76,19 @@ async def create_order(
|
||||
try:
|
||||
# Ensure tenant_id matches
|
||||
order_data.tenant_id = tenant_id
|
||||
|
||||
|
||||
order = await orders_service.create_order(
|
||||
db,
|
||||
order_data,
|
||||
db,
|
||||
order_data,
|
||||
user_id=UUID(current_user["sub"])
|
||||
)
|
||||
|
||||
logger.info("Order created successfully",
|
||||
|
||||
logger.info("Order created successfully",
|
||||
order_id=str(order.id),
|
||||
order_number=order.order_number)
|
||||
|
||||
|
||||
return order
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning("Invalid order data", error=str(e))
|
||||
raise HTTPException(
|
||||
@@ -103,38 +103,6 @@ async def create_order(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("{order_id}"), response_model=OrderResponse)
|
||||
async def get_order(
|
||||
tenant_id: UUID = Path(...),
|
||||
order_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get order details with items"""
|
||||
try:
|
||||
order = await orders_service.get_order_with_items(db, order_id, tenant_id)
|
||||
if not order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Order not found"
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting order",
|
||||
order_id=str(order_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve order"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("orders"),
|
||||
response_model=List[OrderResponse]
|
||||
@@ -176,6 +144,40 @@ async def get_orders(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("{order_id}"),
|
||||
response_model=OrderResponse
|
||||
)
|
||||
async def get_order(
|
||||
tenant_id: UUID = Path(...),
|
||||
order_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get order details with items"""
|
||||
try:
|
||||
order = await orders_service.get_order_with_items(db, order_id, tenant_id)
|
||||
if not order:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Order not found"
|
||||
)
|
||||
|
||||
return order
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting order",
|
||||
order_id=str(order_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve order"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
route_builder.build_base_route("{order_id}"),
|
||||
response_model=OrderResponse
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.api.orders import router as orders_router
|
||||
from app.api.customers import router as customers_router
|
||||
from app.api.order_operations import router as order_operations_router
|
||||
from app.api.procurement_operations import router as procurement_operations_router
|
||||
from app.api import internal_demo
|
||||
from app.services.procurement_scheduler_service import ProcurementSchedulerService
|
||||
from shared.service_base import StandardFastAPIService
|
||||
|
||||
@@ -98,13 +99,18 @@ service.setup_standard_endpoints()
|
||||
|
||||
# Include routers - organized by ATOMIC and BUSINESS operations
|
||||
# ATOMIC: Direct CRUD operations
|
||||
service.add_router(orders_router)
|
||||
# NOTE: Register customers_router BEFORE orders_router to ensure /customers
|
||||
# matches before the parameterized /{order_id} route
|
||||
service.add_router(customers_router)
|
||||
service.add_router(orders_router)
|
||||
|
||||
# BUSINESS: Complex operations and workflows
|
||||
service.add_router(order_operations_router)
|
||||
service.add_router(procurement_operations_router)
|
||||
|
||||
# INTERNAL: Service-to-service endpoints
|
||||
service.add_router(internal_demo.router)
|
||||
|
||||
|
||||
@app.post("/test/procurement-scheduler")
|
||||
async def test_procurement_scheduler():
|
||||
|
||||
229
services/orders/scripts/demo/clientes_es.json
Normal file
229
services/orders/scripts/demo/clientes_es.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"clientes": [
|
||||
{
|
||||
"customer_name": "Cafetería El Rincón",
|
||||
"customer_type": "retail",
|
||||
"business_name": "El Rincón Cafetería S.L.",
|
||||
"contact_person": "Ana Rodríguez García",
|
||||
"email": "pedidos@cafeteriaelrincon.es",
|
||||
"phone": "+34 963 456 789",
|
||||
"address": "Calle Mayor, 78, 46001 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 15.0,
|
||||
"credit_limit": 2000.00,
|
||||
"is_active": true,
|
||||
"notes": "Cliente diario. Entrega preferente 6:00-7:00 AM.",
|
||||
"tags": ["hosteleria", "cafeteria", "diario"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Supermercado La Bodega",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Supermercados La Bodega S.L.",
|
||||
"contact_person": "Carlos Jiménez Moreno",
|
||||
"email": "compras@superlabodega.com",
|
||||
"phone": "+34 965 789 012",
|
||||
"address": "Avenida del Mediterráneo, 156, 03500 Benidorm, Alicante",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 20.0,
|
||||
"credit_limit": 5000.00,
|
||||
"is_active": true,
|
||||
"notes": "Entrega 3 veces/semana: Lunes, Miércoles, Viernes. Horario: 5:00-6:00 AM.",
|
||||
"tags": ["retail", "supermercado", "mayorista"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Restaurante Casa Pepe",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Casa Pepe Restauración S.C.",
|
||||
"contact_person": "José Luis Pérez",
|
||||
"email": "pedidos@casapepe.es",
|
||||
"phone": "+34 961 234 567",
|
||||
"address": "Plaza del Mercado, 12, 46003 Valencia",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 12.0,
|
||||
"credit_limit": 1500.00,
|
||||
"is_active": true,
|
||||
"notes": "Especializado en cocina mediterránea. Requiere panes especiales.",
|
||||
"tags": ["hosteleria", "restaurante"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Hotel Playa Sol",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Hoteles Costa Blanca S.A.",
|
||||
"contact_person": "María Carmen López",
|
||||
"email": "compras@hotelplayasol.com",
|
||||
"phone": "+34 965 123 456",
|
||||
"address": "Paseo Marítimo, 234, 03501 Benidorm, Alicante",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 8000.00,
|
||||
"is_active": true,
|
||||
"notes": "Hotel 4 estrellas. Pedidos grandes para desayuno buffet. Volumen estable todo el año.",
|
||||
"tags": ["hosteleria", "hotel", "mayorista", "alto_volumen"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Bar Los Naranjos",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Los Naranjos C.B.",
|
||||
"contact_person": "Francisco Martínez",
|
||||
"email": "losnaranjos@gmail.com",
|
||||
"phone": "+34 963 789 012",
|
||||
"address": "Calle de la Paz, 45, 46002 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 10.0,
|
||||
"credit_limit": 800.00,
|
||||
"is_active": true,
|
||||
"notes": "Bar de barrio. Pedidos pequeños diarios.",
|
||||
"tags": ["hosteleria", "bar", "pequeño"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Panadería La Tahona",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Panadería La Tahona",
|
||||
"contact_person": "Isabel García Ruiz",
|
||||
"email": "latahona@hotmail.com",
|
||||
"phone": "+34 962 345 678",
|
||||
"address": "Avenida de los Naranjos, 89, 46470 Albal, Valencia",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 25.0,
|
||||
"credit_limit": 3000.00,
|
||||
"is_active": true,
|
||||
"notes": "Panadería que no tiene obrador propio. Compra productos semipreparados.",
|
||||
"tags": ["panaderia", "b2b"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Catering García e Hijos",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "García Catering S.L.",
|
||||
"contact_person": "Miguel García Sánchez",
|
||||
"email": "pedidos@cateringgarcia.es",
|
||||
"phone": "+34 963 567 890",
|
||||
"address": "Polígono Industrial Vara de Quart, Nave 34, 46014 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 22.0,
|
||||
"credit_limit": 6000.00,
|
||||
"is_active": true,
|
||||
"notes": "Catering para eventos. Pedidos variables según calendario de eventos.",
|
||||
"tags": ["catering", "eventos", "variable"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Residencia Tercera Edad San Antonio",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Residencia San Antonio",
|
||||
"contact_person": "Lucía Fernández",
|
||||
"email": "compras@residenciasanantonio.es",
|
||||
"phone": "+34 961 890 123",
|
||||
"address": "Calle San Antonio, 156, 46013 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 15.0,
|
||||
"credit_limit": 4000.00,
|
||||
"is_active": true,
|
||||
"notes": "Residencia con 120 plazas. Pedidos regulares y previsibles.",
|
||||
"tags": ["institucional", "residencia", "estable"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Colegio Santa Teresa",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Cooperativa Colegio Santa Teresa",
|
||||
"contact_person": "Carmen Navarro",
|
||||
"email": "cocina@colegiosantateresa.es",
|
||||
"phone": "+34 963 012 345",
|
||||
"address": "Avenida de la Constitución, 234, 46008 Valencia",
|
||||
"payment_terms": "net_45",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 5000.00,
|
||||
"is_active": true,
|
||||
"notes": "Colegio con 800 alumnos. Pedidos de septiembre a junio (calendario escolar).",
|
||||
"tags": ["institucional", "colegio", "estacional"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Mercado Central - Puesto 23",
|
||||
"customer_type": "retail",
|
||||
"business_name": "Antonio Sánchez - Mercado Central",
|
||||
"contact_person": "Antonio Sánchez",
|
||||
"email": "antoniosanchez.mercado@gmail.com",
|
||||
"phone": "+34 963 456 012",
|
||||
"address": "Mercado Central, Puesto 23, 46001 Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 8.0,
|
||||
"credit_limit": 1000.00,
|
||||
"is_active": true,
|
||||
"notes": "Puesto de venta en el mercado central. Compra para revender.",
|
||||
"tags": ["mercado", "revendedor", "pequeño"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Cafetería Universidad Politécnica",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Servicios Universitarios UPV",
|
||||
"contact_person": "Roberto Martín",
|
||||
"email": "cafeteria@upv.es",
|
||||
"phone": "+34 963 789 456",
|
||||
"address": "Campus de Vera, Edificio 4N, 46022 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 20.0,
|
||||
"credit_limit": 7000.00,
|
||||
"is_active": true,
|
||||
"notes": "Cafetería universitaria. Alto volumen durante curso académico. Cierra en verano.",
|
||||
"tags": ["institucional", "universidad", "estacional", "alto_volumen"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Panadería El Horno de Oro",
|
||||
"customer_type": "retail",
|
||||
"business_name": "El Horno de Oro S.C.",
|
||||
"contact_person": "Manuel Jiménez",
|
||||
"email": "hornodeoro@telefonica.net",
|
||||
"phone": "+34 965 234 567",
|
||||
"address": "Calle del Cid, 67, 03400 Villena, Alicante",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 25.0,
|
||||
"credit_limit": 2500.00,
|
||||
"is_active": true,
|
||||
"notes": "Panadería tradicional. Compra productos especializados que no produce.",
|
||||
"tags": ["panaderia", "b2b", "especializado"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Bar Cafetería La Plaza",
|
||||
"customer_type": "retail",
|
||||
"business_name": "La Plaza Hostelería",
|
||||
"contact_person": "Teresa López",
|
||||
"email": "barlaplaza@hotmail.com",
|
||||
"phone": "+34 962 567 890",
|
||||
"address": "Plaza Mayor, 3, 46470 Catarroja, Valencia",
|
||||
"payment_terms": "net_7",
|
||||
"discount_percentage": 12.0,
|
||||
"credit_limit": 1200.00,
|
||||
"is_active": true,
|
||||
"notes": "Bar de pueblo con clientela local. Pedidos regulares de lunes a sábado.",
|
||||
"tags": ["hosteleria", "bar", "regular"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Supermercado Eco Verde",
|
||||
"customer_type": "wholesale",
|
||||
"business_name": "Eco Verde Distribución S.L.",
|
||||
"contact_person": "Laura Sánchez",
|
||||
"email": "compras@ecoverde.es",
|
||||
"phone": "+34 963 890 123",
|
||||
"address": "Calle Colón, 178, 46004 Valencia",
|
||||
"payment_terms": "net_30",
|
||||
"discount_percentage": 18.0,
|
||||
"credit_limit": 4500.00,
|
||||
"is_active": true,
|
||||
"notes": "Supermercado especializado en productos ecológicos. Interesados en panes artesanales.",
|
||||
"tags": ["retail", "supermercado", "ecologico", "premium"]
|
||||
},
|
||||
{
|
||||
"customer_name": "Restaurante La Alquería",
|
||||
"customer_type": "retail",
|
||||
"business_name": "La Alquería Grupo Gastronómico",
|
||||
"contact_person": "Javier Moreno",
|
||||
"email": "jefe.cocina@laalqueria.es",
|
||||
"phone": "+34 961 456 789",
|
||||
"address": "Camino de Vera, 45, 46022 Valencia",
|
||||
"payment_terms": "net_15",
|
||||
"discount_percentage": 15.0,
|
||||
"credit_limit": 3500.00,
|
||||
"is_active": true,
|
||||
"notes": "Restaurante de alta gama. Exigente con la calidad. Panes artesanales especiales.",
|
||||
"tags": ["hosteleria", "restaurante", "premium", "exigente"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user