feat: subdomain routing por tenant — refac-xxx.nexusautoparts.com
- Nginx wildcard config: *.nexusautoparts.com routes to POS app with X-Tenant-Subdomain header - middleware_tenant.py: resolves subdomain -> tenant_id via nexus_master.tenants.subdomain - auth_bp: login and employee list endpoints accept tenant from subdomain, URL param, or body - login.html: auto-detects tenant from subdomain, shows business name, falls back to ?tenant=ID - tenant_manager: generates subdomain slug from business name on provision_tenant() - Migration v1.2: adds subdomain column + unique index to tenants table - setup-nginx.sh: one-command install script for the nginx config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
pos/app.py
61
pos/app.py
@@ -3,6 +3,17 @@ from flask import Flask
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# Tenant subdomain resolver (before every request)
|
||||
from middleware_tenant import resolve_tenant
|
||||
app.before_request(resolve_tenant)
|
||||
|
||||
# ─── PWA: Service Worker must be served from /pos/ scope ──────
|
||||
@app.route('/pos/sw.js')
|
||||
def pos_sw():
|
||||
from flask import send_from_directory
|
||||
return send_from_directory('static/pwa', 'sw.js',
|
||||
mimetype='application/javascript')
|
||||
|
||||
# Register blueprints
|
||||
from blueprints.auth_bp import auth_bp
|
||||
app.register_blueprint(auth_bp)
|
||||
@@ -31,16 +42,22 @@ def create_app():
|
||||
from blueprints.accounting_bp import accounting_bp
|
||||
app.register_blueprint(accounting_bp)
|
||||
|
||||
from blueprints.chat_bp import chat_bp
|
||||
app.register_blueprint(chat_bp)
|
||||
|
||||
# Health check
|
||||
@app.route('/pos/health')
|
||||
def health():
|
||||
return {'status': 'ok'}
|
||||
|
||||
from flask import render_template, send_from_directory
|
||||
from flask import render_template, send_from_directory, jsonify, g
|
||||
|
||||
@app.route('/pos/login')
|
||||
def pos_login():
|
||||
return render_template('login.html')
|
||||
return render_template('login.html',
|
||||
tenant_id=getattr(g, 'tenant_id', None),
|
||||
tenant_name=getattr(g, 'tenant_name', None),
|
||||
tenant_subdomain=getattr(g, 'tenant_subdomain', None))
|
||||
|
||||
@app.route('/pos/catalog')
|
||||
def pos_catalog():
|
||||
@@ -82,6 +99,46 @@ def create_app():
|
||||
def pos_static(filename):
|
||||
return send_from_directory('static', filename)
|
||||
|
||||
# ─── Sync: full inventory for offline cache ───────────────────
|
||||
from middleware import require_auth as _require_auth
|
||||
|
||||
@app.route('/pos/api/sync/inventory', methods=['GET'])
|
||||
@_require_auth()
|
||||
def sync_full_inventory():
|
||||
"""Download full inventory for offline cache."""
|
||||
from tenant_db import get_tenant_conn
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
cur = conn.cursor()
|
||||
branch_id = g.branch_id
|
||||
|
||||
cur.execute("""
|
||||
SELECT i.id, i.part_number, i.barcode, i.name, i.brand,
|
||||
i.unit, i.price_1, i.price_2, i.price_3, i.tax_rate,
|
||||
COALESCE(s.stock, 0) AS stock
|
||||
FROM inventory i
|
||||
LEFT JOIN (
|
||||
SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock
|
||||
FROM inventory_operations GROUP BY inventory_id
|
||||
) s ON s.inventory_id = i.id
|
||||
WHERE i.is_active = true AND i.branch_id = %s
|
||||
ORDER BY i.name
|
||||
""", [branch_id])
|
||||
|
||||
items = []
|
||||
for r in cur.fetchall():
|
||||
items.append({
|
||||
'item_id': r[0], 'sku': r[1], 'barcode': r[2],
|
||||
'name': r[3], 'brand': r[4], 'unit': r[5],
|
||||
'price_1': float(r[6]) if r[6] else 0,
|
||||
'price_2': float(r[7]) if r[7] else 0,
|
||||
'price_3': float(r[8]) if r[8] else 0,
|
||||
'tax_rate': float(r[9]) if r[9] else 0.16,
|
||||
'stock': r[10]
|
||||
})
|
||||
cur.close()
|
||||
conn.close()
|
||||
return jsonify({'items': items, 'count': len(items)})
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user