Integración con Medusa

Cannahub utiliza Medusa v2 como backend de e-commerce. Esta guía documenta la arquitectura de integración, endpoints disponibles, y las convenciones utilizadas para extender Medusa con funcionalidad específica del dominio.


Qué es Medusa

Medusa es una plataforma de e-commerce headless y open-source que proporciona:

  • Name
    API RESTful
    Type
    core
    Description

    Endpoints completos para productos, órdenes, clientes y más.

  • Name
    Multi-tenancy
    Type
    feature
    Description

    Soporte para múltiples tiendas/organizaciones.

  • Name
    Extensibilidad
    Type
    feature
    Description

    Campos metadata flexibles para datos de dominio.

  • Name
    TypeScript
    Type
    tech
    Description

    SDK tipado y arquitectura moderna.

Stack de Medusa v2

// Backend
- Node.js + TypeScript
- PostgreSQL (database)
- Redis (cache/sessions)

// Arquitectura
- Admin API (/admin/*)
- Store API (/store/*)
- Auth API (/auth/*)

// Extensiones
- Custom metadata en todas las entidades
- Workflows para lógica compleja
- Event system para webhooks

Arquitectura de Integración

Loading diagram...

URLs Base

  • Name
    Producción
    Type
    https://api.cannahub.tech
    Description

    API de producción para aplicaciones en vivo.

  • Name
    Staging
    Type
    https://staging-api.cannahub.tech
    Description

    Entorno de pruebas pre-producción.

  • Name
    Desarrollo
    Type
    http://localhost:9000
    Description

    Servidor local de Medusa.

Configuración de cliente

// lib/medusa/client.ts
import Medusa from '@medusajs/medusa-js'

const MEDUSA_BACKEND_URL =
  process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
  ?? 'http://localhost:9000'

export const medusaClient = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  maxRetries: 3,
})

Estructura de la API

Admin API (/admin/*)

Endpoints para administradores y staff. Requieren autenticación con cannahubAdminToken.

EndpointDescripción
/admin/productsCRUD de productos
/admin/customersGestión de clientes/miembros
/admin/ordersGestión de pedidos
/admin/customer-groupsMembresías (customer groups)
/admin/inventory-itemsControl de inventario
/admin/stock-locationsUbicaciones de stock
/admin/price-listsListas de precios por membresía
/admin/fulfillmentsGestión de fulfillment
/admin/paymentsCaptura de pagos

Ejemplo: Listar productos

curl https://api.cannahub.tech/admin/products \
  -H "Authorization: Bearer {admin_token}" \
  -H "Content-Type: application/json"

Respuesta

{
  "products": [
    {
      "id": "prod_01HQ8BD5G",
      "title": "Blue Dream Indoor",
      "status": "published",
      "variants": [...],
      "metadata": {
        "strainId": "strain_blue_dream",
        "isFlower": true
      }
    }
  ],
  "count": 45,
  "offset": 0,
  "limit": 20
}

Store API (/store/*)

Endpoints públicos para clientes. Algunos requieren autenticación con cannahubToken.

EndpointAuthDescripción
/store/productsNoCatálogo público
/store/products/:idNoDetalle de producto
/store/cartsNoCrear carrito
/store/carts/:idOperaciones en carrito
/store/carts/:id/completeCompletar orden
/store/customers/mePerfil del cliente
/store/customers/me/ordersHistorial de órdenes

Ejemplo: Crear carrito

curl -X POST https://api.cannahub.tech/store/carts \
  -H "Content-Type: application/json" \
  -H "x-publishable-api-key: {api_key}" \
  -d '{
    "region_id": "reg_argentina",
    "sales_channel_id": "sc_high_up"
  }'

Respuesta

{
  "cart": {
    "id": "cart_01HQ8ABC123",
    "region_id": "reg_argentina",
    "items": [],
    "total": 0
  }
}

Auth API (/auth/*)

Endpoints de autenticación para diferentes tipos de usuarios.

EndpointDescripción
/auth/customer/emailpassLogin de miembro
/auth/user/emailpassLogin de admin/staff
/auth/customer/emailpass/registerRegistro de miembro
/auth/sessionVerificar sesión activa

Login de miembro

curl -X POST https://api.cannahub.tech/auth/customer/emailpass \
  -H "Content-Type: application/json" \
  -d '{
    "email": "miembro@ejemplo.com",
    "password": "contraseña_segura"
  }'

Respuesta

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI..."
}

Autenticación

Tokens JWT

Cannahub utiliza JWT tokens almacenados en cookies HTTP-only para seguridad.

  • Name
    cannahubToken
    Type
    cookie
    Description

    Token de miembro. Expira en 7 días.

  • Name
    cannahubAdminToken
    Type
    cookie
    Description

    Token de admin/staff. Expira en 7 días.

Headers Requeridos

  • Name
    Authorization
    Type
    header
    Description

    Bearer {token} para endpoints autenticados.

  • Name
    x-publishable-api-key
    Type
    header
    Description

    API key pública para endpoints de store.

Configuración de headers

// api/medusa/config.ts
export function getMedusaHeaders(
  isAdmin: boolean = false
) {
  const token = isAdmin
    ? cookies().get('cannahubAdminToken')?.value
    : cookies().get('cannahubToken')?.value

  return {
    'Authorization': token
      ? `Bearer ${token}`
      : undefined,
    'Content-Type': 'application/json',
    'x-publishable-api-key':
      process.env.MEDUSA_PUBLISHABLE_KEY,
  }
}

Multi-Tenancy

Cannahub soporta múltiples organizaciones (clubs) usando características nativas de Medusa.

Identificación de Organización

  • Name
    Subdomain
    Type
    identificador
    Description

    high-up.cannahub.tech identifica al club.

  • Name
    Sales Channel
    Type
    medusa
    Description

    Cada club tiene su propio sales channel.

  • Name
    Stock Location
    Type
    medusa
    Description

    Inventario separado por ubicación.

  • Name
    Publishable API Key
    Type
    medusa
    Description

    Key asociada al sales channel.

Organizaciones soportadas

const ORGANIZATIONS = {
  'high-up': {
    salesChannelId: 'sc_high_up',
    stockLocationId: 'sloc_high_up',
    publishableKey: 'pk_high_up_xxx',
  },
  'don-marcelino': {
    salesChannelId: 'sc_don_marcelino',
    stockLocationId: 'sloc_don_marcelino',
    publishableKey: 'pk_don_marcelino_xxx',
  },
  'circulo-rojo': {
    salesChannelId: 'sc_circulo_rojo',
    stockLocationId: 'sloc_circulo_rojo',
    publishableKey: 'pk_circulo_rojo_xxx',
  },
  // ... más organizaciones
}

// Extracción desde request
function getOrganization(request: Request) {
  const host = request.headers.get('host')
  const subdomain = host?.split('.')[0]
  return ORGANIZATIONS[subdomain]
}

Convenciones de Metadata

Medusa permite almacenar datos arbitrarios en el campo metadata de todas las entidades. Cannahub usa convenciones consistentes.

Customer Metadata

Estructura

interface CustomerMetadata {
  // Estado del miembro
  status: MemberStatus
  role: Role

  // Onboarding
  onboardingStep: OnboardingStep
  hasReprocann: boolean

  // Información médica
  medicalRecord: {
    reprocannCode?: string
    reprocannExpiration?: string
    allergies: string[]
    conditions: string[]
    medications: string[]
    emergencyContact: {
      name: string
      phone: string
      relationship: string
    }
  }

  // Membresía activa
  membership: {
    id: string
    name: string
    status: 'PAID' | 'PENDING' | 'EXPIRED'
    expirationDate: string
  }

  // Datos personales extendidos
  bornDate?: string
  document?: string
  avatar?: string
}

Product Metadata

Estructura

interface ProductMetadata {
  // Identificación de tipo
  isFlower: boolean

  // Referencia a cepa (productos de flor)
  strainId?: string
  strainName?: string
  strainType?: 'sativa' | 'indica' | 'hybrid'

  // O cepa completa (legacy)
  strain?: {
    id: string
    name: string
    type: string
    cannabinoids: {
      thc: number
      cbd: number
    }
    terpenes: string[]
    effects: {
      helpsWith: string[]
      feelings: string[]
    }
  }

  // Información adicional
  cultivator?: string
  harvestDate?: string
  batchNumber?: string
}

Variant Metadata

Estructura

interface VariantMetadata {
  // Peso/cantidad por unidad
  weight: number      // Gramos (para flores)
  quantity: number    // Unidades (para otros)
  unit: 'grams' | 'ml' | 'units'

  // Información de lote
  batchId?: string
  expirationDate?: string
}

Customer Group (Membership) Metadata

Estructura

interface CustomerGroupMetadata {
  // Información de membresía
  description: string
  price: number
  currency: string

  // Beneficios y límites
  benefits: string[]
  limits?: {
    monthlyPurchase?: number
    productAccess?: string[]
  }

  // Configuración
  priceListId: string
  isActive: boolean
  sortOrder: number
  color?: string
}

Capa BFF (Backend for Frontend)

El BFF simplifica y optimiza las llamadas al backend.

Propósito

  • Name
    Agregación
    Type
    optimization
    Description

    Combina múltiples llamadas a Medusa en una sola respuesta.

  • Name
    Transformación
    Type
    core
    Description

    Convierte tipos de Medusa a tipos de Cannahub.

  • Name
    Caché
    Type
    optimization
    Description

    Cachea respuestas frecuentes.

  • Name
    Lógica de Negocio
    Type
    core
    Description

    Implementa reglas específicas del dominio.

Ejemplo de ruta BFF

// app/api/products/route.ts
import { getMedusaClient } from '@/lib/medusa'
import { parseMedusaProduct } from '@/lib/medusa/products'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const medusa = getMedusaClient()

  // Llamada a Medusa
  const { products, count } =
    await medusa.admin.products.list({
      status: 'published',
      expand: 'variants,variants.prices',
    })

  // Transformación
  const transformed = products.map(parseMedusaProduct)

  // Respuesta simplificada
  return NextResponse.json({
    products: transformed,
    count,
  })
}

Rutas BFF Disponibles

RutaMétodoDescripciónLlamadas Reducidas
/api/productsGETProductos con cepas2 → 1
/api/products/:id/with-inventoryGETProducto + inventario multi-ubicaciónN+1 → 1
/api/strainsGETCatálogo de cepas filtrable-
/api/membershipsGETMembresías disponibles1 → 1
/api/dashboard/statsGETEstadísticas agregadas4 → 1
/api/orders/create-and-processPOSTFlujo completo de orden15+ → 1

Flujo de Creación de Orden

El flujo completo de orden requiere múltiples llamadas a Medusa que el BFF orquesta.

Loading diagram...

Manejo de Errores

Códigos de Error de Medusa

CódigoSignificado
400Solicitud inválida
401No autenticado
403Sin permisos
404Recurso no encontrado
409Conflicto (ej: email duplicado)
422Entidad no procesable
500Error interno

Manejo de errores

// lib/medusa/error-handler.ts
export function handleMedusaError(
  error: any
): ApiError {
  if (error.response) {
    const { status, data } = error.response

    return {
      code: status,
      message: data.message
        ?? 'Error de servidor',
      details: data.errors,
    }
  }

  return {
    code: 500,
    message: 'Error de conexión con Medusa',
    details: error.message,
  }
}

Rate Limiting

Medusa implementa límites de tasa que Cannahub respeta y propaga.

  • Name
    Store endpoints
    Type
    1000 req/min
    Description

    Endpoints públicos de tienda.

  • Name
    Admin endpoints
    Type
    500 req/min
    Description

    Endpoints administrativos.

  • Name
    Auth endpoints
    Type
    10 req/min
    Description

    Login para prevenir fuerza bruta.


Próximos Pasos

Was this page helpful?