@ -17,6 +17,7 @@ import hashlib
import base64
import urllib . request
import urllib . error
import urllib . parse
import uuid
import re
import zipfile
@ -46,6 +47,7 @@ SQUARE_WEBHOOK_NOTIFICATION_URL = os.getenv("SQUARE_WEBHOOK_NOTIFICATION_URL", "
SQUARE_API_BASE = " https://connect.squareup.com "
SQUARE_API_VERSION = " 2026-01-22 "
SQUARE_WEBHOOK_LOG = str ( BASE_DIR / " logs " / " square_webhook_events.log " )
ORACLE_BASE_URL = os . getenv ( " ORACLE_BASE_URL " , " https://monitor.outsidethebox.top " )
@ -92,9 +94,90 @@ def fmt_money(value, currency_code="CAD"):
return f " { amount : .2f } "
return f " { amount : .8f } "
def normalize_oracle_datetime ( value ) :
if not value :
return None
try :
text = str ( value ) . replace ( " Z " , " +00:00 " )
dt = datetime . fromisoformat ( text )
if dt . tzinfo is None :
dt = dt . replace ( tzinfo = timezone . utc )
return dt . astimezone ( timezone . utc ) . strftime ( " % Y- % m- %d % H: % M: % S " )
except Exception :
return None
def ensure_invoice_quote_columns ( ) :
conn = get_db_connection ( )
cursor = conn . cursor ( dictionary = True )
cursor . execute ( """
SELECT COLUMN_NAME
FROM information_schema . COLUMNS
WHERE TABLE_SCHEMA = DATABASE ( )
AND TABLE_NAME = ' invoices '
""" )
existing = { row [ " COLUMN_NAME " ] for row in cursor . fetchall ( ) }
wanted = {
" quote_fiat_amount " : " ALTER TABLE invoices ADD COLUMN quote_fiat_amount DECIMAL(18,8) DEFAULT NULL AFTER status " ,
" quote_fiat_currency " : " ALTER TABLE invoices ADD COLUMN quote_fiat_currency VARCHAR(16) DEFAULT NULL AFTER quote_fiat_amount " ,
" quote_expires_at " : " ALTER TABLE invoices ADD COLUMN quote_expires_at DATETIME DEFAULT NULL AFTER quote_fiat_currency " ,
" oracle_snapshot " : " ALTER TABLE invoices ADD COLUMN oracle_snapshot LONGTEXT DEFAULT NULL AFTER quote_expires_at "
}
exec_cursor = conn . cursor ( )
changed = False
for column_name , ddl in wanted . items ( ) :
if column_name not in existing :
exec_cursor . execute ( ddl )
changed = True
if changed :
conn . commit ( )
conn . close ( )
def fetch_oracle_quote_snapshot ( currency_code , total_amount ) :
if str ( currency_code or " " ) . upper ( ) != " CAD " :
return None
try :
amount_value = Decimal ( str ( total_amount ) )
if amount_value < = 0 :
return None
except ( InvalidOperation , ValueError ) :
return None
try :
qs = urllib . parse . urlencode ( {
" fiat " : " CAD " ,
" amount " : format ( amount_value , " f " ) ,
} )
req = urllib . request . Request (
f " { ORACLE_BASE_URL . rstrip ( ' / ' ) } /api/oracle/quote? { qs } " ,
headers = {
" Accept " : " application/json " ,
" User-Agent " : " otb-billing-oracle/0.1 "
} ,
method = " GET "
)
with urllib . request . urlopen ( req , timeout = 15 ) as resp :
data = json . loads ( resp . read ( ) . decode ( " utf-8 " ) )
if not isinstance ( data , dict ) or not isinstance ( data . get ( " quotes " ) , list ) :
return None
return {
" oracle_url " : ORACLE_BASE_URL . rstrip ( " / " ) ,
" quoted_at " : data . get ( " quoted_at " ) ,
" expires_at " : data . get ( " expires_at " ) ,
" ttl_seconds " : data . get ( " ttl_seconds " ) ,
" source_status " : data . get ( " source_status " ) ,
" fiat " : data . get ( " fiat " ) or " CAD " ,
" amount " : format ( amount_value , " f " ) ,
" quotes " : data . get ( " quotes " , [ ] ) ,
}
except Exception :
return None
def square_amount_to_cents ( value ) :
return int ( ( to_decimal ( value ) * 100 ) . quantize ( Decimal ( " 1 " ) ) )
@ -2162,6 +2245,7 @@ def invoices():
@app . route ( " /invoices/new " , methods = [ " GET " , " POST " ] )
def new_invoice ( ) :
ensure_invoice_quote_columns ( )
conn = get_db_connection ( )
cursor = conn . cursor ( dictionary = True )
@ -2230,6 +2314,12 @@ def new_invoice():
if notes :
line_description = f " { service_name } - { notes } "
oracle_snapshot = fetch_oracle_quote_snapshot ( currency_code , total_amount )
oracle_snapshot_json = json . dumps ( oracle_snapshot , ensure_ascii = False ) if oracle_snapshot else None
quote_expires_at = normalize_oracle_datetime ( ( oracle_snapshot or { } ) . get ( " expires_at " ) )
quote_fiat_amount = total_amount if oracle_snapshot else None
quote_fiat_currency = currency_code if oracle_snapshot else None
insert_cursor = conn . cursor ( )
insert_cursor . execute ( """
INSERT INTO invoices
@ -2243,9 +2333,13 @@ def new_invoice():
issued_at ,
due_at ,
status ,
notes
notes ,
quote_fiat_amount ,
quote_fiat_currency ,
quote_expires_at ,
oracle_snapshot
)
VALUES ( % s , % s , % s , % s , % s , % s , UTC_TIMESTAMP ( ) , % s , ' pending ' , % s )
VALUES ( % s , % s , % s , % s , % s , % s , UTC_TIMESTAMP ( ) , % s , ' pending ' , % s , % s , % s , % s , % s )
""" , (
client_id ,
service_id ,
@ -2254,7 +2348,11 @@ def new_invoice():
total_amount ,
total_amount ,
due_at ,
notes
notes ,
quote_fiat_amount ,
quote_fiat_currency ,
quote_expires_at ,
oracle_snapshot_json
) )
invoice_id = insert_cursor . lastrowid
@ -2514,6 +2612,7 @@ def invoice_pdf(invoice_id):
@app . route ( " /invoices/view/<int:invoice_id> " )
def view_invoice ( invoice_id ) :
ensure_invoice_quote_columns ( )
conn = get_db_connection ( )
cursor = conn . cursor ( dictionary = True )
@ -2538,6 +2637,14 @@ def view_invoice(invoice_id):
conn . close ( )
return " Invoice not found " , 404
invoice [ " oracle_quote " ] = None
invoice [ " quote_expires_at_local " ] = fmt_local ( invoice . get ( " quote_expires_at " ) )
if invoice . get ( " oracle_snapshot " ) :
try :
invoice [ " oracle_quote " ] = json . loads ( invoice [ " oracle_snapshot " ] )
except Exception :
invoice [ " oracle_quote " ] = None
conn . close ( )
settings = get_app_settings ( )
return render_template ( " invoices/view.html " , invoice = invoice , settings = settings )
@ -3632,11 +3739,13 @@ def portal_invoice_detail(invoice_id):
if not client :
return redirect ( " /portal " )
ensure_invoice_quote_columns ( )
conn = get_db_connection ( )
cursor = conn . cursor ( dictionary = True )
cursor . execute ( """
SELECT id , client_id , invoice_number , status , created_at , total_amount , amount_paid
SELECT id , client_id , invoice_number , status , created_at , total_amount , amount_paid ,
quote_fiat_amount , quote_fiat_currency , quote_expires_at , oracle_snapshot
FROM invoices
WHERE id = % s AND client_id = % s
LIMIT 1
@ -3663,6 +3772,13 @@ def portal_invoice_detail(invoice_id):
invoice [ " total_amount " ] = _fmt_money ( invoice . get ( " total_amount " ) )
invoice [ " amount_paid " ] = _fmt_money ( invoice . get ( " amount_paid " ) )
invoice [ " created_at " ] = fmt_local ( invoice . get ( " created_at " ) )
invoice [ " quote_expires_at_local " ] = fmt_local ( invoice . get ( " quote_expires_at " ) )
invoice [ " oracle_quote " ] = None
if invoice . get ( " oracle_snapshot " ) :
try :
invoice [ " oracle_quote " ] = json . loads ( invoice [ " oracle_snapshot " ] )
except Exception :
invoice [ " oracle_quote " ] = None
for item in items :
item [ " quantity " ] = _fmt_money ( item . get ( " quantity " ) )