FELDMAN v2.0 - El Contable
- Validación de milestones y bloques - Encadenamiento hash (hash_previo + hash_contenido) - Preparación para blockchain (blockchain_pending) - Tablas: milestones, bloques, feldman_cola, feldman_validaciones - Endpoints: /validar, /stats, /pendientes-blockchain, etc.
This commit is contained in:
425
app.py
Normal file
425
app.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
|
import requests
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
H_INSTANCIA = os.getenv('H_INSTANCIA')
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': os.getenv('DB_HOST', '172.17.0.1'),
|
||||||
|
'port': os.getenv('DB_PORT', '5432'),
|
||||||
|
'dbname': os.getenv('DB_NAME', 'corp'),
|
||||||
|
'user': os.getenv('DB_USER', 'corp'),
|
||||||
|
'password': os.getenv('DB_PASSWORD', 'corp')
|
||||||
|
}
|
||||||
|
|
||||||
|
TIPOS_MILESTONE = ['documento', 'hito', 'contrato', 'estado', 'decision']
|
||||||
|
TIPOS_BLOQUE = ['trabajo', 'verificacion', 'entrega', 'medicion', 'firma']
|
||||||
|
TIPOS_EVIDENCIA = ['image/jpeg', 'image/png', 'audio/mp3', 'audio/wav', 'video/mp4', 'application/pdf']
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth_key = request.headers.get('X-Auth-Key')
|
||||||
|
if auth_key != H_INSTANCIA:
|
||||||
|
return jsonify({'error': 'No autorizado'}), 401
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def calcular_hash_contenido(datos: dict) -> str:
|
||||||
|
datos_ordenados = json.dumps(datos, sort_keys=True, ensure_ascii=False)
|
||||||
|
return hashlib.sha256(datos_ordenados.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def calcular_hash_registro(hash_previo: str, hash_contenido: str) -> str:
|
||||||
|
cadena = f'{hash_previo}:{hash_contenido}'
|
||||||
|
return hashlib.sha256(cadena.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validar_milestone(datos: dict) -> tuple:
|
||||||
|
errores = []
|
||||||
|
reglas = []
|
||||||
|
|
||||||
|
alias_ok = bool(datos.get('alias'))
|
||||||
|
if not alias_ok:
|
||||||
|
errores.append('M-001: Alias es requerido')
|
||||||
|
reglas.append({'codigo': 'M-001', 'ok': alias_ok})
|
||||||
|
|
||||||
|
tipo = datos.get('tipo_item')
|
||||||
|
tipo_ok = tipo in TIPOS_MILESTONE
|
||||||
|
if not tipo_ok:
|
||||||
|
errores.append(f'M-002: tipo_item debe ser uno de {TIPOS_MILESTONE}')
|
||||||
|
reglas.append({'codigo': 'M-002', 'ok': tipo_ok})
|
||||||
|
|
||||||
|
proyecto_ok = bool(datos.get('proyecto_tag'))
|
||||||
|
if not proyecto_ok:
|
||||||
|
errores.append('M-003: proyecto_tag es requerido')
|
||||||
|
reglas.append({'codigo': 'M-003', 'ok': proyecto_ok})
|
||||||
|
|
||||||
|
return (len(errores) == 0, errores, reglas)
|
||||||
|
|
||||||
|
|
||||||
|
def validar_bloque(datos: dict) -> tuple:
|
||||||
|
errores = []
|
||||||
|
reglas = []
|
||||||
|
|
||||||
|
alias_ok = bool(datos.get('alias'))
|
||||||
|
if not alias_ok:
|
||||||
|
errores.append('B-001: Alias es requerido')
|
||||||
|
reglas.append({'codigo': 'B-001', 'ok': alias_ok})
|
||||||
|
|
||||||
|
tipo = datos.get('tipo_accion')
|
||||||
|
tipo_ok = tipo in TIPOS_BLOQUE
|
||||||
|
if not tipo_ok:
|
||||||
|
errores.append(f'B-002: tipo_accion debe ser uno de {TIPOS_BLOQUE}')
|
||||||
|
reglas.append({'codigo': 'B-002', 'ok': tipo_ok})
|
||||||
|
|
||||||
|
proyecto_ok = bool(datos.get('proyecto_tag'))
|
||||||
|
if not proyecto_ok:
|
||||||
|
errores.append('B-003: proyecto_tag es requerido')
|
||||||
|
reglas.append({'codigo': 'B-003', 'ok': proyecto_ok})
|
||||||
|
|
||||||
|
ev_hash = datos.get('evidencia_hash', '')
|
||||||
|
hash_ok = len(ev_hash) == 64
|
||||||
|
if not hash_ok:
|
||||||
|
errores.append('B-004: evidencia_hash debe ser SHA256 (64 caracteres)')
|
||||||
|
reglas.append({'codigo': 'B-004', 'ok': hash_ok})
|
||||||
|
|
||||||
|
ev_url = datos.get('evidencia_url', '')
|
||||||
|
url_ok = ev_url.startswith('https://')
|
||||||
|
if not url_ok:
|
||||||
|
errores.append('B-005: evidencia_url debe ser HTTPS')
|
||||||
|
reglas.append({'codigo': 'B-005', 'ok': url_ok})
|
||||||
|
|
||||||
|
ev_tipo = datos.get('evidencia_tipo')
|
||||||
|
tipo_ev_ok = ev_tipo in TIPOS_EVIDENCIA
|
||||||
|
if not tipo_ev_ok:
|
||||||
|
errores.append(f'B-006: evidencia_tipo debe ser uno de {TIPOS_EVIDENCIA}')
|
||||||
|
reglas.append({'codigo': 'B-006', 'ok': tipo_ev_ok})
|
||||||
|
|
||||||
|
evidencia_existe = False
|
||||||
|
if url_ok:
|
||||||
|
try:
|
||||||
|
resp = requests.head(ev_url, timeout=5)
|
||||||
|
evidencia_existe = resp.status_code == 200
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not evidencia_existe:
|
||||||
|
errores.append('B-007: La evidencia no existe o no es accesible')
|
||||||
|
reglas.append({'codigo': 'B-007', 'ok': evidencia_existe})
|
||||||
|
|
||||||
|
return (len(errores) == 0, errores, reglas)
|
||||||
|
|
||||||
|
|
||||||
|
def crear_milestone(cur, datos: dict) -> str:
|
||||||
|
cur.execute('SELECT get_ultimo_hash_milestone(%s) as hash', (H_INSTANCIA,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
hash_previo = row['hash'] if row else 'GENESIS'
|
||||||
|
|
||||||
|
cur.execute('SELECT get_siguiente_secuencia_milestone(%s) as seq', (H_INSTANCIA,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
secuencia = row['seq'] if row else 1
|
||||||
|
|
||||||
|
hash_contenido = calcular_hash_contenido(datos)
|
||||||
|
h_milestone = calcular_hash_registro(hash_previo, hash_contenido)
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO milestones (
|
||||||
|
h_milestone, h_instancia, secuencia, hash_previo, hash_contenido,
|
||||||
|
alias, tipo_item, descripcion, datos,
|
||||||
|
etiqueta_principal, proyecto_tag, id_padre_milestone, blockchain_pending
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, TRUE)
|
||||||
|
''', (
|
||||||
|
h_milestone, H_INSTANCIA, secuencia, hash_previo, hash_contenido,
|
||||||
|
datos.get('alias'), datos.get('tipo_item'), datos.get('descripcion'), json.dumps(datos),
|
||||||
|
datos.get('etiqueta_principal'), datos.get('proyecto_tag'), datos.get('id_padre_milestone')
|
||||||
|
))
|
||||||
|
return h_milestone
|
||||||
|
|
||||||
|
|
||||||
|
def crear_bloque(cur, datos: dict) -> str:
|
||||||
|
cur.execute('SELECT get_ultimo_hash_bloque(%s) as hash', (H_INSTANCIA,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
hash_previo = row['hash'] if row else 'GENESIS'
|
||||||
|
|
||||||
|
cur.execute('SELECT get_siguiente_secuencia_bloque(%s) as seq', (H_INSTANCIA,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
secuencia = row['seq'] if row else 1
|
||||||
|
|
||||||
|
hash_contenido = calcular_hash_contenido(datos)
|
||||||
|
h_bloque = calcular_hash_registro(hash_previo, hash_contenido)
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO bloques (
|
||||||
|
h_bloque, h_instancia, secuencia, hash_previo, hash_contenido,
|
||||||
|
alias, tipo_accion, descripcion, datos,
|
||||||
|
evidencia_hash, evidencia_url, evidencia_tipo,
|
||||||
|
etiqueta_principal, proyecto_tag, id_padre_bloque, id_milestone_asociado, blockchain_pending
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, TRUE)
|
||||||
|
''', (
|
||||||
|
h_bloque, H_INSTANCIA, secuencia, hash_previo, hash_contenido,
|
||||||
|
datos.get('alias'), datos.get('tipo_accion'), datos.get('descripcion'), json.dumps(datos),
|
||||||
|
datos.get('evidencia_hash'), datos.get('evidencia_url'), datos.get('evidencia_tipo'),
|
||||||
|
datos.get('etiqueta_principal'), datos.get('proyecto_tag'),
|
||||||
|
datos.get('id_padre_bloque'), datos.get('id_milestone_asociado')
|
||||||
|
))
|
||||||
|
return h_bloque
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/health')
|
||||||
|
def health():
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('SELECT 1')
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({
|
||||||
|
'service': 'feldman',
|
||||||
|
'status': 'healthy',
|
||||||
|
'version': '2.0',
|
||||||
|
'rol': 'contable-validador'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'unhealthy', 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/s-contract')
|
||||||
|
def s_contract():
|
||||||
|
return jsonify({
|
||||||
|
'service': 'feldman',
|
||||||
|
'version': '2.0',
|
||||||
|
'contract_version': 'S-CONTRACT v2.1',
|
||||||
|
'rol': 'El Contable - Validador y preparador para blockchain',
|
||||||
|
'endpoints': {
|
||||||
|
'/validar': {'method': 'POST', 'auth': True},
|
||||||
|
'/estado/<h>': {'method': 'GET', 'auth': True},
|
||||||
|
'/stats': {'method': 'GET', 'auth': True},
|
||||||
|
'/pendientes-blockchain': {'method': 'GET', 'auth': True},
|
||||||
|
'/milestone/<h>': {'method': 'GET', 'auth': True},
|
||||||
|
'/bloque/<h>': {'method': 'GET', 'auth': True},
|
||||||
|
'/milestones': {'method': 'GET', 'auth': True},
|
||||||
|
'/bloques': {'method': 'GET', 'auth': True}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/validar', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def validar():
|
||||||
|
data = request.json
|
||||||
|
origen = data.get('origen')
|
||||||
|
h_origen = data.get('h_origen')
|
||||||
|
tipo_destino = data.get('tipo_destino')
|
||||||
|
datos = data.get('datos', {})
|
||||||
|
|
||||||
|
if tipo_destino not in ['milestone', 'bloque']:
|
||||||
|
return jsonify({'error': 'tipo_destino debe ser milestone o bloque'}), 400
|
||||||
|
|
||||||
|
h_entrada = calcular_hash_contenido({
|
||||||
|
'origen': origen, 'h_origen': h_origen,
|
||||||
|
'tipo_destino': tipo_destino, 'datos': datos,
|
||||||
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO feldman_cola (h_entrada, h_instancia, origen, h_origen, tipo_destino, datos, estado)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, 'pendiente')
|
||||||
|
ON CONFLICT (h_entrada) DO NOTHING
|
||||||
|
''', (h_entrada, H_INSTANCIA, origen, h_origen, tipo_destino, json.dumps(datos)))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if tipo_destino == 'milestone':
|
||||||
|
ok, errores, reglas = validar_milestone(datos)
|
||||||
|
else:
|
||||||
|
ok, errores, reglas = validar_bloque(datos)
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO feldman_validaciones (h_entrada, validacion_ok, reglas_aplicadas)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
''', (h_entrada, ok, json.dumps(reglas)))
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
cur.execute('''
|
||||||
|
UPDATE feldman_cola SET estado = 'error', error_mensaje = %s WHERE h_entrada = %s
|
||||||
|
''', ('; '.join(errores), h_entrada))
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'ok': False, 'h_entrada': h_entrada, 'estado': 'error', 'errores': errores})
|
||||||
|
|
||||||
|
if tipo_destino == 'milestone':
|
||||||
|
h_registro = crear_milestone(cur, datos)
|
||||||
|
else:
|
||||||
|
h_registro = crear_bloque(cur, datos)
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
UPDATE feldman_validaciones SET tipo_registro = %s, h_registro = %s WHERE h_entrada = %s
|
||||||
|
''', (tipo_destino, h_registro, h_entrada))
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
UPDATE feldman_cola SET estado = 'ok', processed_at = CURRENT_TIMESTAMP WHERE h_entrada = %s
|
||||||
|
''', (h_entrada,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'ok': True, 'h_entrada': h_entrada, 'estado': 'ok',
|
||||||
|
'tipo_registro': tipo_destino, 'h_registro': h_registro,
|
||||||
|
'mensaje': f'{tipo_destino.capitalize()} creado y encadenado'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/estado/<h_entrada>')
|
||||||
|
@require_auth
|
||||||
|
def estado(h_entrada):
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('''
|
||||||
|
SELECT c.*, v.validacion_ok, v.reglas_aplicadas, v.tipo_registro, v.h_registro
|
||||||
|
FROM feldman_cola c
|
||||||
|
LEFT JOIN feldman_validaciones v ON c.h_entrada = v.h_entrada
|
||||||
|
WHERE c.h_entrada = %s
|
||||||
|
''', (h_entrada,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
if not result:
|
||||||
|
return jsonify({'error': 'No encontrado'}), 404
|
||||||
|
return jsonify(dict(result))
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/stats')
|
||||||
|
@require_auth
|
||||||
|
def stats():
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('SELECT COUNT(*) as c FROM milestones WHERE h_instancia = %s', (H_INSTANCIA,))
|
||||||
|
total_milestones = cur.fetchone()['c']
|
||||||
|
cur.execute('SELECT COUNT(*) as c FROM bloques WHERE h_instancia = %s', (H_INSTANCIA,))
|
||||||
|
total_bloques = cur.fetchone()['c']
|
||||||
|
cur.execute('SELECT COUNT(*) as c FROM feldman_validaciones WHERE validated_at >= CURRENT_DATE')
|
||||||
|
validaciones_hoy = cur.fetchone()['c']
|
||||||
|
cur.execute("SELECT COUNT(*) as c FROM feldman_cola WHERE estado = 'error' AND created_at >= CURRENT_DATE")
|
||||||
|
errores_hoy = cur.fetchone()['c']
|
||||||
|
tasa = 100.0 if validaciones_hoy == 0 else ((validaciones_hoy - errores_hoy) / validaciones_hoy) * 100
|
||||||
|
return jsonify({
|
||||||
|
'total_milestones': total_milestones, 'total_bloques': total_bloques,
|
||||||
|
'validaciones_hoy': validaciones_hoy, 'errores_hoy': errores_hoy,
|
||||||
|
'tasa_exito': round(tasa, 1)
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/pendientes-blockchain')
|
||||||
|
@require_auth
|
||||||
|
def pendientes_blockchain():
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('SELECT COUNT(*) as c FROM milestones WHERE h_instancia = %s AND blockchain_pending = TRUE', (H_INSTANCIA,))
|
||||||
|
mp = cur.fetchone()['c']
|
||||||
|
cur.execute('SELECT COUNT(*) as c FROM bloques WHERE h_instancia = %s AND blockchain_pending = TRUE', (H_INSTANCIA,))
|
||||||
|
bp = cur.fetchone()['c']
|
||||||
|
return jsonify({'milestones_pendientes': mp, 'bloques_pendientes': bp})
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/milestone/<h_milestone>')
|
||||||
|
@require_auth
|
||||||
|
def get_milestone(h_milestone):
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('SELECT * FROM milestones WHERE h_milestone = %s', (h_milestone,))
|
||||||
|
m = cur.fetchone()
|
||||||
|
if not m:
|
||||||
|
return jsonify({'error': 'No encontrado'}), 404
|
||||||
|
return jsonify(dict(m))
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/bloque/<h_bloque>')
|
||||||
|
@require_auth
|
||||||
|
def get_bloque(h_bloque):
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('SELECT * FROM bloques WHERE h_bloque = %s', (h_bloque,))
|
||||||
|
b = cur.fetchone()
|
||||||
|
if not b:
|
||||||
|
return jsonify({'error': 'No encontrado'}), 404
|
||||||
|
return jsonify(dict(b))
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/milestones')
|
||||||
|
@require_auth
|
||||||
|
def list_milestones():
|
||||||
|
proyecto = request.args.get('proyecto')
|
||||||
|
limit = request.args.get('limit', 50, type=int)
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
if proyecto:
|
||||||
|
cur.execute('SELECT * FROM milestones WHERE h_instancia = %s AND proyecto_tag = %s ORDER BY secuencia DESC LIMIT %s', (H_INSTANCIA, proyecto, limit))
|
||||||
|
else:
|
||||||
|
cur.execute('SELECT * FROM milestones WHERE h_instancia = %s ORDER BY secuencia DESC LIMIT %s', (H_INSTANCIA, limit))
|
||||||
|
return jsonify({'milestones': [dict(m) for m in cur.fetchall()]})
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/bloques')
|
||||||
|
@require_auth
|
||||||
|
def list_bloques():
|
||||||
|
proyecto = request.args.get('proyecto')
|
||||||
|
limit = request.args.get('limit', 50, type=int)
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
if proyecto:
|
||||||
|
cur.execute('SELECT * FROM bloques WHERE h_instancia = %s AND proyecto_tag = %s ORDER BY secuencia DESC LIMIT %s', (H_INSTANCIA, proyecto, limit))
|
||||||
|
else:
|
||||||
|
cur.execute('SELECT * FROM bloques WHERE h_instancia = %s ORDER BY secuencia DESC LIMIT %s', (H_INSTANCIA, limit))
|
||||||
|
return jsonify({'bloques': [dict(b) for b in cur.fetchall()]})
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=5054, debug=False)
|
||||||
123
init_libros_contables.sql
Normal file
123
init_libros_contables.sql
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
-- Los Libros Contables - FELDMAN v2.0
|
||||||
|
-- Tablas para milestones, bloques y validaciones
|
||||||
|
|
||||||
|
-- MILESTONES
|
||||||
|
CREATE TABLE IF NOT EXISTS milestones (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
h_milestone VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
h_instancia VARCHAR(64) NOT NULL,
|
||||||
|
secuencia BIGINT NOT NULL,
|
||||||
|
hash_previo VARCHAR(64),
|
||||||
|
hash_contenido VARCHAR(64) NOT NULL,
|
||||||
|
alias VARCHAR(200) NOT NULL,
|
||||||
|
tipo_item VARCHAR(50) NOT NULL,
|
||||||
|
descripcion TEXT,
|
||||||
|
datos JSONB DEFAULT '{}',
|
||||||
|
etiqueta_principal VARCHAR(64),
|
||||||
|
proyecto_tag VARCHAR(64),
|
||||||
|
id_padre_milestone BIGINT REFERENCES milestones(id),
|
||||||
|
id_bloque_asociado BIGINT,
|
||||||
|
blockchain_pending BOOLEAN DEFAULT TRUE,
|
||||||
|
blockchain_tx_ref VARCHAR(128),
|
||||||
|
notario_batch_id VARCHAR(64),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(64),
|
||||||
|
CONSTRAINT milestone_secuencia_unica UNIQUE (h_instancia, secuencia)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- BLOQUES
|
||||||
|
CREATE TABLE IF NOT EXISTS bloques (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
h_bloque VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
h_instancia VARCHAR(64) NOT NULL,
|
||||||
|
secuencia BIGINT NOT NULL,
|
||||||
|
hash_previo VARCHAR(64),
|
||||||
|
hash_contenido VARCHAR(64) NOT NULL,
|
||||||
|
alias VARCHAR(200) NOT NULL,
|
||||||
|
tipo_accion VARCHAR(50) NOT NULL,
|
||||||
|
descripcion TEXT,
|
||||||
|
datos JSONB DEFAULT '{}',
|
||||||
|
evidencia_hash VARCHAR(64) NOT NULL,
|
||||||
|
evidencia_url VARCHAR(500) NOT NULL,
|
||||||
|
evidencia_tipo VARCHAR(50) NOT NULL,
|
||||||
|
etiqueta_principal VARCHAR(64),
|
||||||
|
proyecto_tag VARCHAR(64),
|
||||||
|
id_padre_bloque BIGINT REFERENCES bloques(id),
|
||||||
|
id_milestone_asociado BIGINT REFERENCES milestones(id),
|
||||||
|
blockchain_pending BOOLEAN DEFAULT TRUE,
|
||||||
|
blockchain_tx_ref VARCHAR(128),
|
||||||
|
notario_batch_id VARCHAR(64),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(64),
|
||||||
|
CONSTRAINT bloque_secuencia_unica UNIQUE (h_instancia, secuencia)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- COLA DE VALIDACION
|
||||||
|
CREATE TABLE IF NOT EXISTS feldman_cola (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
h_entrada VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
h_instancia VARCHAR(64) NOT NULL,
|
||||||
|
origen VARCHAR(50) NOT NULL,
|
||||||
|
h_origen VARCHAR(64),
|
||||||
|
tipo_destino VARCHAR(20) NOT NULL,
|
||||||
|
datos JSONB NOT NULL,
|
||||||
|
estado VARCHAR(20) DEFAULT 'pendiente',
|
||||||
|
error_mensaje TEXT,
|
||||||
|
intentos INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
processed_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- VALIDACIONES
|
||||||
|
CREATE TABLE IF NOT EXISTS feldman_validaciones (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
h_entrada VARCHAR(64) NOT NULL,
|
||||||
|
validacion_ok BOOLEAN NOT NULL,
|
||||||
|
reglas_aplicadas JSONB NOT NULL,
|
||||||
|
tipo_registro VARCHAR(20),
|
||||||
|
h_registro VARCHAR(64),
|
||||||
|
validated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- INDICES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_milestones_pending ON milestones(blockchain_pending) WHERE blockchain_pending = TRUE;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bloques_pending ON bloques(blockchain_pending) WHERE blockchain_pending = TRUE;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_milestones_proyecto ON milestones(proyecto_tag);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bloques_proyecto ON bloques(proyecto_tag);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_feldman_estado ON feldman_cola(estado);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_feldman_instancia ON feldman_cola(h_instancia);
|
||||||
|
|
||||||
|
-- FUNCIONES
|
||||||
|
CREATE OR REPLACE FUNCTION get_ultimo_hash_milestone(p_h_instancia VARCHAR)
|
||||||
|
RETURNS VARCHAR AS $$
|
||||||
|
DECLARE v_hash VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT hash_contenido INTO v_hash FROM milestones
|
||||||
|
WHERE h_instancia = p_h_instancia ORDER BY secuencia DESC LIMIT 1;
|
||||||
|
RETURN COALESCE(v_hash, 'GENESIS');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_ultimo_hash_bloque(p_h_instancia VARCHAR)
|
||||||
|
RETURNS VARCHAR AS $$
|
||||||
|
DECLARE v_hash VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT hash_contenido INTO v_hash FROM bloques
|
||||||
|
WHERE h_instancia = p_h_instancia ORDER BY secuencia DESC LIMIT 1;
|
||||||
|
RETURN COALESCE(v_hash, 'GENESIS');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_siguiente_secuencia_milestone(p_h_instancia VARCHAR)
|
||||||
|
RETURNS BIGINT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN (SELECT COALESCE(MAX(secuencia), 0) + 1 FROM milestones WHERE h_instancia = p_h_instancia);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_siguiente_secuencia_bloque(p_h_instancia VARCHAR)
|
||||||
|
RETURNS BIGINT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN (SELECT COALESCE(MAX(secuencia), 0) + 1 FROM bloques WHERE h_instancia = p_h_instancia);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
gunicorn==21.2.0
|
||||||
|
requests>=2.31.0
|
||||||
Reference in New Issue
Block a user