State Machine as a Service (Nami)

Conceptos

  • Organización:

    Es la entidad dueña de la cuenta (ejemplo GaiaDesign), una organización puede tener multiples máquinas de estado.

  • State Machine:

    Es grafo que representa el estado de un item en un momento dado, una máquina de estados puede tener multiples estados y multiples formas de transicionar entre ellos.

  • State:

    Es un estado del item que transiciona en la máquina de estados, un item puede estar en un solo estado en un momento dado.

  • Transition:

    Es el cambio de estado, solo se puede ejecutar una transición a la vez, cada transición necesita validar ciertos campos antes de poder aceptar la transición.

  • Item:

    Es el objeto que transiciona en la máquina de estados, un item puede tener multiples máquinas de estados, puede ser un producto, un cliente, un pedido, etc.

    Un item tiene atributos requeridos, los cuales pueden ser de tipo string o numéricos, todo lo que no sea definido como requerido es opcional aunque no se defina explicitamente en el ItemModel.

    Un item puede tener flags, las flags solo se activan o desactivan cuando se transiciona por ciertos estados.

Crear cuenta

$response = $client->request('POST', '/api/users', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json'
    ],
    'json' => [
        'email' => '[email protected]',
        'plainPassword' => 'password',
        'name' => 'Test',
        'lastName' => 'User',
    ]
]);

Login

$response = $client->request('POST', '/auth/login', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json'
    ],
    'json' => [
        'email' => '[email protected]',
        'password' => 'password',
    ]
]);

$data = $response->toArray();
$token = $data['token'];

Crear Organización

$response = $client->request('POST', '/api/organizations', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json',
        'Authorization' => 'Bearer wegf76sdf8sd899082f3kjk3jhu893'
    ],
    'json' => [
        'name' => 'Test Organization',
        'code' => 'TORG',
    ]
]);

$organizationId = $response->toArray()['@id'];

Crear State Machine

$mapConfig = [
    'code' => 'product-map',
    'states' => [
        'initial' => 'init',
        'middle' => [
            'pending',
            'show',
            'hide',
        ],
        'final' => [
            'publish'
        ]
    ],
    'transitions' => [
        'pending' => [
            'from' => [
                'init',
            ],
            'to' => 'pending',
            'validators' => [
                [
                    'validator' => 'internal.not_empty',
                    'config' => [
                        'sku',
                        'stars',
                        'review'
                    ]
                ]
            ],
            'flags' => [
                'active' => [
                    'active'
                ]
            ]
        ],
        'to_high' => [
            'from' => [
                'pending'
            ],
            'to' => 'high',
            'validators' => [
                [
                    'validator' => 'internal.flag',
                    'config' => [
                        'true' => ['active']
                    ]
                ],
                [
                    'validator' => 'internal.regex',
                    'config' => [
                        'stars' => '/^[4-5]$/'
                    ]
                ]
            ],
        ],
        'to_low' => [
            'from' => [
                'pending'
            ],
            'to' => 'hide',
            'validators' => [
                [
                    'validator' => 'internal.flag',
                    'config' => [
                        'true' => ['active']
                    ]
                ],
                [
                    'validator' => 'internal.regex',
                    'config' => [
                        'stars' => '/^[1-3]$/'
                    ]
                ]
            ],
        ],
        'to_publish' => [
            'from' => [
                'high'
            ],
            'to' => 'publish',
            'validators' => [
                [
                    'validator' => 'internal.flag',
                    'config' => [
                        'true' => ['active']
                    ]
                ]
            ],
        ],
    ]
];

$stateMachineCode = 'TSTM';

$itemModel = [
    'type' => 'object',
    'required' => ['sku', 'review', 'stars'],
    'properties' => [
        'sku' => [
            'type' => 'string',
            'minLength' => 5
        ],
        'review' => [
            'type' => 'string',
            'minLength' => 5
        ],
        'stars' => [
            'type' => 'integer',
            'minimum' => 0,
            'maximum' => 5
        ]
    ],
    'metadata' => [
        'flags' => [
            'active',
            'high',
            'low',
        ]
    ]
];

$stateMachine = [
    'name' => 'Test State Machine',
    'organization' => $organizationId,
    'code' => $stateMachineCode,
    'definition' => $mapConfig,
    'itemModel' => $itemModel
];

// Create state machine using the organization from previous test
$response = $client->request('POST', '/api/organization-state-machines', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json',
        'Authorization' => 'Bearer wegf76sdf8sd899082f3kjk3jhu893'
    ],
    'json' => $stateMachine
]);

$stateMachineId = $response->toArray()['@id'];

Crear item

$response = $client->request('POST', '/api/organization-state-machine-items', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json',
        'Authorization' => 'Bearer wegf76sdf8sd899082f3kjk3jhu893'
    ],
    'json' => [
        'state_machine' => $stateMachineId,
        'body' => [
            'sku' => 'Producto Silla Replica Eames',
            'review' => 'lorem ipsum dolor sit amet',
            'stars' => 4
        ],
    ]
]);

Actualizar item

$itemId = $response->toArray()['@id'];
$response = $client->request('PATCH', $itemId, [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/merge-patch+json',
        'Authorization' => 'Bearer wegf76sdf8sd899082f3kjk3jhu893'
    ],    
    'json' => [
        'state_machine' => $stateMachineId,
        'body' => [
            'field' => 'value'
        ],
    ]
]);

$item = $response->toArray();

