Skip to content

Commit f54dfcc

Browse files
author
Sqoia Dev Agent
committed
feat: audit trail, batch invoices, webhook status, rate history
Audit Trail: - Every invoice status change, creation, and refund logged with timestamp, actor, and before/after values - Expandable audit timeline per invoice in the Invoices view - Rate change history log (last 50 entries) with human-readable diffs Invoice Improvements: - Batch invoice generation: 'Generate All Invoices' creates one per account with progress tracking and sequential numbering - Pre-flight check blocks export when institution profile incomplete - Export Invoice button disabled during generation (prevents duplicates) - Webhook status tracked per invoice (success/failed with timestamp) - Webhook failures surfaced as UI errors, not just console.warn UX Polish: - State/Province changed to free text input (international support) - Contact field asterisks now backed by real validation - Webhook status column in invoice table (green check/red warning)
1 parent ffc19c5 commit f54dfcc

2 files changed

Lines changed: 462 additions & 87 deletions

File tree

src/ledger_util.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import shutil
77
import sys
8+
from datetime import datetime
89

910
LEDGER_PATH = '/etc/slurmledger/invoices.json'
1011
BACKUP_COUNT = 3
@@ -58,6 +59,13 @@ def update_invoice(invoice_id, patch):
5859
print(json.dumps({"error": "InvalidTransition",
5960
"message": f"Cannot change status from '{current}' to '{new_status}'"}))
6061
sys.exit(1)
62+
inv.setdefault('audit_log', []).append({
63+
'action': 'status_change',
64+
'from': current,
65+
'to': new_status,
66+
'by': os.environ.get('USER', 'unknown'),
67+
'at': datetime.utcnow().isoformat() + 'Z'
68+
})
6169
inv.update(patch)
6270
break
6371
else:
@@ -80,6 +88,11 @@ def update_invoice(invoice_id, patch):
8088
update_invoice(args.invoice_id, patch)
8189
elif args.action == 'add':
8290
invoice = json.load(sys.stdin)
91+
invoice.setdefault('audit_log', []).append({
92+
'action': 'created',
93+
'by': os.environ.get('USER', 'unknown'),
94+
'at': datetime.utcnow().isoformat() + 'Z'
95+
})
8396
data = read_ledger()
8497
data['invoices'].append(invoice)
8598
write_ledger(data)
@@ -90,6 +103,13 @@ def update_invoice(invoice_id, patch):
90103
for inv in data.get('invoices', []):
91104
if inv.get('id') == args.invoice_id:
92105
inv.setdefault('refunds', []).append(refund)
106+
inv.setdefault('audit_log', []).append({
107+
'action': 'refund_issued',
108+
'refund_id': refund.get('id', ''),
109+
'amount': refund.get('amount', 0),
110+
'by': os.environ.get('USER', 'unknown'),
111+
'at': datetime.utcnow().isoformat() + 'Z'
112+
})
93113
if refund.get('full'):
94114
inv['status'] = 'refunded'
95115
break

0 commit comments

Comments
 (0)