2026-10-08 19:39:38 +01:00
#!/usr/bin/env python3
import json
import re
import ipaddress
import mimetypes
import os
import secrets
import socket
2026-10-08 15:07:31 -04:00
import ssl
import http.client
2026-10-08 19:39:38 +01:00
import sqlite3
import subprocess
import threading
import business_security as security
import business_rollout as rollout
import screen_activity
from datetime import datetime , timezone
from http.server import BaseHTTPRequestHandler , ThreadingHTTPServer
from http.cookies import CookieError
from pathlib import Path
from urllib.parse import parse_qs , urlparse
from urllib.parse import urlencode
from urllib.error import HTTPError
from urllib.request import Request , urlopen , build_opener , HTTPRedirectHandler
from urllib.parse import quote
ROOT = Path ( __file__ ) . resolve () . parent
PUBLIC = ROOT / "public"
NAPLET_DIST = ROOT / "napplets/order-to-table/dist"
DATA_DIR = Path ( os . environ . get ( "JWB_DATA_DIR" , ROOT / "data" ))
DB_FILE = DATA_DIR / "business.sqlite3"
HOST = os . environ . get ( "JWB_HOST" , "127.0.0.1" )
PORT = int ( os . environ . get ( "JWB_PORT" , "4310" ))
BASE = os . environ . get ( "JWB_BASE_PATH" , "/business-demo" ) . rstrip ( "/" )
LOCK = threading . RLock ()
PAYMENT_HELPER = ROOT / "payment-helper.cjs"
PAYMENT_RELAYS = [ "wss://relay.primal.net" , "wss://nos.lol" ]
PAYMENT_RECIPIENT = "f81611363554b64306467234d7396ec88455707633f54738f6c4683535098cd3"
LNURL_URL = "https://primal.net/.well-known/lnurlp/fucking"
DEV_LIGHTNING_ADDRESS = "fucking@primal.net" # dev stand-in matching LNURL_URL, used when a merchant has no real lud16
LNURL_CALLBACK = "https://primal.net/lnurlp/fucking/callback"
MODULE_DEFAULTS = {
"order" : ( 1 , "live" ), "pay" : ( 1 , "live" ), "payandgo" : ( 1 , "preview" ),
"booking" : ( 1 , "live" ), "screen" : ( 1 , "preview" ),
"gallery" : ( 1 , "preview" ), "stats" : ( 1 , "live" ), "events" : ( 0 , "coming-soon" ),
}
def now ():
return datetime . now ( timezone . utc ) . isoformat ( timespec = "seconds" )
def db ():
DATA_DIR . mkdir ( parents = True , exist_ok = True )
conn = sqlite3 . connect ( DB_FILE , timeout = 10 )
conn . row_factory = sqlite3 . Row
security . migrate ( conn )
conn . execute ( "PRAGMA journal_mode=WAL" )
conn . executescript ( """
CREATE TABLE IF NOT EXISTS merchant (
id TEXT PRIMARY KEY, name TEXT NOT NULL, module_enabled INTEGER NOT NULL,
table_count INTEGER NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, name TEXT NOT NULL,
price_cents INTEGER NOT NULL, enabled INTEGER NOT NULL, sort_order INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tables (
merchant_id TEXT NOT NULL, table_number INTEGER NOT NULL, token TEXT NOT NULL UNIQUE,
PRIMARY KEY(merchant_id, table_number)
);
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, table_number INTEGER NOT NULL,
items_json TEXT NOT NULL, note TEXT NOT NULL, status TEXT NOT NULL,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY, order_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
table_number INTEGER NOT NULL, amount_cents INTEGER NOT NULL,
amount_sats INTEGER NOT NULL, bolt11 TEXT NOT NULL, zap_pubkey TEXT NOT NULL,
status TEXT NOT NULL, receipt_id TEXT, created_at TEXT NOT NULL, settled_at TEXT
);
CREATE TABLE IF NOT EXISTS quick_payments (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, merchant_name TEXT NOT NULL,
amount_cents INTEGER NOT NULL, amount_sats INTEGER NOT NULL, bolt11 TEXT NOT NULL,
zap_pubkey TEXT, recipient TEXT, status TEXT NOT NULL, receipt_id TEXT,
created_at TEXT NOT NULL, settled_at TEXT
);
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, guest_name TEXT NOT NULL,
guests INTEGER NOT NULL, scheduled_at TEXT NOT NULL, mode TEXT NOT NULL,
notes TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS merchant_modules (
merchant_id TEXT NOT NULL, module_id TEXT NOT NULL, enabled INTEGER NOT NULL,
status TEXT NOT NULL, config_json TEXT NOT NULL, updated_at TEXT NOT NULL,
PRIMARY KEY(merchant_id,module_id)
);
""" )
legacy = rollout . legacy_merchant ( conn )
conn . execute ( "INSERT OR IGNORE INTO merchant VALUES (?,'LocalPub Madeira — Napplet Demo',1,30,?)" , ( legacy , now ()))
defaults = [( "poncha" , "Poncha Fisherman" , 350 , 1 ), ( "nikita" , "Nikita" , 450 , 2 ), ( "coral" , "Coral" , 250 , 3 )]
for pid , name , cents , order in defaults :
conn . execute ( "INSERT OR IGNORE INTO products VALUES (?, ?, ?, ?, 1, ?)" , ( pid , legacy , name , cents , order ))
for table in range ( 1 , 31 ):
conn . execute ( "INSERT OR IGNORE INTO tables VALUES (?, ?, ?)" , ( legacy , table , secrets . token_urlsafe ( 18 )))
for module_id , ( enabled , status ) in MODULE_DEFAULTS . items ():
conn . execute ( "INSERT OR IGNORE INTO merchant_modules VALUES (?,?,?,?,?,?)" ,
( legacy , module_id , enabled , status , ' {} ' , now ()))
conn . execute ( "UPDATE merchant_modules SET status='live' WHERE module_id='booking' AND status='preview'" )
conn . commit ()
return conn
def helper ( payload ):
result = subprocess . run ([ "node" , str ( PAYMENT_HELPER )], input = json . dumps ( payload ), text = True , capture_output = True , timeout = 12 , check = True )
return json . loads ( result . stdout )
def fetch_json ( url ):
request = Request ( url , headers = { "User-Agent" : "JustWorks-Business/0.1" })
with urlopen ( request , timeout = 10 ) as response :
return json . loads ( response . read ())
def screen_tips ( npub , lightning_address ):
"""Read-only LNURL metadata + signed NIP-57 announcements; no invoice creation."""
if lightning_address . count ( '@' ) != 1 :
raise ValueError ( 'invalid Lightning address' )
name , host = lightning_address . split ( '@' )
if not name or not host or any ( char in host for char in '/?#:@' ):
raise ValueError ( 'invalid Lightning address' )
2026-10-08 15:07:31 -04:00
endpoint = f 'https:// { host } /.well-known/lnurlp/ { quote ( name , safe = "" ) } '
pay = fetch_lnurl_json ( endpoint , timeout = 4 )
2026-10-08 19:39:38 +01:00
provider = pay . get ( 'nostrPubkey' )
if pay . get ( 'tag' ) != 'payRequest' or pay . get ( 'allowsNostr' ) is not True or not isinstance ( provider , str ) or len ( provider ) != 64 or any ( c not in '0123456789abcdef' for c in provider ):
raise ValueError ( 'No verified Nostr receipt source' )
return helper ({ 'action' : 'screen-tips' , 'npub' : npub , 'provider' : provider , 'relays' : PAYMENT_RELAYS })[ 'tips' ]
2026-10-08 15:07:31 -04:00
def resolve_public_https ( url ):
2026-10-08 19:39:38 +01:00
parsed = urlparse ( url )
2026-10-08 15:07:31 -04:00
if parsed . scheme != "https" or not parsed . hostname or parsed . username or parsed . password or parsed . fragment :
2026-10-08 19:39:38 +01:00
raise ValueError ( "Lightning provider returned an unsafe URL" )
addresses = socket . getaddrinfo ( parsed . hostname , parsed . port or 443 , type = socket . SOCK_STREAM )
if not addresses or any ( not ipaddress . ip_address ( item [ 4 ][ 0 ]) . is_global for item in addresses ):
raise ValueError ( "Lightning provider must use a public host" )
2026-10-08 15:07:31 -04:00
return parsed , addresses
2026-10-08 19:39:38 +01:00
2026-10-08 15:07:31 -04:00
class PinnedLnurlConnection ( http . client . HTTPSConnection ):
def __init__ ( self , parsed , addresses , timeout ):
# Direct sockets intentionally ignore ambient HTTP(S)_PROXY settings.
super () . __init__ ( parsed . hostname , parsed . port or 443 , timeout = timeout ,
context = ssl . create_default_context ())
self . addresses = addresses
def connect ( self ):
last_error = None
for family , kind , protocol , _canonical , address in self . addresses :
connection = socket . socket ( family , kind , protocol )
try :
connection . settimeout ( self . timeout )
# Numeric sockaddr from the validated lookup: no second DNS lookup.
connection . connect ( address )
self . sock = self . _context . wrap_socket ( connection , server_hostname = self . host )
return
except OSError as error :
connection . close ()
last_error = error
raise last_error or OSError ( "Lightning provider connection unavailable" )
2026-10-08 14:58:10 -04:00
2026-10-08 15:07:31 -04:00
def fetch_lnurl_json ( url , timeout = 10 ):
parsed , addresses = resolve_public_https ( url )
connection = PinnedLnurlConnection ( parsed , addresses , timeout )
try :
target = parsed . path or "/"
if parsed . query :
target += "?" + parsed . query
connection . request ( "GET" , target , headers = { "User-Agent" : "JustWorks-Business/0.1" })
response = connection . getresponse ()
if 300 <= response . status < 400 :
raise ValueError ( "Lightning provider redirects are not permitted" )
if response . status >= 400 :
raise HTTPError ( url , response . status , response . reason , response . headers , None )
2026-10-08 14:58:10 -04:00
raw = response . read ( 65537 )
2026-10-08 15:07:31 -04:00
if len ( raw ) > 65536 :
raise ValueError ( "Lightning provider response too large" )
return json . loads ( raw )
finally :
connection . close ()
2026-10-08 14:58:10 -04:00
2026-10-08 19:39:38 +01:00
def lightning_invoice ( lightning_address , amount_msat , comment = "" ):
if "@" not in lightning_address :
raise ValueError ( "Merchant Lightning address is invalid" )
name , host = lightning_address . rsplit ( "@" , 1 )
if not name or not host or any ( char in host for char in "/?#" ):
raise ValueError ( "Merchant Lightning address is invalid" )
2026-10-08 15:07:31 -04:00
endpoint = f "https:// { host } /.well-known/lnurlp/ { quote ( name , safe = "" ) } "
2026-10-08 14:58:10 -04:00
pay = fetch_lnurl_json ( endpoint )
2026-10-08 19:39:38 +01:00
if pay . get ( "tag" ) != "payRequest" or not pay . get ( "callback" ):
raise ValueError ( "Lightning address does not support payments" )
minimum , maximum = int ( pay . get ( "minSendable" , 0 )), int ( pay . get ( "maxSendable" , 0 ))
if amount_msat < minimum or ( maximum and amount_msat > maximum ):
raise ValueError ( f "Amount must be between { max ( 1 , minimum // 1000 ) } and { maximum // 1000 } sats" )
2026-10-08 15:07:31 -04:00
callback = str ( pay [ "callback" ])
2026-10-08 19:39:38 +01:00
# Some LNURL providers reject otherwise valid NIP-57 requests when their
# signed content contains a literal percent sign. Keep the human meaning
# while using a provider-safe comment for both the zap and callback.
safe_comment = str ( comment or "" ) . replace ( "%" , " percent" ) . strip ()
params = { "amount" : amount_msat }
proof = None
recipient = str ( pay . get ( "nostrPubkey" ) or "" ) . strip ()
if pay . get ( "allowsNostr" ) is True and len ( recipient ) == 64 :
proof = helper ({ "action" : "create" , "lnurlUrl" : endpoint , "relays" : PAYMENT_RELAYS ,
"amountMsat" : amount_msat , "recipient" : recipient ,
"content" : safe_comment or "Just Works payment" })
params [ "nostr" ] = json . dumps ( proof [ "event" ], separators = ( "," , ":" ))
params [ "lnurl" ] = proof [ "lnurl" ]
if safe_comment and int ( pay . get ( "commentAllowed" , 0 )):
params [ "comment" ] = safe_comment [: int ( pay [ "commentAllowed" ])]
separator = "&" if "?" in callback else "?"
try :
2026-10-08 14:58:10 -04:00
invoice = fetch_lnurl_json ( f " { callback }{ separator }{ urlencode ( params ) } " )
2026-10-08 19:39:38 +01:00
except HTTPError :
if not proof :
raise
# Invoice creation is the primary action. If a provider advertises
# NIP-57 but fails its zap path, retry as standard LNURL-pay and mark
# the result non-verifiable instead of blocking the customer.
proof = None
fallback = { key : value for key , value in params . items () if key not in { "nostr" , "lnurl" }}
2026-10-08 14:58:10 -04:00
invoice = fetch_lnurl_json ( f " { callback }{ separator }{ urlencode ( fallback ) } " )
2026-10-08 19:39:38 +01:00
if invoice . get ( "status" ) == "ERROR" or not invoice . get ( "pr" ):
raise ValueError ( invoice . get ( "reason" , "Lightning invoice unavailable" ))
return { "bolt11" : invoice [ "pr" ], "zap_pubkey" : proof [ "pubkey" ] if proof else None ,
"recipient" : recipient if proof else None , "verifiable" : bool ( proof )}
def remote_json ( url , method = "GET" , payload = None , timeout = 60 ):
data = json . dumps ( payload ) . encode () if payload is not None else None
request = Request ( url , data = data , method = method , headers = { "User-Agent" : "JustWorks-Business/0.1" , "Content-Type" : "application/json" })
try :
with urlopen ( request , timeout = timeout ) as response :
return json . loads ( response . read ())
except HTTPError as error :
try : message = json . loads ( error . read ()) . get ( "error" )
except Exception : message = None
raise ValueError ( message or f "Just Works profile request failed ( { error . code } )" ) from error
def resolve_merchant_npub ( npub ):
npub = security . canonical_npub ( npub )
# Core resolves every published website; the regional directory is optional
# presentation metadata, never a merchant allowlist or ownership proof.
try :
nsite = fetch_json ( f "https://justworks.cash/api/nsites/ { npub } " ) . get ( "nsite" , {})
except HTTPError as error :
if error . code == 404 : return None , None
raise
slug = nsite . get ( "id" , "" )
if ( nsite . get ( "npub" ) != npub or not isinstance ( slug , str )
or not re . fullmatch ( r "[a-z0-9]+(?:-[a-z0-9]+)*" , slug )
or nsite . get ( "nsite" , {}) . get ( "deploymentStatus" ) != "published" ):
return None , None
place = { "npub" : npub , "slug" : slug , "name" : nsite . get ( "name" ) or slug ,
"justworks" : f "https://justworks.cash/ { slug } " }
return place , nsite
def merchant_payload ( conn , include_tokens = False ):
merchant = dict ( conn . execute ( "SELECT * FROM merchant WHERE id=?" , ( rollout . legacy_merchant ( conn ),)) . fetchone ())
merchant [ "module_enabled" ] = bool ( merchant [ "module_enabled" ])
merchant [ "products" ] = [ dict ( row ) for row in conn . execute ( "SELECT id,name,price_cents,enabled,sort_order FROM products WHERE merchant_id=? ORDER BY sort_order" , ( merchant [ "id" ],))]
for product in merchant [ "products" ]:
product [ "enabled" ] = bool ( product [ "enabled" ])
merchant [ "modules" ] = {}
for row in conn . execute ( "SELECT module_id,enabled,status,config_json FROM merchant_modules WHERE merchant_id=?" , ( merchant [ "id" ],)):
merchant [ "modules" ][ row [ "module_id" ]] = { "enabled" : bool ( row [ "enabled" ]), "status" : row [ "status" ], "config" : json . loads ( row [ "config_json" ] or ' {} ' )}
if include_tokens :
merchant [ "tables" ] = [ dict ( row ) for row in conn . execute ( "SELECT table_number,token FROM tables WHERE merchant_id=? ORDER BY table_number" , ( merchant [ "id" ],))]
return merchant
def merchant_modules ( conn , merchant_id ):
for module_id , ( enabled , status ) in MODULE_DEFAULTS . items ():
conn . execute ( "INSERT OR IGNORE INTO merchant_modules VALUES (?,?,?,?,?,?)" ,
( merchant_id , module_id , enabled , status , ' {} ' , now ()))
conn . commit ()
return { row [ "module_id" ]: { "enabled" : bool ( row [ "enabled" ]), "status" : row [ "status" ], "config" : json . loads ( row [ "config_json" ] or ' {} ' )}
for row in conn . execute ( "SELECT module_id,enabled,status,config_json FROM merchant_modules WHERE merchant_id=?" , ( merchant_id ,))}
class Handler ( BaseHTTPRequestHandler ):
server_version = "JustWorksBusiness/0.1"
def log_message ( self , fmt , * args ):
# Query strings may contain table tokens. Do not log request lines or bodies.
print ( json . dumps ({ "ts" : now (), "method" : self . command , "status" : str ( args [ 1 ]) if len ( args ) > 1 else "" }))
def send_json ( self , status , value ):
mutation = getattr ( self , "authorized_mutation" , None )
if mutation and 200 <= status < 300 :
with LOCK , db () as conn :
security . audit ( conn , "mutation" , self . route ()[ 0 ], * mutation )
self . authorized_mutation = None
raw = json . dumps ( value , separators = ( "," , ":" )) . encode ()
self . send_response ( status )
self . send_header ( "Content-Type" , "application/json" )
self . send_header ( "Content-Length" , str ( len ( raw )))
self . send_header ( "Cache-Control" , "no-store" )
if getattr ( self , "session_cookie" , None ):
self . send_header ( "Set-Cookie" , self . session_cookie )
if getattr ( self , "exchange_cookie" , None ):
self . send_header ( "Set-Cookie" , self . exchange_cookie )
self . send_header ( "Referrer-Policy" , "no-referrer" )
self . end_headers ()
self . wfile . write ( raw )
def redirect ( self , location ):
self . send_response ( 302 )
self . send_header ( "Location" , location )
self . send_header ( "Cache-Control" , "public,max-age=3600" )
self . end_headers ()
def body ( self ):
size = int ( self . headers . get ( "Content-Length" , "0" ))
if size < 0 or size > 64 * 1024 :
raise ValueError ( "body too large" )
value = json . loads ( self . rfile . read ( size ) or b " {} " )
if not isinstance ( value , dict ): raise ValueError ( "JSON object required" )
return value
def route ( self ):
parsed = urlparse ( self . path )
path = parsed . path
if BASE and path . startswith ( BASE ):
path = path [ len ( BASE ):] or "/"
return path , parse_qs ( parsed . query )
def surface ( self ):
host = self . headers . get ( "Host" , "" ) . split ( ":" , 1 )[ 0 ] . lower ()
return host . removesuffix ( ".justworks.cash" ) if host . endswith ( ".justworks.cash" ) else ""
def is_local ( self ):
if os . environ . get ( "JWB_ARCHIPELAGO" ) == "1" : return False
# Anything not served behind the production subdomains is a dev server.
host = self . headers . get ( "Host" , "" ) . split ( ":" , 1 )[ 0 ] . lower ()
return not host . endswith ( ".justworks.cash" )
def static ( self , file , cache = "no-store" ):
file = file . resolve ()
allowed = [ PUBLIC . resolve (), NAPLET_DIST . resolve ()]
if not any ( file == root or root in file . parents for root in allowed ) or not file . is_file ():
self . send_error ( 404 )
return
raw = file . read_bytes ()
if os . environ . get ( "JWB_ARCHIPELAGO" ) == "1" and file . suffix == ".html" :
html = raw . decode () . replace ( '"/assets/' , '"/business-demo/assets/' ) . replace ( '"/fonts/' , '"/business-demo/fonts/' )
html = html . replace ( '<head>' , '<head><script src="/business-demo/assets/archipelago-paths.js"></script>' , 1 )
raw = html . encode ()
mime = mimetypes . guess_type ( file . name )[ 0 ] or "application/octet-stream"
self . send_response ( 200 )
self . send_header ( "Content-Type" , mime )
self . send_header ( "Content-Length" , str ( len ( raw )))
self . send_header ( "Cache-Control" , cache )
self . send_header ( "X-Content-Type-Options" , "nosniff" )
if file . name == "business-sso.html" :
self . send_header ( "Referrer-Policy" , "no-referrer" )
self . send_header ( "X-Frame-Options" , "DENY" )
self . send_header ( "Content-Security-Policy" , "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'" )
self . end_headers ()
self . wfile . write ( raw )
def nsite ( self ):
file = PUBLIC / "nsite.html"
html = file . read_text ()
html = html . replace ( "</body>" , f '<script type="module" src=" { BASE } /assets/nsite-live.js"></script></body>' )
raw = html . encode ()
self . send_response ( 200 )
self . send_header ( "Content-Type" , "text/html; charset=utf-8" )
self . send_header ( "Content-Length" , str ( len ( raw )))
self . send_header ( "Cache-Control" , "no-store" )
self . send_header ( "X-Content-Type-Options" , "nosniff" )
self . end_headers ()
self . wfile . write ( raw )
def app_manifest ( self , app , query ):
manifest = json . loads (( PUBLIC / f " { app } .webmanifest" ) . read_text ())
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ]
if npub :
try : npub = security . canonical_npub ( npub )
except ValueError : return self . send_json ( 400 , { "error" : "invalid npub" })
manifest [ "start_url" ] = f "/?npub= { npub } "
manifest [ "id" ] += f "&npub= { npub } "
raw = json . dumps ( manifest , separators = ( "," , ":" )) . encode ()
self . send_response ( 200 )
self . send_header ( "Content-Type" , "application/manifest+json" )
self . send_header ( "Content-Length" , str ( len ( raw )))
self . send_header ( "Cache-Control" , "no-store" )
self . send_header ( "X-Content-Type-Options" , "nosniff" )
self . end_headers ()
self . wfile . write ( raw )
def do_GET ( self ):
self . session_cookie = self . exchange_cookie = self . authorized_mutation = None
path , query = self . route ()
if path == "/api/business-session" or path in security . PRIVILEGED_GET :
with LOCK , db () as conn :
if path == "/api/business-session" :
current = security . session ( conn , self . headers )
return self . send_json ( 200 , { "authenticated" : bool ( current ), "session" : current , "mode" : security . mode (), "sso_enabled" : rollout . enabled () and ( 'https://' + self . headers . get ( 'Host' , '' ) . lower ()) in rollout . origins (), "legacy_npub" : rollout . legacy_merchant ( conn ) if rollout . legacy_merchant ( conn ) != 'localpub-demo' else None })
allowed , _ = security . authorize ( conn , "GET" , path , { key : value [ 0 ] for key , value in query . items ()}, self . headers )
if not allowed : return self . send_json ( 403 , { "error" : "Merchant session required for this account" })
if path == "/healthz" : return self . send_json ( 200 , { "ok" : True })
if path == "/api/config" :
with LOCK , db () as conn : return self . send_json ( 200 , merchant_payload ( conn ))
if path == "/api/dashboard" :
with LOCK , db () as conn :
payload = merchant_payload ( conn , True )
payload [ "orders" ] = [ dict ( row ) | { "items" : json . loads ( row [ "items_json" ])} for row in conn . execute ( "SELECT * FROM orders WHERE merchant_id=? AND status NOT IN ('delivered','closed') ORDER BY created_at DESC LIMIT 50" , ( rollout . legacy_merchant ( conn ),))]
for item in payload [ "orders" ]: item . pop ( "items_json" , None )
payload [ "payments" ] = [ dict ( row ) for row in conn . execute ( "SELECT id,order_id,table_number,amount_cents,amount_sats,status,created_at,settled_at FROM payments WHERE merchant_id=? AND status='paid' ORDER BY settled_at DESC LIMIT 20" , ( rollout . legacy_merchant ( conn ),))]
return self . send_json ( 200 , payload )
if path == "/api/business" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
try :
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
with LOCK , db () as conn :
modules = merchant_modules ( conn , npub )
counts = {
"orders" : conn . execute ( "SELECT COUNT(*) FROM orders WHERE merchant_id=? AND status!='closed'" , ( npub ,)) . fetchone ()[ 0 ],
"payments" : conn . execute ( "SELECT COUNT(*) FROM payments WHERE merchant_id=? AND status NOT IN ('acknowledged')" , ( npub ,)) . fetchone ()[ 0 ],
"bookings" : conn . execute ( "SELECT COUNT(*) FROM bookings WHERE merchant_id=? AND status NOT IN ('seated','closed','cancelled')" , ( npub ,)) . fetchone ()[ 0 ],
}
current = security . session ( conn , self . headers )
if rollout . effective_mode ( conn , "GET" , "/api/business" , npub ) == "enforce" and ( not current or current [ "npub" ] != npub ):
counts = { key : 0 for key in counts }
images = list ( dict . fromkeys ([ nsite . get ( "media" , {}) . get ( "heroImage" ), * nsite . get ( "media" , {}) . get ( "galleryImages" , []), * place . get ( "images" , [])]))
images = [ image for image in images if image and not any ( marker in image . lower () for marker in ( "logo" , "icon" , "static.cdninstagram.com" ))]
merchant_name = nsite . get ( "name" ) or place . get ( "name" )
query_string = urlencode ({ "q" : merchant_name })
map_query_string = urlencode ({ "q" : merchant_name , "place" : place . get ( "slug" )})
links = {
"justworks" : place . get ( "justworks" ) or f "https://justworks.cash/ { place . get ( 'slug' ) } " ,
"tip" : f "https://justworks.cash/tip/? { urlencode ({ 'id' : place . get ( 'slug' )}) } " ,
"nsite" : nsite . get ( "nsite" , {}) . get ( "gatewayUrl" ),
"nostr" : nsite . get ( "identity" , {}) . get ( "primalUrl" ) or f "https://njump.me/ { npub } " ,
"maps" : f "https://maps.justworks.cash/? { map_query_string } " ,
"gallery" : f "https://gallery.justworks.cash/? { query_string } " ,
}
if self . is_local ():
# Dev: keep merchants on local surfaces — /page renders the
# platform-hosted Just Works page, /site renders the nsite
# copy the product generates and publishes to Nostr.
links [ "justworks" ] = f " { BASE } /page?npub= { npub } "
links [ "nsite" ] = f " { BASE } /site?npub= { npub } "
links [ "gallery" ] = f " { BASE } /gallery? { query_string } "
profile = nsite . get ( "identity" , {}) . get ( "profile" , {})
lightning_address = str ( profile . get ( "lud16" ) or "" ) . strip ()
payment_ready = bool ( lightning_address and not lightning_address . lower () . endswith ( "@npub.cash" ))
if self . is_local () and not payment_ready :
# Dev only: stand in a working Lightning address so Pay / Pay & Go are unlocked.
lightning_address = DEV_LIGHTNING_ADDRESS
payment_ready = True
return self . send_json ( 200 , { "merchant" : { "name" : merchant_name , "npub" : npub , "slug" : place . get ( "slug" ), "area" : place . get ( "area" ), "category" : place . get ( "category" )}, "images" : images [: 8 ], "links" : links , "profile" : profile , "lightning_address" : lightning_address , "payment_ready" : payment_ready , "modules" : modules , "counts" : counts })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "merchant unavailable: { error } " })
if path == "/api/nsite-page" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
try :
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
sections = nsite . get ( "page" , {}) . get ( "sections" , [])
menu = next (( section for section in sections if section . get ( "type" ) == "menu" and section . get ( "enabled" )), {})
products = [{ "id" : str ( item . get ( "id" ) or f "item- { index } " )[: 40 ], "name" : str ( item . get ( "title" ) or item . get ( "name" ) or "Item" )[: 80 ], "description" : str ( item . get ( "body" ) or item . get ( "description" ) or "" )[: 180 ], "price" : str ( item . get ( "price" ) or "" )[: 30 ]} for index , item in enumerate ( menu . get ( "items" , [])[: 60 ])]
assets = [ item . get ( "url" ) for item in nsite . get ( "page" , {}) . get ( "assets" , []) if item . get ( "url" )]
sources = [ image for source in nsite . get ( "sources" , []) for image in source . get ( "images" , [])]
media = nsite . get ( "media" , {})
images = [ image for image in dict . fromkeys ([ media . get ( "heroImage" ), * media . get ( "galleryImages" , []), * assets , * sources , * place . get ( "images" , [])]) if image and not any ( marker in image . lower () for marker in ( "logo" , "icon" , "static.cdninstagram.com" ))][: 12 ]
profile = nsite . get ( "identity" , {}) . get ( "profile" , {})
sources = [{ "name" : str ( source . get ( "title" ) or source . get ( "host" ) or "Source" )[: 80 ], "host" : str ( source . get ( "host" ) or "" )[: 60 ], "url" : str ( source . get ( "url" ) or "" )[: 300 ], "description" : str ( source . get ( "description" ) or "" )[: 180 ], "photos" : len ( source . get ( "images" , []))} for source in nsite . get ( "sources" , [])][: 6 ]
return self . send_json ( 200 , {
"merchant" : { "name" : nsite . get ( "name" ) or place . get ( "name" ), "npub" : npub , "slug" : place . get ( "slug" ), "area" : place . get ( "area" ), "category" : place . get ( "category" ), "address" : place . get ( "address" ), "hours" : place . get ( "hours" ), "phone" : place . get ( "phone" ), "website" : place . get ( "website" ), "tags" : [ tag for tag in place . get ( "tags" , [])[: 8 ] if tag ], "rating" : place . get ( "rating" )},
"about" : str ( profile . get ( "about" ) or "" ) . strip ()[: 600 ],
"products" : products ,
"images" : images ,
"sources" : sources ,
"gateway" : nsite . get ( "nsite" , {}) . get ( "gatewayUrl" , "" ),
"links" : { "business" : f " { BASE } /dashboard?npub= { npub } " , "page" : f " { BASE } /page?npub= { npub } " },
})
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "site unavailable: { error } " })
if path == "/api/qr" :
text = ( query . get ( "text" ) or [ "" ])[ 0 ] . strip ()
if not text or len ( text ) > 500 : return self . send_json ( 400 , { "error" : "provide qr text" })
try :
raw = helper ({ "action" : "qr" , "text" : text })[ "svg" ] . encode ()
self . send_response ( 200 )
self . send_header ( "Content-Type" , "image/svg+xml" )
self . send_header ( "Cache-Control" , "public,max-age=3600" )
self . send_header ( "Content-Length" , str ( len ( raw )))
self . end_headers ()
self . wfile . write ( raw )
return
except Exception as error : return self . send_json ( 502 , { "error" : f "qr unavailable: { error } " })
if path == "/api/gallery" :
try :
payload = fetch_json ( "https://justworks.cash/data/regions/madeira.json" )
return self . send_json ( 200 , { "name" : payload . get ( "name" , "Madeira" ), "places" : payload . get ( "places" , [])})
except Exception as error :
return self . send_json ( 502 , { "error" : f "gallery data unavailable: { error } " })
if path == "/api/screen" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
try :
npub = security . canonical_npub ( npub )
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
screen_activity . configure ( npub , nsite )
assets = [ item . get ( "url" ) for item in nsite . get ( "page" , {}) . get ( "assets" , []) if item . get ( "url" )]
sources = [ image for source in nsite . get ( "sources" , []) for image in source . get ( "images" , [])]
images = list ( dict . fromkeys ([ nsite . get ( "media" , {}) . get ( "heroImage" ), * nsite . get ( "media" , {}) . get ( "galleryImages" , []), * assets , * sources , * place . get ( "images" , [])]))
images = [ image for image in images if image and not any ( marker in image . lower () for marker in ( "static.cdninstagram.com" , "facebook.com/images" , "logo" , "icon" ))][: 18 ]
images . sort ( key = lambda image : ( image . lower () . split ( "?" , 1 )[ 0 ] . endswith ( ".png" ), "folha" in image . lower ()))
order_url = f "http:// { self . headers . get ( 'Host' , '127.0.0.1:4310' ) }{ BASE } /order?npub= { npub } " if self . is_local () else f "https://order.justworks.cash/?npub= { npub } "
justpay_url = f "http:// { self . headers . get ( 'Host' , '127.0.0.1:4310' ) }{ BASE } /portal? { urlencode ({ 'surface' : 'payandgo' , 'npub' : npub }) } " if self . is_local () else f "https://payandgo.justworks.cash/?npub= { npub } "
tip_url = f "https://justworks.cash/tip/? { urlencode ({ 'id' : place [ 'slug' ]}) } "
qr = helper ({ "action" : "qr" , "text" : order_url })[ "svg" ]
try : latest = helper ({ "action" : "latest" , "npub" : npub , "relays" : PAYMENT_RELAYS })[ "event" ]
except Exception : latest = None
return self . send_json ( 200 , { "merchant" : { "name" : nsite . get ( "name" ) or place . get ( "name" ), "npub" : npub , "slug" : place . get ( "slug" ), "area" : place . get ( "area" ), "category" : place . get ( "category" )}, "images" : images , "latest" : latest , "order_url" : order_url , "qr_svg" : qr , "screen_links" : { "order" : order_url , "justpay" : justpay_url , "tip" : tip_url }})
except ValueError as error :
return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error :
return self . send_json ( 502 , { "error" : f "screen data unavailable: { error } " })
if path == "/api/screen-activity" :
try :
npub = security . canonical_npub (( query . get ( 'npub' ) or [ '' ])[ 0 ] . strip ())
except ValueError :
return self . send_json ( 400 , { 'error' : 'provide a valid merchant npub' })
with LOCK , db () as conn :
payload = screen_activity . projection ( conn , npub , screen_tips )
return self . send_json ( 200 , payload )
if path == "/api/order-merchant" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
try :
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
sections = nsite . get ( "page" , {}) . get ( "sections" , [])
menu = next (( section for section in sections if section . get ( "type" ) == "menu" and section . get ( "enabled" )), {})
products = [{ "id" : str ( item . get ( "id" ) or f "item- { index } " )[: 40 ], "name" : str ( item . get ( "title" ) or item . get ( "name" ) or "Item" )[: 80 ], "description" : str ( item . get ( "body" ) or item . get ( "description" ) or "" )[: 180 ], "price" : str ( item . get ( "price" ) or "" )[: 30 ]} for index , item in enumerate ( menu . get ( "items" , [])[: 50 ])]
images = list ( dict . fromkeys ([ nsite . get ( "media" , {}) . get ( "heroImage" ), * nsite . get ( "media" , {}) . get ( "galleryImages" , []), * place . get ( "images" , [])]))
images = [ image for image in images if image and not any ( marker in image . lower () for marker in ( "logo" , "icon" , "static.cdninstagram.com" ))]
images . sort ( key = lambda image : image . lower () . split ( "?" , 1 )[ 0 ] . endswith ( ".png" ))
return self . send_json ( 200 , { "merchant" : { "name" : nsite . get ( "name" ) or place . get ( "name" ), "npub" : npub , "slug" : place . get ( "slug" ), "area" : place . get ( "area" ), "category" : place . get ( "category" )}, "images" : images [: 8 ], "products" : products })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "merchant unavailable: { error } " })
if path == "/api/merchant-order-status" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
order_id = ( query . get ( "id" ) or [ "" ])[ 0 ] . strip ()
if not npub . startswith ( "npub1" ) or len ( npub ) > 100 or not order_id :
return self . send_json ( 400 , { "error" : "provide the merchant and order" })
with LOCK , db () as conn :
row = conn . execute ( "SELECT id,table_number,status,updated_at FROM orders WHERE id=? AND merchant_id=?" , ( order_id , npub )) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "order not found" })
return self . send_json ( 200 , dict ( row ))
if path == "/api/quick-pay-status" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
payment_id = ( query . get ( "id" ) or [ "" ])[ 0 ] . strip ()
if not npub . startswith ( "npub1" ) or len ( npub ) > 100 or len ( payment_id ) != 32 :
return self . send_json ( 400 , { "error" : "provide the merchant and payment" })
with LOCK , db () as conn :
row = conn . execute ( "SELECT * FROM quick_payments WHERE id=? AND merchant_id=?" , ( payment_id , npub )) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "payment not found" })
payment = dict ( row )
if payment [ "status" ] == "pending" and payment [ "zap_pubkey" ] and payment [ "recipient" ]:
try :
created_at = int ( datetime . fromisoformat ( payment [ "created_at" ]) . timestamp ())
check = helper ({ "action" : "check" , "relays" : PAYMENT_RELAYS , "recipient" : payment [ "recipient" ],
"pubkey" : payment [ "zap_pubkey" ], "since" : created_at })
if check [ "paid" ]:
settled_at = now ()
with LOCK , db () as conn :
conn . execute ( "UPDATE quick_payments SET status='paid',receipt_id=?,settled_at=? WHERE id=?" ,
( check [ "receipt" ], settled_at , payment_id ))
conn . commit ()
payment . update ( status = "paid" , receipt_id = check [ "receipt" ], settled_at = settled_at )
except Exception :
pass
fields = ( "id" , "merchant_name" , "amount_cents" , "amount_sats" , "status" , "receipt_id" , "created_at" , "settled_at" )
return self . send_json ( 200 , { key : payment [ key ] for key in fields } | { "verifiable" : bool ( payment [ "zap_pubkey" ])})
if path == "/api/pay-orders" :
# Existing customer receipt discovery, without staff notes, guest bookings or payments.
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
try : security . canonical_npub ( npub )
except ValueError : return self . send_json ( 400 , { "error" : "provide a valid npub" })
with LOCK , db () as conn :
orders = [{ "table_number" : row [ "table_number" ], "status" : row [ "status" ],
"items" : [{ key : item . get ( key ) for key in ( "name" , "qty" , "price_cents" )} for item in json . loads ( row [ "items_json" ])]}
for row in conn . execute ( "SELECT table_number,status,items_json FROM orders WHERE merchant_id=? AND status!='closed' ORDER BY created_at" , ( npub ,))]
return self . send_json ( 200 , { "orders" : orders })
if path == "/api/pos" :
npub = ( query . get ( "npub" ) or [ "" ])[ 0 ] . strip ()
if not npub . startswith ( "npub1" ) or len ( npub ) > 100 :
return self . send_json ( 400 , { "error" : "provide a valid npub" })
with LOCK , db () as conn :
orders = [ dict ( row ) | { "items" : json . loads ( row [ "items_json" ])} for row in conn . execute ( "SELECT * FROM orders WHERE merchant_id=? AND status!='closed' ORDER BY created_at" , ( npub ,))]
for item in orders : item . pop ( "items_json" , None )
payments = [ dict ( row ) for row in conn . execute ( "SELECT id,order_id,table_number,amount_cents,amount_sats,status,created_at,settled_at FROM payments WHERE merchant_id=? AND status!='acknowledged' ORDER BY created_at" , ( npub ,))]
bookings = [ dict ( row ) for row in conn . execute ( "SELECT * FROM bookings WHERE merchant_id=? AND status NOT IN ('closed','cancelled') ORDER BY scheduled_at" , ( npub ,))]
return self . send_json ( 200 , { "orders" : orders , "payments" : payments , "bookings" : bookings , "server_time" : now ()})
if path == "/api/table" :
token = ( query . get ( "token" ) or [ "" ])[ 0 ]
with LOCK , db () as conn :
row = conn . execute ( "SELECT table_number FROM tables WHERE token=?" , ( token ,)) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "invalid table token" })
config = merchant_payload ( conn )
if not config [ "module_enabled" ]: return self . send_json ( 403 , { "error" : "module disabled" })
return self . send_json ( 200 , { "merchant" : config [ "name" ], "table" : row [ "table_number" ], "products" : [ p for p in config [ "products" ] if p [ "enabled" ]]})
if path == "/api/order" :
token = ( query . get ( "token" ) or [ "" ])[ 0 ]
order_id = ( query . get ( "id" ) or [ "" ])[ 0 ]
with LOCK , db () as conn :
row = conn . execute ( """SELECT o.id,o.table_number,o.status,o.updated_at
FROM orders o JOIN tables t ON t.merchant_id=o.merchant_id AND t.table_number=o.table_number
WHERE o.id=? AND t.token=?""" , ( order_id , token )) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "order not found" })
return self . send_json ( 200 , dict ( row ))
if path == "/api/payment" :
token = ( query . get ( "token" ) or [ "" ])[ 0 ]
payment_id = ( query . get ( "id" ) or [ "" ])[ 0 ]
with LOCK , db () as conn :
row = conn . execute ( """SELECT p.* FROM payments p JOIN tables t ON t.merchant_id=p.merchant_id AND t.table_number=p.table_number
WHERE p.id=? AND t.token=?""" , ( payment_id , token )) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "payment not found" })
payment = dict ( row )
if payment [ "status" ] == "pending" :
check = helper ({ "action" : "check" , "relays" : PAYMENT_RELAYS , "recipient" : PAYMENT_RECIPIENT , "pubkey" : payment [ "zap_pubkey" ], "since" : int ( datetime . fromisoformat ( payment [ "created_at" ]) . timestamp ())})
if check [ "paid" ]:
with LOCK , db () as conn :
conn . execute ( "UPDATE payments SET status='paid',receipt_id=?,settled_at=? WHERE id=?" , ( check [ "receipt" ], now (), payment_id ))
conn . commit ()
payment [ "status" ], payment [ "receipt_id" ], payment [ "settled_at" ] = "paid" , check [ "receipt" ], now ()
return self . send_json ( 200 , { key : payment [ key ] for key in ( "id" , "order_id" , "table_number" , "amount_cents" , "amount_sats" , "bolt11" , "status" , "receipt_id" , "created_at" , "settled_at" )})
if path == "/napplet/order-to-table/index.html" : return self . static ( NAPLET_DIST / "index.html" , "public,max-age=300" )
if path == "/assets/examples/localpub-venue.webp" : return self . redirect ( "https://justworks.cash/assets/examples/localpub-venue.webp" )
if path == "/nsite" : return self . nsite ()
if path == "/business-sso" : return self . static ( PUBLIC / "business-sso.html" )
if path in ( "/pos-sw.js" , "/screen-sw.js" ):
return self . static ( PUBLIC / path [ 1 :], "no-cache" )
if path in ( "/pos.webmanifest" , "/screen.webmanifest" ):
return self . app_manifest ( path [ 1 :] . split ( "." )[ 0 ], query )
surface_pages = {
"business" : "dashboard.html" , "pos" : "pos.html" , "order" : "order.html" , "book" : "book.html" ,
"pay" : "portal.html" , "payandgo" : "portal.html" , "screen" : "screen.html" ,
"gallery" : "gallery.html" , "events" : "portal.html" ,
}
pages = { "/qr-sheet" : "qr-sheet.html" , "/" : surface_pages . get ( self . surface (), "dashboard.html" ), "/dashboard" : "dashboard.html" , "/profile" : "profile.html" , "/stats" : "stats.html" , "/embed" : "shell.html" , "/portal" : "portal.html" , "/gallery" : "gallery.html" , "/screen" : "screen.html" , "/order" : "order.html" , "/book" : "book.html" , "/pos" : "pos.html" , "/site" : "site.html" , "/page" : "page.html" }
if path in pages : return self . static ( PUBLIC / pages [ path ])
if path . startswith ( "/assets/" ): return self . static ( PUBLIC / path . removeprefix ( "/" ), "public,max-age=3600" )
if path . startswith ( "/fonts/" ): return self . static ( PUBLIC / path . removeprefix ( "/" ), "public,max-age=86400" )
self . send_error ( 404 )
def do_POST ( self ):
self . session_cookie = self . exchange_cookie = self . authorized_mutation = None
path , _ = self . route ()
try : body = self . body ()
except Exception as error : return self . send_json ( 400 , { "error" : str ( error )})
if path . startswith ( "/api/business-sso/" ):
if not rollout . enabled (): return self . send_json ( 404 , { "error" : "Session exchange is not enabled" })
with LOCK , db () as conn :
if not security . rate_allowed ( self . client_address [ 0 ], "/api/business-owner-login" , {}, self . headers ):
return self . send_json ( 429 , { "error" : "Too many exchange requests" })
try :
if path == "/api/business-sso/start" :
result , self . exchange_cookie = rollout . start ( conn , body , self . headers )
elif path in { "/api/business-sso/details" , "/api/business-sso/approve" }:
result = rollout . source_details ( conn , body , self . headers , path . endswith ( "approve" ))
elif path == "/api/business-sso/complete" :
result , self . session_cookie = rollout . complete ( conn , body , self . headers )
self . exchange_cookie = rollout . exchange_cookie ()
else : return self . send_json ( 404 , { "error" : "Unknown session action" })
return self . send_json ( 200 , result )
except ( ValueError , KeyError , CookieError ):
security . audit ( conn , "exchange_denied" , path , reason = "invalid_or_expired_exchange" )
return self . send_json ( 403 , { "error" : "Session exchange could not be verified; log in and try again" })
self . authorized_mutation = None
self . session_cookie = None
if path in { "/api/business-owner-login" , "/api/orders" , "/api/merchant-orders" , "/api/bookings" , "/api/payments" , "/api/quick-pay" }:
with LOCK :
if not security . rate_allowed ( self . client_address [ 0 ], path , body , self . headers ):
return self . send_json ( 429 , { "error" : "Too many requests; try again shortly" })
if path in { "/api/business-owner-login" , "/api/business-logout" , "/api/business-revoke" } and not security . same_origin ( self . headers ):
return self . send_json ( 403 , { "error" : "Same-origin request required" })
with LOCK , db () as conn :
if path in { "/api/business-logout" , "/api/business-revoke" }:
current = security . revoke ( conn , self . headers , path . endswith ( "revoke" ))
security . audit ( conn , "logout" , path , actor = current [ "npub" ] if current else "" )
self . session_cookie = security . cookie_header ()
return self . send_json ( 200 , { "ok" : True })
allowed , context = security . authorize ( conn , "POST" , path , body , self . headers )
if not allowed : return self . send_json ( 403 , { "error" : "Merchant session required for this account" })
self . authorized_mutation = context
if path == "/api/config" :
with LOCK , db () as conn :
enabled = 1 if body . get ( "module_enabled" ) else 0
count = max ( 1 , min ( 100 , int ( body . get ( "table_count" , 30 ))))
legacy = rollout . legacy_merchant ( conn )
conn . execute ( "UPDATE merchant SET module_enabled=?,table_count=?,updated_at=? WHERE id=?" , ( enabled , count , now (), legacy ))
products = body . get ( "products" ) or []
for index , product in enumerate ( products [: 50 ]):
pid = str ( product . get ( "id" ) or f "item- { index } " )[: 40 ]
name = str ( product . get ( "name" ) or "Item" )[: 80 ]
cents = max ( 0 , min ( 1000000 , int ( product . get ( "price_cents" , 0 ))))
conn . execute ( "INSERT INTO products VALUES (?, ?, ?, ?, 1, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name,price_cents=excluded.price_cents,sort_order=excluded.sort_order WHERE products.merchant_id=excluded.merchant_id" , ( pid , legacy , name , cents , index ))
for table in range ( 1 , count + 1 ): conn . execute ( "INSERT OR IGNORE INTO tables VALUES (?, ?, ?)" , ( legacy , table , secrets . token_urlsafe ( 18 )))
for module_id , value in ( body . get ( "modules" ) or {}) . items ():
if module_id not in MODULE_DEFAULTS : continue
enabled = 1 if value . get ( "enabled" ) else 0
config = json . dumps ( value . get ( "config" ) or {}, separators = ( "," , ":" ))
conn . execute ( "UPDATE merchant_modules SET enabled=?,config_json=?,updated_at=? WHERE merchant_id=? AND module_id=?" , ( enabled , config , now (), legacy , module_id ))
conn . commit ()
return self . send_json ( 200 , merchant_payload ( conn , True ))
if path == "/api/business-config" :
npub = str ( body . get ( "npub" , "" )) . strip ()
try :
place , _ = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "merchant unavailable: { error } " })
requested = body . get ( "modules" ) or {}
with LOCK , db () as conn :
merchant_modules ( conn , npub )
for module_id , value in requested . items ():
if module_id not in MODULE_DEFAULTS or MODULE_DEFAULTS [ module_id ][ 1 ] == "coming-soon" : continue
conn . execute ( "UPDATE merchant_modules SET enabled=?,updated_at=? WHERE merchant_id=? AND module_id=?" , ( 1 if value else 0 , now (), npub , module_id ))
conn . commit ()
return self . send_json ( 200 , { "ok" : True , "modules" : merchant_modules ( conn , npub )})
if path == "/api/business-owner-login" :
npub = str ( body . get ( "npub" , "" )) . strip ()
try :
place , _ = resolve_merchant_npub ( npub )
if not place : raise ValueError ( "merchant_not_found" )
if os . environ . get ( "JWB_ALLOW_DEMO_LOGIN" ) == "1" and security . mode () != "enforce" and str ( body . get ( "ownerIdentifier" , "" )) . strip () . lower () == "demo" :
# Dev shortcut: prefilled demo login resolves locally without remote credentials.
return self . send_json ( 200 , { "ok" : True , "merchant" : place . get ( "name" ), "npub" : npub , "dev" : True })
credentials = { key : str ( body . get ( key , "" )) . strip () for key in ( "ownerKey" , "ownerIdentifier" , "ownerPassword" ) if body . get ( key )}
if not credentials . get ( "ownerKey" ) and not ( credentials . get ( "ownerIdentifier" ) and credentials . get ( "ownerPassword" )):
raise ValueError ( "credentials_required" )
result = remote_json ( f "https://justworks.cash/api/nsites/ { place [ 'slug' ] } /login" , "POST" , credentials )
owner = result . get ( "nsite" , {})
if owner . get ( "npub" ) != npub :
raise ValueError ( "ownership_mismatch" )
with LOCK , db () as conn :
self . session_cookie , expires = security . issue ( conn , npub , self . headers )
rollout . verification ( conn , npub , place [ 'slug' ])
security . audit ( conn , "login_success" , path , npub , npub )
return self . send_json ( 200 , { "ok" : True , "merchant" : owner . get ( "name" ) or place . get ( "name" ), "npub" : npub , "expires_at" : expires })
except Exception :
self . session_cookie = None
with LOCK , db () as conn : security . audit ( conn , "login_failure" , path , reason = "verification_failed" )
return self . send_json ( 401 , { "error" : "Just Works login could not be verified" })
if path == "/api/business-profile" :
npub = str ( body . get ( "npub" , "" )) . strip ()
try :
place , _ = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
profile = body . get ( "profile" ) or {}
lightning_address = str ( profile . get ( "lud16" , "" )) . strip () . lower ()
if not lightning_address or "@" not in lightning_address or lightning_address . endswith ( "@npub.cash" ):
return self . send_json ( 400 , { "error" : "Enter the merchant’ s own Lightning address, not the default npub.cash address" })
credentials = { key : str ( body . get ( key , "" )) . strip () for key in ( "ownerKey" , "ownerIdentifier" , "ownerPassword" ) if body . get ( key )}
if not credentials . get ( "ownerKey" ) and not ( credentials . get ( "ownerIdentifier" ) and credentials . get ( "ownerPassword" )):
return self . send_json ( 400 , { "error" : "Enter the Just Works owner login or owner key" })
if os . environ . get ( "JWB_ALLOW_DEMO_LOGIN" ) == "1" and security . mode () != "enforce" and credentials . get ( "ownerIdentifier" , "" ) . lower () == "demo" :
# Dev shortcut: accept the save locally without republishing the production nsite.
dev_profile = { "display_name" : str ( profile . get ( "display_name" , "" ))[: 120 ], "about" : str ( profile . get ( "about" , "" ))[: 1200 ], "picture" : str ( profile . get ( "picture" , "" ))[: 1000 ], "banner" : str ( profile . get ( "banner" , "" ))[: 1000 ], "lud16" : lightning_address , "website" : place . get ( "justworks" ) or "" }
return self . send_json ( 200 , { "ok" : True , "profile" : dev_profile , "published" : True , "dev" : True })
update = {
** credentials ,
"name" : str ( profile . get ( "display_name" , "" ))[: 120 ],
"note" : str ( profile . get ( "about" , "" ))[: 1200 ],
"payment" : lightning_address ,
"avatarImage" : str ( profile . get ( "picture" , "" ))[: 1000 ],
"heroImage" : str ( profile . get ( "banner" , "" ))[: 1000 ],
"stage" : "Business profile updated" ,
}
remote_json ( f "https://justworks.cash/api/nsites/ { place [ 'slug' ] } " , "PATCH" , update )
published = remote_json ( f "https://justworks.cash/api/nsites/ { place [ 'slug' ] } /deploy" , "POST" , credentials , 90 )
return self . send_json ( 200 , { "ok" : True , "profile" : published . get ( "nsite" , {}) . get ( "identity" , {}) . get ( "profile" , {}), "published" : True })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "profile unavailable: { error } " })
if path == "/api/orders" :
token , items = str ( body . get ( "token" , "" )), body . get ( "items" ) or []
note = str ( body . get ( "note" , "" ))[: 240 ]
with LOCK , db () as conn :
table = conn . execute ( "SELECT table_number,merchant_id FROM tables WHERE token=?" , ( token ,)) . fetchone ()
if not table : return self . send_json ( 404 , { "error" : "invalid table token" })
merchant = conn . execute ( "SELECT module_enabled FROM merchant WHERE id=?" , ( table [ 'merchant_id' ],)) . fetchone ()
if not merchant [ "module_enabled" ]: return self . send_json ( 403 , { "error" : "module disabled" })
clean = []
for item in items [: 20 ]:
product = conn . execute ( "SELECT id,name,price_cents FROM products WHERE id=? AND merchant_id=? AND enabled=1" , ( str ( item . get ( "id" , "" )), table [ 'merchant_id' ])) . fetchone ()
qty = max ( 0 , min ( 20 , int ( item . get ( "qty" , 0 ))))
if product and qty : clean . append ( dict ( product ) | { "qty" : qty })
if not clean : return self . send_json ( 400 , { "error" : "order is empty" })
oid , timestamp = secrets . token_hex ( 5 ), now ()
conn . execute ( "INSERT INTO orders VALUES (?, ?, ?, ?, ?, 'new', ?, ?)" , ( oid , table [ 'merchant_id' ], table [ "table_number" ], json . dumps ( clean ), note , timestamp , timestamp ))
conn . commit ()
total_cents = sum ( item [ "price_cents" ] * item [ "qty" ] for item in clean )
return self . send_json ( 201 , { "id" : oid , "table" : table [ "table_number" ], "status" : "new" , "total_cents" : total_cents })
if path == "/api/merchant-orders" :
npub = str ( body . get ( "npub" , "" )) . strip ()
request_text = str ( body . get ( "request" , "" )) . strip ()[: 500 ]
guest = str ( body . get ( "guest" , "" )) . strip ()[: 80 ]
try : table_number = max ( 1 , min ( 999 , int ( body . get ( "table" , 1 ))))
except ( TypeError , ValueError ): return self . send_json ( 400 , { "error" : "enter a valid table number" })
selected = body . get ( "items" ) or []
if not request_text and not selected : return self . send_json ( 400 , { "error" : "add an order or request" })
try :
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except Exception as error : return self . send_json ( 502 , { "error" : f "merchant unavailable: { error } " })
items = [{ "id" : str ( item . get ( "id" , "item" ))[: 40 ], "name" : str ( item . get ( "name" , "Item" ))[: 80 ], "qty" : max ( 1 , min ( 20 , int ( item . get ( "qty" , 1 )))), "price_cents" : 0 } for item in selected [: 30 ]]
if request_text : items . append ({ "id" : "request" , "name" : request_text , "qty" : 1 , "price_cents" : 0 })
oid , timestamp = secrets . token_hex ( 5 ), now ()
note = f "Guest: { guest } " if guest else ""
with LOCK , db () as conn :
conn . execute ( "INSERT INTO orders VALUES (?,?,?,?,?,'new',?,?)" , ( oid , npub , table_number , json . dumps ( items ), note , timestamp , timestamp ))
conn . commit ()
return self . send_json ( 201 , { "id" : oid , "merchant" : nsite . get ( "name" ) or place . get ( "name" ), "table" : table_number , "status" : "new" , "created_at" : timestamp })
if path == "/api/bookings" :
npub = str ( body . get ( "npub" , "" )) . strip ()
name = str ( body . get ( "name" , "" )) . strip ()[: 80 ]
notes = str ( body . get ( "notes" , "" )) . strip ()[: 240 ]
mode = str ( body . get ( "mode" , "later" ))
try : guests = max ( 1 , min ( 20 , int ( body . get ( "guests" , 2 ))))
except ( TypeError , ValueError ): return self . send_json ( 400 , { "error" : "enter a valid guest count" })
if not name : return self . send_json ( 400 , { "error" : "tell the merchant who is booking" })
if mode not in { "now" , "later" }: return self . send_json ( 400 , { "error" : "choose now or later" })
try :
scheduled = datetime . fromisoformat ( str ( body . get ( "scheduled_at" , "" )) . replace ( "Z" , "+00:00" ))
if scheduled . tzinfo is None : scheduled = scheduled . replace ( tzinfo = timezone . utc )
current = datetime . now ( timezone . utc )
if scheduled < current . replace ( microsecond = 0 ) and ( current - scheduled ) . total_seconds () > 120 : return self . send_json ( 400 , { "error" : "choose a future time" })
if scheduled < current : scheduled = current
scheduled_at = scheduled . astimezone ( timezone . utc ) . isoformat ( timespec = "seconds" )
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error ) or "choose a valid time" })
except Exception as error : return self . send_json ( 502 , { "error" : f "merchant unavailable: { error } " })
booking_id , timestamp = secrets . token_hex ( 6 ), now ()
with LOCK , db () as conn :
conn . execute ( "INSERT INTO bookings VALUES (?,?,?,?,?,?,?,'new',?,?)" , ( booking_id , npub , name , guests , scheduled_at , mode , notes , timestamp , timestamp ))
conn . commit ()
return self . send_json ( 201 , { "id" : booking_id , "merchant" : nsite . get ( "name" ) or place . get ( "name" ), "name" : name , "guests" : guests , "scheduled_at" : scheduled_at , "status" : "new" })
if path == "/api/quick-pay" :
npub = str ( body . get ( "npub" , "" )) . strip ()
try :
amount_cents = int ( body . get ( "amount_cents" , 0 ))
if amount_cents < 1 or amount_cents > 10_000_000 :
raise ValueError ( "Enter an amount between €0.01 and €100,000" )
place , nsite = resolve_merchant_npub ( npub )
if not place : return self . send_json ( 404 , { "error" : "merchant npub not found" })
lightning_address = str ( nsite . get ( "identity" , {}) . get ( "profile" , {}) . get ( "lud16" ) or "" ) . strip ()
if not lightning_address : raise ValueError ( "This merchant has no Lightning address" )
prices = fetch_json ( "https://mempool.space/api/v1/prices" )
amount_sats = max ( 1 , round ( amount_cents * 1_000_000 / int ( prices [ "EUR" ])))
merchant_name = nsite . get ( "name" ) or place . get ( "name" )
invoice = lightning_invoice ( lightning_address , amount_sats * 1000 , str ( body . get ( "comment" , "" )))
payment_id , created_at = secrets . token_hex ( 16 ), now ()
with LOCK , db () as conn :
conn . execute ( "INSERT INTO quick_payments VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" ,
( payment_id , npub , merchant_name , amount_cents , amount_sats , invoice [ "bolt11" ],
invoice [ "zap_pubkey" ], invoice [ "recipient" ], "pending" , None , created_at , None ))
conn . commit ()
qr_svg = helper ({ "action" : "qr" , "text" : invoice [ "bolt11" ]})[ "svg" ]
return self . send_json ( 201 , { "id" : payment_id , "merchant" : merchant_name , "amount_cents" : amount_cents ,
"amount_sats" : amount_sats , "bolt11" : invoice [ "bolt11" ], "qr_svg" : qr_svg ,
"status" : "pending" , "verifiable" : invoice [ "verifiable" ], "created_at" : created_at })
except ValueError as error : return self . send_json ( 400 , { "error" : str ( error )})
except HTTPError as error : return self . send_json ( 502 , { "error" : f "Lightning provider rejected the request ( { error . code } )" })
except Exception as error : return self . send_json ( 502 , { "error" : f "Invoice unavailable: { error } " })
if path == "/api/payments" and os . environ . get ( "JWB_ARCHIPELAGO" ) == "1" :
return self . send_json ( 503 , { "error" : "Legacy order checkout is not connected to your wallet. Use the merchant website payment page." })
if path == "/api/payments" :
token , order_id = str ( body . get ( "token" , "" )), str ( body . get ( "order_id" , "" ))
with LOCK , db () as conn :
row = conn . execute ( """SELECT o.*,t.token FROM orders o JOIN tables t ON t.merchant_id=o.merchant_id AND t.table_number=o.table_number
WHERE o.id=? AND t.token=?""" , ( order_id , token )) . fetchone ()
if not row : return self . send_json ( 404 , { "error" : "order not found" })
existing = conn . execute ( "SELECT * FROM payments WHERE order_id=? ORDER BY created_at DESC LIMIT 1" , ( order_id ,)) . fetchone ()
if existing and existing [ "status" ] == "pending" : return self . send_json ( 200 , dict ( existing ))
items = json . loads ( row [ "items_json" ])
amount_cents = sum ( item [ "price_cents" ] * item [ "qty" ] for item in items )
prices = fetch_json ( "https://mempool.space/api/v1/prices" )
amount_sats = max ( 1 , round ( amount_cents * 1_000_000 / int ( prices [ "EUR" ])))
created_at = now ()
zap = helper ({ "action" : "create" , "lnurlUrl" : LNURL_URL , "relays" : PAYMENT_RELAYS , "amountMsat" : amount_sats * 1000 , "recipient" : PAYMENT_RECIPIENT , "table" : row [ "table_number" ]})
params = urlencode ({ "amount" : amount_sats * 1000 , "nostr" : json . dumps ( zap [ "event" ], separators = ( "," , ":" )), "lnurl" : zap [ "lnurl" ], "comment" : f "Just Works Table { row [ 'table_number' ] } " })
invoice = fetch_json ( f " { LNURL_CALLBACK } ? { params } " )
if invoice . get ( "status" ) == "ERROR" or not invoice . get ( "pr" ): return self . send_json ( 502 , { "error" : invoice . get ( "reason" , "invoice unavailable" )})
payment_id = secrets . token_hex ( 6 )
with LOCK , db () as conn :
conn . execute ( "INSERT INTO payments VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" , ( payment_id , order_id , row [ "merchant_id" ], row [ "table_number" ], amount_cents , amount_sats , invoice [ "pr" ], zap [ "pubkey" ], "pending" , None , created_at , None ))
conn . commit ()
return self . send_json ( 201 , { "id" : payment_id , "order_id" : order_id , "table_number" : row [ "table_number" ], "amount_cents" : amount_cents , "amount_sats" : amount_sats , "bolt11" : invoice [ "pr" ], "status" : "pending" })
if path . startswith ( "/api/orders/" ) and path . endswith ( "/status" ):
oid = path . split ( "/" )[ 3 ]
status = str ( body . get ( "status" , "" ))
if status not in { "new" , "accepted" , "delivered" , "closed" }: return self . send_json ( 400 , { "error" : "invalid status" })
with LOCK , db () as conn :
changed = conn . execute ( "UPDATE orders SET status=?,updated_at=? WHERE id=?" , ( status , now (), oid )) . rowcount
conn . commit ()
return self . send_json ( 200 if changed else 404 , { "ok" : bool ( changed ), "status" : status })
if path . startswith ( "/api/payments/" ) and path . endswith ( "/acknowledge" ):
payment_id = path . split ( "/" )[ 3 ]
with LOCK , db () as conn :
changed = conn . execute ( "UPDATE payments SET status='acknowledged' WHERE id=? AND status='paid'" , ( payment_id ,)) . rowcount
conn . commit ()
return self . send_json ( 200 if changed else 404 , { "ok" : bool ( changed ), "status" : "acknowledged" })
if path . startswith ( "/api/bookings/" ) and path . endswith ( "/status" ):
booking_id = path . split ( "/" )[ 3 ]
status = str ( body . get ( "status" , "" ))
if status not in { "new" , "confirmed" , "seated" , "closed" , "cancelled" }: return self . send_json ( 400 , { "error" : "invalid status" })
with LOCK , db () as conn :
changed = conn . execute ( "UPDATE bookings SET status=?,updated_at=? WHERE id=?" , ( status , now (), booking_id )) . rowcount
conn . commit ()
return self . send_json ( 200 if changed else 404 , { "ok" : bool ( changed ), "status" : status })
self . send_error ( 404 )
if __name__ == "__main__" :
security . mode ()
rollout . policy ()
rollout . origins ()
with db (): pass
print ( f "JustWorks Business listening on http:// { HOST } : { PORT }{ BASE } /" )
ThreadingHTTPServer (( HOST , PORT ), Handler ) . serve_forever ()