from datetime import datetime, timezone from flask import Blueprint, request, jsonify, current_app from functools import wraps sync_bp = Blueprint('sync', __name__) def require_sync_secret(f): @wraps(f) def decorated(*args, **kwargs): secret = current_app.config.get('SYNC_SECRET', '') if secret and request.headers.get('X-Sync-Secret') != secret: return jsonify({"error": "unauthorized"}), 401 return f(*args, **kwargs) return decorated @sync_bp.route('/api/ping') def ping(): return jsonify({"status": "ok"}), 200 @sync_bp.route('/api/sync/push', methods=['POST']) @require_sync_secret def sync_push(): data = request.get_json() if not data: return jsonify({"error": "empty payload"}), 400 instance_id = data.get('instance_id', 'unknown') synced_at = data.get('synced_at') or datetime.now(timezone.utc).isoformat() from core.db import get_db_connection with get_db_connection() as conn: # 1. Deletions for d in data.get('deletions', []): et = d['entity_type'] eu = d['entity_uuid'] # Log to server's sync_deletions so other pulling clients see it existing = conn.execute( "SELECT id FROM sync_deletions WHERE entity_type = ? AND entity_uuid = ?", (et, eu) ).fetchone() if not existing: conn.execute( "INSERT INTO sync_deletions (entity_type, entity_uuid, deleted_at) VALUES (?, ?, ?)", (et, eu, synced_at) ) if et == 'debtor': conn.execute("DELETE FROM debtors WHERE uuid = ?", (eu,)) elif et == 'ticket': conn.execute("DELETE FROM debtor_tickets WHERE uuid = ?", (eu,)) elif et == 'dicom': conn.execute("DELETE FROM dicom WHERE uuid = ?", (eu,)) elif et == 'expense': conn.execute("DELETE FROM expenses WHERE uuid = ?", (eu,)) elif et == 'sale': conn.execute("DELETE FROM sales WHERE uuid = ?", (eu,)) elif et == 'product': conn.execute("DELETE FROM products WHERE barcode = ?", (eu,)) # 2. Sales for sale in data.get('sales', []): existing = conn.execute( "SELECT id FROM sales WHERE uuid = ?", (sale['uuid'],) ).fetchone() if existing: continue cur = conn.execute( "INSERT INTO sales (date, total, payment_method, uuid, synced_at) VALUES (?, ?, ?, ?, ?)", (sale.get('date', synced_at), sale['total'], sale.get('payment_method', 'efectivo'), sale['uuid'], synced_at) ) sale_id = cur.lastrowid for item in sale.get('items', []): conn.execute( "INSERT INTO sale_items (sale_id, barcode, name, price, quantity, subtotal) VALUES (?, ?, ?, ?, ?, ?)", (sale_id, item['barcode'], item['name'], item.get('price', 0), item.get('qty', 1), item.get('subtotal', 0)) ) # 3. Products now = datetime.now(timezone.utc).isoformat() for p in data.get('products', []): existing = conn.execute( "SELECT updated_at FROM products WHERE barcode = ?", (p['barcode'],) ).fetchone() if existing and existing[0] >= p.get('updated_at', now): continue conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type, updated_at, updated_by) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(barcode) DO UPDATE SET name=excluded.name, price=excluded.price, image_url=excluded.image_url, stock=excluded.stock, unit_type=excluded.unit_type, updated_at=excluded.updated_at, updated_by=excluded.updated_by''', (p['barcode'], p.get('name', ''), p.get('price', 0), p.get('image_url'), p.get('stock', 0), p.get('unit_type', 'unit'), p.get('updated_at', now), instance_id)) # 4. Debtors for d in data.get('debtors', []): conn.execute('''INSERT INTO debtors (uuid, name, contact_info, updated_at, updated_by) VALUES (?,?,?,?,?) ON CONFLICT(uuid) DO UPDATE SET name=excluded.name, contact_info=excluded.contact_info, updated_at=excluded.updated_at, updated_by=excluded.updated_by''', (d['uuid'], d['name'], d.get('contact_info', ''), d.get('updated_at', now), d.get('updated_by', instance_id))) # 5. Dicom (legacy) for d in data.get('dicom', []): conn.execute('''INSERT INTO dicom (uuid, name, amount, notes, image_url, updated_at, updated_by) VALUES (?,?,?,?,?,?,?) ON CONFLICT(uuid) DO UPDATE SET name=excluded.name, amount=excluded.amount, notes=excluded.notes, image_url=excluded.image_url, updated_at=excluded.updated_at, updated_by=excluded.updated_by''', (d['uuid'], d['name'], d.get('amount', 0), d.get('notes', ''), d.get('image_url', ''), d.get('updated_at', now), d.get('updated_by', instance_id))) # 6. Debtor tickets with items for t in data.get('tickets', []): existing = conn.execute( "SELECT id FROM debtor_tickets WHERE uuid = ?", (t['uuid'],) ).fetchone() if existing: continue debtor_row = conn.execute( "SELECT id FROM debtors WHERE uuid = ?", (t['debtor_uuid'],) ).fetchone() if not debtor_row: continue cur = conn.execute( "INSERT INTO debtor_tickets (uuid, debtor_id, date, total, amount_paid, status, updated_at, updated_by) VALUES (?,?,?,?,?,?,?,?)", (t['uuid'], debtor_row[0], t.get('date', synced_at), t['total'], t.get('amount_paid', 0), t.get('status', 'unpaid'), t.get('updated_at', now), t.get('updated_by', instance_id)) ) ticket_id = cur.lastrowid for item in t.get('items', []): conn.execute( "INSERT INTO debtor_ticket_items (ticket_id, barcode, name, price, quantity, subtotal) VALUES (?,?,?,?,?,?)", (ticket_id, item['barcode'], item['name'], item.get('price', 0), item.get('qty', 1), item.get('subtotal', 0)) ) # 7. Expenses for e in data.get('expenses', []): existing = conn.execute( "SELECT id FROM expenses WHERE uuid = ?", (e['uuid'],) ).fetchone() if existing: continue conn.execute( "INSERT INTO expenses (uuid, date, description, amount) VALUES (?,?,?,?)", (e['uuid'], e.get('date', synced_at), e['description'], e['amount']) ) conn.commit() return jsonify({"status": "ok", "instance_id": instance_id}), 200 @sync_bp.route('/api/sync/pull') @require_sync_secret def sync_pull(): since = request.args.get('since', '1970-01-01') since_sales = request.args.get('since_sales', '1970-01-01') since_debtors = request.args.get('since_debtors', '1970-01-01') since_tickets = request.args.get('since_tickets', '1970-01-01') since_dicom = request.args.get('since_dicom', '1970-01-01') since_expenses = request.args.get('since_expenses', '1970-01-01') since_deletions = request.args.get('since_deletions', '1970-01-01') from core.db import get_db_connection with get_db_connection() as conn: # Products products = conn.execute( "SELECT barcode, name, price, image_url, stock, unit_type, updated_at, updated_by FROM products WHERE updated_at >= ?", (since,) ).fetchall() # Sales with items sales_rows = conn.execute( "SELECT id, uuid, date, total, payment_method FROM sales WHERE date >= ? ORDER BY date", (since_sales,) ).fetchall() sales_payload = [] for s in sales_rows: items = conn.execute( "SELECT barcode, name, price, quantity, subtotal FROM sale_items WHERE sale_id = ?", (s[0],) ).fetchall() sales_payload.append({ "uuid": s[1], "date": s[2], "total": s[3], "payment_method": s[4], "items": [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items] }) # Debtors debtors = conn.execute( "SELECT uuid, name, contact_info, updated_at, updated_by FROM debtors WHERE updated_at >= ?", (since_debtors,) ).fetchall() # Dicom (legacy) dicom_rows = conn.execute( "SELECT uuid, name, amount, notes, image_url, updated_at, updated_by FROM dicom WHERE updated_at >= ?", (since_dicom,) ).fetchall() # Debtor tickets with items tickets = conn.execute( "SELECT id, uuid, debtor_id, date, total, amount_paid, status, updated_at, updated_by FROM debtor_tickets WHERE updated_at >= ?", (since_tickets,) ).fetchall() ticket_payload = [] for t in tickets: ticket_id, ticket_uuid, debtor_id = t[0], t[1], t[2] debtor_uuid = conn.execute("SELECT uuid FROM debtors WHERE id = ?", (debtor_id,)).fetchone() items = conn.execute( "SELECT barcode, name, price, quantity, subtotal FROM debtor_ticket_items WHERE ticket_id = ?", (ticket_id,) ).fetchall() ticket_payload.append({ "uuid": ticket_uuid, "debtor_uuid": debtor_uuid[0] if debtor_uuid else '', "date": t[3], "total": t[4], "amount_paid": t[5], "status": t[6], "updated_at": t[7], "updated_by": t[8], "items": [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items] }) # Expenses expenses = conn.execute( "SELECT uuid, date, description, amount FROM expenses WHERE date >= ?", (since_expenses,) ).fetchall() # Deletions deletions = conn.execute( "SELECT entity_type, entity_uuid, deleted_at FROM sync_deletions WHERE deleted_at >= ?", (since_deletions,) ).fetchall() return jsonify({ "products": [ {"barcode": p[0], "name": p[1], "price": p[2], "image_url": p[3], "stock": p[4], "unit_type": p[5], "updated_at": p[6], "updated_by": p[7]} for p in products ], "sales": sales_payload, "debtors": [ {"uuid": d[0], "name": d[1], "contact_info": d[2], "updated_at": d[3], "updated_by": d[4]} for d in debtors ], "dicom": [ {"uuid": d[0], "name": d[1], "amount": d[2], "notes": d[3], "image_url": d[4], "updated_at": d[5], "updated_by": d[6]} for d in dicom_rows ], "tickets": ticket_payload, "expenses": [ {"uuid": e[0], "date": e[1], "description": e[2], "amount": e[3]} for e in expenses ], "deletions": [ {"entity_type": d[0], "entity_uuid": d[1], "deleted_at": d[2]} for d in deletions ] }), 200