diff --git a/src/__pycache__/billing.cpython-311.pyc b/src/__pycache__/billing.cpython-311.pyc index ab1e494..3f2e9d6 100644 Binary files a/src/__pycache__/billing.cpython-311.pyc and b/src/__pycache__/billing.cpython-311.pyc differ diff --git a/src/billing.py b/src/billing.py index de21965..cfb2e4b 100644 --- a/src/billing.py +++ b/src/billing.py @@ -148,16 +148,28 @@ class BillingDatabase: connection = self._get_connection(resolved_id) try: with connection.cursor() as cursor: - # Comprehensive query to get all necessary data in one go - sql = """ + # Get current month and year for invoice lookup + from datetime import datetime as dt + current_month = dt.now().strftime('%m') + current_year = dt.now().year + + # Comprehensive query to get all necessary data including current invoice status + sql = f""" SELECT c.*, pi.name as package_name, pi.price as package_price, - pi.description as package_description + pi.description as package_description, + inv.status as invoice_status, + inv.month as invoice_month, + inv.year as invoice_year, + inv.inv_due_date as invoice_due_date FROM customer c LEFT JOIN services s ON c.no_services = s.no_services LEFT JOIN package_item pi ON s.item_id = pi.p_item_id + LEFT JOIN invoice inv ON c.no_services = inv.no_services + AND inv.month = '{current_month}' + AND inv.year = {current_year} """ cursor.execute(sql) customers = cursor.fetchall() @@ -244,11 +256,15 @@ class BillingDatabase: else: # Handle generic text search token_lower = token.lower() - # Check multiple fields + # Check multiple fields including email, customer ID, and KTP if not (token_lower in str(cust.get('name', '')).lower() or token_lower in str(cust.get('no_wa', '')).lower() or token_lower in str(cust.get('address', '')).lower() or - token_lower in str(cust.get('user_profile', '')).lower()): + token_lower in str(cust.get('user_profile', '')).lower() or + token_lower in str(cust.get('email', '')).lower() or + token_lower in str(cust.get('no_services', '')).lower() or + token_lower in str(cust.get('user_mikrotik', '')).lower() or + token_lower in str(cust.get('no_ktp', '')).lower()): match_all = False break diff --git a/src/server.py b/src/server.py index 7fb0abc..3948223 100644 --- a/src/server.py +++ b/src/server.py @@ -95,7 +95,10 @@ async def list_tools() -> List[Tool]: "type": "object", "properties": { "query": { - "type": "string", + "oneOf": [ + {"type": "string"}, + {"type": "integer"} + ], "description": "Search query (User ID, Name, or IP address). Leave empty to list all (use with limit)." }, "limit": { @@ -158,6 +161,33 @@ async def list_tools() -> List[Tool]: "type": "object", "properties": {}, } + ), + Tool( + name="get_customer_details", + description="Get detailed information about a specific customer by their User ID (no_services) or phone number.", + inputSchema={ + "type": "object", + "properties": { + "customer_id": { + "oneOf": [ + {"type": "string"}, + {"type": "integer"} + ], + "description": "The customer's User ID (no_services field, e.g. '221001182866')" + }, + "phone_number": { + "oneOf": [ + {"type": "string"}, + {"type": "integer"} + ], + "description": "The customer's phone number (alternative to customer_id)" + }, + "isp_name": { + "type": "string", + "description": "Optional: Specific ISP/Provider name. If omitted, defaults to first available." + } + } + } ) ] @@ -166,8 +196,8 @@ async def call_tool(name: str, arguments: Any) -> List[TextContent | ImageConten db = get_billing_db() if name == "search_customers": - query = arguments.get("query", "") - limit = arguments.get("limit", 5) + query = str(arguments.get("query", "")) # Convert to string in case it's a number + limit = arguments.get("limit", 20) # Increased default limit for Telegram isp_name = arguments.get("isp_name", "1") # Default logic in simple mode # If isp_name is not provided, BillingDatabase likely handles "1" as default fallback @@ -180,19 +210,47 @@ async def call_tool(name: str, arguments: Any) -> List[TextContent | ImageConten return [TextContent(type="text", text=f"Error searching ({isp_name}): {result.get('error')}")] customers = result.get('customers', []) + total_found = result.get('total', len(customers)) + if not customers: return [TextContent(type="text", text=f"No customers found matching '{query}'.")] - # Format output - output_lines = [f"Found {len(customers)} customers (ISP: {result.get('server_id')}):"] + # Format output with total count + if total_found > len(customers): + output_lines = [f"📊 Ditemukan {total_found} pelanggan, menampilkan {len(customers)} hasil (ISP: {result.get('server_id')}):"] + else: + output_lines = [f"📊 Ditemukan {total_found} pelanggan (ISP: {result.get('server_id')}):"] for c in customers: - # Format key details + # Determine payment status from invoice table (current month) + invoice_status = c.get('invoice_status', '') + if invoice_status: + if invoice_status.upper() == 'SUDAH BAYAR': + payment_status = "✅ Lunas" + elif invoice_status.upper() == 'BELUM BAYAR': + payment_status = "⚠️ Belum Bayar" + else: + payment_status = f"📋 {invoice_status}" + else: + # Fallback to action field if no invoice exists + action = c.get('action', 0) + payment_status = "✅ Lunas" if action == 1 else "⏳ Belum Ada Invoice" + + # Format amount + amount = c.get('cust_amount', 0) + amount_str = f"Rp{amount:,}".replace(",", ".") if amount else "N/A" + + # Use invoice due date if available + due_date = c.get('invoice_due_date') or f"Tgl {c.get('due_date', 'N/A')}" + + # Format key details with billing info details = [ f"User: {c.get('user_mikrotik', 'N/A')}", f"Name: {c.get('name', 'N/A')}", f"Status: {c.get('c_status', 'N/A')}", - f"Address: {c.get('address', 'N/A')}", - f"Packet: {c.get('user_profile', 'N/A')}" + f"Packet: {c.get('user_profile', 'N/A')}", + f"Tagihan: {amount_str}", + f"Jatuh Tempo: {due_date}", + payment_status ] output_lines.append(" | ".join(details)) @@ -301,6 +359,74 @@ async def call_tool(name: str, arguments: Any) -> List[TextContent | ImageConten return [TextContent(type="text", text="\n".join(output))] + elif name == "get_customer_details": + customer_id = arguments.get("customer_id") + phone_number = arguments.get("phone_number") + isp_name = arguments.get("isp_name", "1") + + # Convert to string if provided (handles integer inputs) + if customer_id is not None: + customer_id = str(customer_id) + if phone_number is not None: + phone_number = str(phone_number) + + if not customer_id and not phone_number: + return [TextContent(type="text", text="Error: Either customer_id or phone_number must be provided.")] + + result = db.get_customer_details( + customer_id=customer_id, + phone_number=phone_number, + server_id=isp_name + ) + + if not result['success']: + return [TextContent(type="text", text=f"Error: {result.get('error')}")] + + c = result.get('customer', {}) + + # Determine payment status form invoice table (current month) + invoice_status = c.get('invoice_status', '') + if invoice_status: + if invoice_status.upper() == 'SUDAH BAYAR': + payment_status = "✅ Lunas" + elif invoice_status.upper() == 'BELUM BAYAR': + payment_status = "⚠️ Belum Bayar" + else: + payment_status = f"📋 {invoice_status}" + else: + # Fallback + action = c.get('action', 0) + payment_status = "✅ Lunas" if action == 1 else "⏳ Belum Ada Invoice" + + # Use invoice due date if available + due_date = c.get('invoice_due_date') or f"Tanggal {c.get('due_date', 'N/A')}" + + # Format amount + amount = c.get('cust_amount', 0) + amount_str = f"Rp{amount:,}".replace(",", ".") if amount else "N/A" + + # Format detailed output with billing info + output_lines = [ + "📋 Customer Details:", + f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + f"👤 Name: {c.get('name', 'N/A')}", + f"🆔 User ID: {c.get('no_services', 'N/A')}", + f"🌐 Mikrotik User: {c.get('user_mikrotik', 'N/A')}", + f"📊 Status: {c.get('c_status', 'N/A')}", + f"📦 Package: {c.get('user_profile', 'N/A')} ({c.get('package_name', 'N/A')})", + f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + f"💰 Tagihan: {amount_str}", + f"📅 Jatuh Tempo: {due_date}", + f"💳 Status Bayar: {payment_status}", + f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + f"📍 Address: {c.get('address', 'N/A')}", + f"📱 Phone: {c.get('no_wa', 'N/A')}", + f"📧 Email: {c.get('email', 'N/A')}", + f"🔗 Router: {c.get('router', 'N/A')}", + ] + + return [TextContent(type="text", text="\n".join(output_lines))] + raise ValueError(f"Unknown tool: {name}") async def main():