Búsqueda de Items

Puedes buscar items usando cualquier campo dentro del body JSON usando la notación body.campo. El campo puede estar en cualquier nivel del JSON.

Ejemplos de búsqueda:

// Búsqueda por email
$response = $client->request('GET', '/api/organization-state-machine-items', [
    'headers' => $this->getHeaders(),
    'query' => [
        'body.email' => '[email protected]'
    ]
]);

// Búsqueda por teléfono
$response = $client->request('GET', '/api/organization-state-machine-items', [
    'headers' => $this->getHeaders(),
    'query' => [
        'body.phone' => '5525675224'
    ]
]);

// Búsqueda por código de producto
$response = $client->request('GET', '/api/organization-state-machine-items', [
    'headers' => $this->getHeaders(),
    'query' => [
        'body.products.sku' => 'T0123'
    ]
]);

// Búsqueda por monto total
$response = $client->request('GET', '/api/organization-state-machine-items', [
    'headers' => $this->getHeaders(),
    'query' => [
        'body.total' => '48700.53'
    ]
]);

Notas importantes:

  • La búsqueda es exacta (case-sensitive)
  • Funciona con cualquier campo definido en el body del item
  • Los campos numéricos deben enviarse como strings
  • Para campos anidados, usa la notación con punto (ej: products.sku)

Catálogos

Los catálogos permiten definir listas de valores predefinidos que pueden ser referenciados en los items. Por ejemplo, tipos de solicitud, categorías de productos, etc.

Crear un catálogo

// Primero definimos el modelo de los items del catálogo
$itemModel = [
    'type' => 'object',
    'required' => ['name', 'code'],
    'properties' => [
        'name' => [
            'type' => 'string'
        ],
        'code' => [
            'type' => 'string',
        ]
    ],
    'metadata' => [
        'flags' => [
            'active'
        ]
    ]
];

// Creamos el catálogo
$response = $client->request('POST', '/api/organization-catalogs', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json',
        'Authorization' => 'Bearer token'
    ],
    'json' => [
        'organization' => $organizationId,
        'itemModel' => $itemModel
    ]
]);

Crear items del catálogo

$catalog = $response->toArray();

// Ejemplo: Crear tipos de solicitud
$requestTypes = [
    'order' => 'Purchase Order',
    'service' => 'Service Payment'
];

foreach ($requestTypes as $code => $name) {
    $response = $client->request('POST', '/api/organization-catalog-items', [
        'headers' => [
            'Accept' => 'application/ld+json',
            'Content-Type' => 'application/ld+json',
            'Authorization' => 'Bearer token'
        ],
        'json' => [
            'catalog' => $catalog['@id'],
            'body' => [
                'name' => $name,
                'code' => $code,
            ]
        ]
    ]);
}

Usar items del catálogo en el ItemModel

$itemModel = [
    'type' => 'object',
    'required' => ['requestDate', 'requestType', 'project', 'requester', 'department'],
    'properties' => [
        'requestType' => [
            'type' => 'string',
            'pattern' => '^/api/organization-catalog-items/[0-9a-fA-F-]{36}$'
        ],
        // ... otras propiedades
    ]
];

Crear un item referenciando al catálogo

$response = $client->request('POST', '/api/organization-state-machine-items', [
    'headers' => [
        'Accept' => 'application/ld+json',
        'Content-Type' => 'application/ld+json',
        'Authorization' => 'Bearer token'
    ],
    'json' => [
        'state_machine' => $stateMachineId,
        'body' => [
            'requestDate' => date('Y-m-d'),
            'requestType' => '/api/organization-catalog-items/catalog-item-uuid',
            'project' => 'Nombre del Proyecto',
            'requester' => [
                'name' => 'Juan Pérez',
                'position' => 'Gerente'
            ],
            'department' => 'construction'
        ]
    ]
]);

Webhooks

Los webhooks permiten recibir notificaciones y actualizar el estado de los items automáticamente cuando ocurren eventos externos.

Configurar un Webhook en el State Machine

$mapConfig = [
    'transitions' => [
        'payment-received' => [
            'from' => ['payment-link-created'],
            'to' => 'payment-received',
            'validators' => [
                [
                    'validator' => 'internal.webhook',
                    'config' => {
                        'store' => {
                            'payment' => [
                                'amount',
                                'paid_at',
                                'payment_id'
                            ],
                            'webhook_token' => null
                        }
                    }
                ]
            ]
        ]
    ]
];

Recibir Notificaciones

Al crear el state machine, se genera automáticamente:

  • Un código único para el webhook
  • Un token de seguridad
  • Una URL en formato /api/webhooks/{code}/{item_code}

Importante: El token debe enviarse en el header nami-webhook-token. Este token es único para cada webhook y es requerido para que la petición sea procesada.

Ejemplo de Llamada al Webhook

curl -X POST https://api.example.com/api/webhooks/123e4567-e89b/ITEM-001 \
    -H "Content-Type: application/ld+json" \
    -H "nami-webhook-token: abc123" \  # Token requerido para autenticación
    -d '{
        "payment": [{
            "amount": 1000,
            "paid_at": "2025-03-07",
            "payment_id": "PK_123456"
        }]
    }'

Notas sobre seguridad:

  • El token es requerido en cada petición al webhook
  • Si el token no se envía o es inválido, la petición será rechazada
  • Cada webhook tiene su propio token único
  • El token se genera automáticamente al crear el state machine