Files
SekiPOS/blueprints/sync_server.py
2026-06-23 17:20:06 -04:00

267 lines
12 KiB
Python

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