From 4717906d1475857fee54ce925761a83b6f3fd591 Mon Sep 17 00:00:00 2001 From: wartana Date: Tue, 3 Feb 2026 22:40:57 +0800 Subject: [PATCH] Fix: Update invoice status logic to use current month invoice table, fixing incorrect payment status display --- src/__pycache__/billing.cpython-311.pyc | Bin 21533 -> 22929 bytes src/billing.py | 26 ++++- src/server.py | 142 ++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 13 deletions(-) diff --git a/src/__pycache__/billing.cpython-311.pyc b/src/__pycache__/billing.cpython-311.pyc index ab1e4943a2bc0d042e71b74f6d225fc4b95f8982..3f2e9d6096c4bae014130d5a2454d43571650b68 100644 GIT binary patch delta 4364 zcmaJ^eQ;FO6@T}={mO23lZD+)mV7PA=3`^Nmt^@!NMb%9LJ}nrWnv~Q`yOP;euVqp zLVyjy8K+Xk8s~PZ9c2Wnh@-UPj$*A-p^7k8TEe6zzWyOP)9LiX=`wX3T4y@F=e{H$ zrM}tyy>rez_uljFIp^MUkDW%(q>$|utJTcWr{-UehTrZ!Yjg1E)l>=JtV3C;?S_MB z*>c*5b129>1&;?i$XCXl$dwv2MR?>O7tE8$MOrQG*~Oq)0^kKGCDWFGK@*ZImRw5} zNNWJa^EHG!%JUAouI5$S(>W$rSmB5EUl% zh)e3p4Y!y4z0E}qwPX`%;|lV=$yU4@e9WFnqo_+7p65h;(n!&8Oh0K#>W&+Uz0gTs zTa#s=W9W{X$QL!m{A321^3m9HGF1M8uzQ_sVRVmKJ@i;D-aSCzVYxJ)@qUi-8 znIUGpz)zZ!+;IyVdAcGyXi?mYI<%KXG>ewkGfxS$Fa2vzBR83;x7)TzC(X%B z(W(?K@*MZN?Uau8rGM=~9JBVtB6n?;n4RS5*V&H=-)reIYN>EM8$Q;IxB;$&TV%<( z9&0tk!os6K$}Q4mixkK06ucbHpR|h0$#eG>taQAbjvJNb~_o02{`KzzG|*ozAt?R}k0u36jSDL+tC4h{Cj24-T5=YW&W z;vB&{T|)-LLXkfhN6# zaUA;`nB`vtLIV>rN#YzIwzZ$!3{9sYk*s_yHPi9-n^SQ z-JGf`JB+3d(KUJ2*R6cD=klHFCS1Rl?m6IGX1^22yC6ZWX2 zrIVZHhqp}HL{rkXm%|fc#$#O4M%p*J$cxpNta{NhX+_*LihX)?itMTRv0)m?k0+Ot z8`WM?UE2+8{{s8G#^x-r8Fd?k>l)3A+2le^6xk_u5v8_*))GK{NvY+Gy1&jW@J9#9 za9u5ZE9B|A_AEzIpF~MVQb_8OPSHs|uNzwFTG(H6CS8YIbbn><+xFL7GF1N@dWw8q z|FEZ=E`|@OdYYx-ak^_%P+mpjw~CiYLl?S9-fZw?vJ$45ws+I2>dy_uQ+g>H3`bPU zZCjgaP0&0aiH65;TnUe<2HG~JjH{-_#H5t~+jtCeJb^=!wu3zllj@bY5{%$!O50W)cH z-kOJ%4x7k%zleOKbXAr6Bvn%FmR=Ah2QPkrpv`Mf!^6b z?~K21*55bhAH3$L9kO5uvl z-dieMDMCP(>U%3wbAbuI9Q=fjQ5eL>DblkE+>B39P)&nW3%hCxzZxOZYT9G#uUpFQ!e+qi>O${ol!{e0h!_DNY^?B=AV6m#S5j0FYaXbq5 z0-sJZgIVw_ZNk5zAj7!{GicX@cS(0^6>2AP>n~|pU)_cT)J$qS{BB4jypVmmup&ie zs14y_bFiZe?IIUCT$^1~GDwrsN8hRuOM4|OOMeleCm2W<`9o;Yvm;+Mk4rS8VoGqA zgpWhB{+17L4k_upX9~R3=gYp8O=0BQ6XZs5_fS3 z^P}(1lUz$+NGAL{7$g&uE1d=C4EbAUVaabn{0RW_%fjKj8h&^R8lYhDEks)m2w03&0`(}s1o`=r)2vCqv zst}GT_$^@H19%tU4*-7zfN;YQR9bApqGNzuj?;0nAfz7YZ%2W4qz|Myz|$q#`yJgo z1LsTLdH6k>#@_|Id2;3cmZ8n8FW_~TJFRD|fpg@fjf^yL*~`-zj5Kq0N7};3OwMdO z5}NiO89iE$P* VJ?ckk?*G4TXl-AAj|y|je*vKlR!{%{ delta 3653 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 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():