|
| 1 | +import threading |
| 2 | +import webbrowser |
| 3 | +import tkinter as tk |
| 4 | +from dataclasses import dataclass |
| 5 | +from typing import List |
| 6 | +import time |
| 7 | +import random |
| 8 | + |
| 9 | +import ttkbootstrap as tb |
| 10 | +from ttkbootstrap.constants import * |
| 11 | +from ttkbootstrap.widgets.scrolled import ScrolledText |
| 12 | +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 13 | +import matplotlib.pyplot as plt |
| 14 | + |
| 15 | +# ---------------- CONFIG ---------------- # |
| 16 | +UPDATE_INTERVAL = 2 # seconds between auto transactions |
| 17 | +PROCESSING_DELAY = 1 # seconds to simulate network processing |
| 18 | + |
| 19 | +# Payment methods with success probabilities |
| 20 | +PAYMENT_METHODS = { |
| 21 | + "Credit Card": 0.85, |
| 22 | + "UPI": 0.95, |
| 23 | + "Wallet": 0.9 |
| 24 | +} |
| 25 | + |
| 26 | +FAILURE_REASONS = [ |
| 27 | + "Insufficient Funds", |
| 28 | + "Network Error", |
| 29 | + "Card Expired", |
| 30 | + "Payment Gateway Timeout", |
| 31 | + "Invalid Credentials" |
| 32 | +] |
| 33 | + |
| 34 | +# ---------------- GLOBAL STATE ---------------- # |
| 35 | +payment_active = False |
| 36 | +transaction_history: List["Transaction"] = [] |
| 37 | + |
| 38 | +# ---------------- DATA STRUCTURE ---------------- # |
| 39 | +@dataclass(frozen=True) |
| 40 | +class Transaction: |
| 41 | + timestamp: str |
| 42 | + method: str |
| 43 | + amount: float |
| 44 | + status: str |
| 45 | + reference_id: str |
| 46 | + failure_reason: str = "" # Optional |
| 47 | + |
| 48 | +# ---------------- PAYMENT SIMULATION ---------------- # |
| 49 | +def generate_transaction(method=None, amount=None) -> Transaction: |
| 50 | + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") |
| 51 | + if method is None: |
| 52 | + method = random.choice(list(PAYMENT_METHODS.keys())) |
| 53 | + if amount is None: |
| 54 | + amount = round(random.uniform(5, 500), 2) |
| 55 | + status = "⏳ Pending" |
| 56 | + reference_id = f"{method[:2].upper()}{random.randint(100000,999999)}" |
| 57 | + return Transaction(timestamp, method, amount, status, reference_id) |
| 58 | + |
| 59 | +def finalize_transaction(txn: Transaction): |
| 60 | + success_prob = PAYMENT_METHODS[txn.method] |
| 61 | + if random.random() < success_prob: |
| 62 | + return Transaction(txn.timestamp, txn.method, txn.amount, "✅ Success", txn.reference_id) |
| 63 | + else: |
| 64 | + reason = random.choice(FAILURE_REASONS) |
| 65 | + return Transaction(txn.timestamp, txn.method, txn.amount, "❌ Failed", txn.reference_id, failure_reason=reason) |
| 66 | + |
| 67 | +# ---------------- PROCESSING THREAD ---------------- # |
| 68 | +def processing_loop(): |
| 69 | + global payment_active |
| 70 | + while payment_active: |
| 71 | + txn = generate_transaction() |
| 72 | + transaction_history.append(txn) |
| 73 | + app.after(0, lambda t=txn: display_transaction(t)) |
| 74 | + time.sleep(PROCESSING_DELAY) |
| 75 | + |
| 76 | + # Finalize the pending transaction |
| 77 | + finalized_txn = finalize_transaction(txn) |
| 78 | + transaction_history[-1] = finalized_txn |
| 79 | + app.after(0, lambda t=finalized_txn: update_transaction_display(t)) |
| 80 | + time.sleep(UPDATE_INTERVAL - PROCESSING_DELAY) |
| 81 | + |
| 82 | +# ---------------- UI HELPERS ---------------- # |
| 83 | +def format_transaction_text(txn: Transaction) -> str: |
| 84 | + base = f"💰 {txn.timestamp}\nMethod: {txn.method}\nAmount: ${txn.amount}\nStatus: {txn.status}\nRef: {txn.reference_id}" |
| 85 | + if txn.status == "❌ Failed" and txn.failure_reason: |
| 86 | + base += f"\nReason: {txn.failure_reason}" |
| 87 | + return base + "\n\n" |
| 88 | + |
| 89 | +def display_transaction(txn: Transaction): |
| 90 | + text.configure(state="normal") |
| 91 | + text.insert("end", format_transaction_text(txn)) |
| 92 | + text.see("end") |
| 93 | + text.configure(state="disabled") |
| 94 | + update_stats() |
| 95 | + |
| 96 | +def update_transaction_display(txn: Transaction): |
| 97 | + # Update last transaction to finalized status |
| 98 | + text.configure(state="normal") |
| 99 | + text.delete("end-8l", "end") |
| 100 | + text.insert("end", format_transaction_text(txn)) |
| 101 | + text.see("end") |
| 102 | + text.configure(state="disabled") |
| 103 | + update_stats() |
| 104 | + |
| 105 | +def open_gateway_docs(): |
| 106 | + webbrowser.open_new_tab("https://www.example-payment-gateway.com/docs") |
| 107 | + |
| 108 | +# ---------------- STATS & CHART ---------------- # |
| 109 | +def update_stats(): |
| 110 | + success = sum(1 for t in transaction_history if t.status == "✅ Success") |
| 111 | + failed = sum(1 for t in transaction_history if t.status == "❌ Failed") |
| 112 | + pending = sum(1 for t in transaction_history if t.status == "⏳ Pending") |
| 113 | + stats_label.config(text=f"✅ {success} ❌ {failed} ⏳ {pending}") |
| 114 | + |
| 115 | + # Update pie chart safely |
| 116 | + ax.clear() |
| 117 | + labels = ["Success", "Failed", "Pending"] |
| 118 | + sizes = [success, failed, pending] |
| 119 | + colors = ["green", "red", "orange"] |
| 120 | + |
| 121 | + if sum(sizes) == 0: |
| 122 | + # Draw a blank circle instead of pie chart |
| 123 | + ax.text(0.5, 0.5, "No Transactions", ha="center", va="center", fontsize=12) |
| 124 | + ax.axis("off") |
| 125 | + else: |
| 126 | + ax.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90) |
| 127 | + |
| 128 | + canvas.draw() |
| 129 | + |
| 130 | +# ---------------- CONTROL FUNCTIONS ---------------- # |
| 131 | +def start_processing(): |
| 132 | + global payment_active |
| 133 | + if payment_active: |
| 134 | + return |
| 135 | + payment_active = True |
| 136 | + status_label.config(text="🟢 Payment Processing Active", bootstyle="success") |
| 137 | + threading.Thread(target=processing_loop, daemon=True).start() |
| 138 | + |
| 139 | +def stop_processing(): |
| 140 | + global payment_active |
| 141 | + payment_active = False |
| 142 | + status_label.config(text="🔴 Payment Processing Stopped", bootstyle="danger") |
| 143 | + |
| 144 | +def clear_history(): |
| 145 | + transaction_history.clear() |
| 146 | + text.configure(state="normal") |
| 147 | + text.delete("1.0", "end") |
| 148 | + text.configure(state="disabled") |
| 149 | + update_stats() |
| 150 | + |
| 151 | +# ---------------- MANUAL PAYMENT SUBMISSION ---------------- # |
| 152 | +def submit_payment(): |
| 153 | + method = method_var.get() |
| 154 | + try: |
| 155 | + amount = float(amount_entry.get()) |
| 156 | + if amount <= 0: |
| 157 | + raise ValueError |
| 158 | + except ValueError: |
| 159 | + tb.messagebox.showerror("Invalid Amount", "Please enter a valid positive number for amount.") |
| 160 | + return |
| 161 | + |
| 162 | + txn = generate_transaction(method, amount) |
| 163 | + transaction_history.append(txn) |
| 164 | + display_transaction(txn) |
| 165 | + |
| 166 | + # Simulate processing delay in background |
| 167 | + def finalize(): |
| 168 | + time.sleep(PROCESSING_DELAY) |
| 169 | + finalized_txn = finalize_transaction(txn) |
| 170 | + transaction_history[-1] = finalized_txn |
| 171 | + app.after(0, lambda t=finalized_txn: update_transaction_display(t)) |
| 172 | + threading.Thread(target=finalize, daemon=True).start() |
| 173 | + |
| 174 | +# ---------------- TRANSACTION SEARCH/FILTER ---------------- # |
| 175 | +def filter_transactions(): |
| 176 | + query_method = filter_method_var.get() |
| 177 | + query_status = filter_status_var.get().lower() |
| 178 | + query_ref = filter_ref_entry.get().strip().lower() |
| 179 | + |
| 180 | + text.configure(state="normal") |
| 181 | + text.delete("1.0", "end") |
| 182 | + for txn in transaction_history: |
| 183 | + if query_method != "All" and txn.method != query_method: |
| 184 | + continue |
| 185 | + if query_status != "all" and txn.status.lower() != query_status: |
| 186 | + continue |
| 187 | + if query_ref and query_ref not in txn.reference_id.lower(): |
| 188 | + continue |
| 189 | + text.insert("end", format_transaction_text(txn)) |
| 190 | + text.configure(state="disabled") |
| 191 | + |
| 192 | +# ---------------- UI SETUP ---------------- # |
| 193 | +app = tb.Window( |
| 194 | + title="Online Payment Gateway Simulator", |
| 195 | + themename="darkly", |
| 196 | + size=(1000, 700), |
| 197 | + resizable=(True, True), |
| 198 | +) |
| 199 | + |
| 200 | +# Top section |
| 201 | +top = tb.Frame(app, padding=15) |
| 202 | +top.pack(fill=tk.X) |
| 203 | +tb.Label(top, text="💳 Online Payment Gateway Simulator", font=("Segoe UI", 18, "bold")).pack(anchor=tk.W) |
| 204 | +status_label = tb.Label(top, text="🔴 Payment Processing Stopped", bootstyle="danger") |
| 205 | +status_label.pack(anchor=tk.W, pady=5) |
| 206 | + |
| 207 | +# Auto transaction buttons |
| 208 | +btn_frame = tb.Frame(app) |
| 209 | +btn_frame.pack(fill=tk.X, pady=5) |
| 210 | +tb.Button(btn_frame, text="▶ Start Auto Transactions", bootstyle="success", command=start_processing).pack(side=tk.LEFT, padx=5) |
| 211 | +tb.Button(btn_frame, text="⏹ Stop Auto Transactions", bootstyle="danger", command=stop_processing).pack(side=tk.LEFT, padx=5) |
| 212 | +tb.Button(btn_frame, text="📄 Docs", bootstyle="info", command=open_gateway_docs).pack(side=tk.LEFT, padx=5) |
| 213 | +tb.Button(btn_frame, text="🧹 Clear", bootstyle="warning", command=clear_history).pack(side=tk.LEFT, padx=5) |
| 214 | + |
| 215 | +# Manual payment section |
| 216 | +manual_frame = tb.Labelframe(app, text="Manual Payment Submission", padding=10) |
| 217 | +manual_frame.pack(fill=tk.X, pady=10) |
| 218 | +method_var = tk.StringVar(value="Credit Card") |
| 219 | +tb.Label(manual_frame, text="Payment Method:").pack(side=tk.LEFT, padx=5) |
| 220 | +tb.OptionMenu(manual_frame, method_var, *PAYMENT_METHODS.keys()).pack(side=tk.LEFT, padx=5) |
| 221 | +tb.Label(manual_frame, text="Amount:").pack(side=tk.LEFT, padx=5) |
| 222 | +amount_entry = tb.Entry(manual_frame, width=10) |
| 223 | +amount_entry.pack(side=tk.LEFT, padx=5) |
| 224 | +amount_entry.insert(0, "100") |
| 225 | +tb.Button(manual_frame, text="💳 Submit Payment", bootstyle="primary", command=submit_payment).pack(side=tk.LEFT, padx=10) |
| 226 | + |
| 227 | +# Transaction filter/search section |
| 228 | +filter_frame = tb.Labelframe(app, text="Search / Filter Transactions", padding=10) |
| 229 | +filter_frame.pack(fill=tk.X, pady=10) |
| 230 | +filter_method_var = tk.StringVar(value="All") |
| 231 | +tb.Label(filter_frame, text="Method:").pack(side=tk.LEFT, padx=5) |
| 232 | +tb.OptionMenu(filter_frame, filter_method_var, "All", *PAYMENT_METHODS.keys()).pack(side=tk.LEFT, padx=5) |
| 233 | +filter_status_var = tk.StringVar(value="All") |
| 234 | +tb.Label(filter_frame, text="Status:").pack(side=tk.LEFT, padx=5) |
| 235 | +tb.OptionMenu(filter_frame, filter_status_var, "All", "✅ Success", "❌ Failed", "⏳ Pending").pack(side=tk.LEFT, padx=5) |
| 236 | +tb.Label(filter_frame, text="Reference ID:").pack(side=tk.LEFT, padx=5) |
| 237 | +filter_ref_entry = tb.Entry(filter_frame, width=15) |
| 238 | +filter_ref_entry.pack(side=tk.LEFT, padx=5) |
| 239 | +tb.Button(filter_frame, text="🔍 Filter", bootstyle="info", command=filter_transactions).pack(side=tk.LEFT, padx=10) |
| 240 | + |
| 241 | +# Transaction log |
| 242 | +result_frame = tb.Frame(app) |
| 243 | +result_frame.pack(fill=tk.BOTH, expand=True, side=tk.LEFT) |
| 244 | +result_box = ScrolledText(result_frame) |
| 245 | +result_box.pack(fill=tk.BOTH, expand=True) |
| 246 | +text = result_box.text |
| 247 | +text.configure(state="disabled", wrap="word") |
| 248 | + |
| 249 | +# Stats & Pie Chart |
| 250 | +stats_frame = tb.Frame(app, padding=10) |
| 251 | +stats_frame.pack(fill=tk.BOTH, side=tk.RIGHT, expand=False) |
| 252 | +stats_label = tb.Label(stats_frame, text="✅ 0 ❌ 0 ⏳ 0", font=("Segoe UI", 12, "bold")) |
| 253 | +stats_label.pack(pady=10) |
| 254 | +fig, ax = plt.subplots(figsize=(4,4)) |
| 255 | +canvas = FigureCanvasTkAgg(fig, master=stats_frame) |
| 256 | +canvas.get_tk_widget().pack() |
| 257 | +ax.text(0.5, 0.5, "No Transactions", ha="center", va="center", fontsize=12) |
| 258 | +ax.axis("off") |
| 259 | + |
| 260 | +canvas.draw() |
| 261 | + |
| 262 | +# Run app |
| 263 | +app.mainloop() |
0 commit comments