Skip to content

Commit aac8666

Browse files
committed
Merge main and resolve pycache conflicts
2 parents 549d25b + a072a62 commit aac8666

9 files changed

Lines changed: 205 additions & 61 deletions

File tree

-5.94 KB
Binary file not shown.
-17.6 KB
Binary file not shown.

ai_brain.py

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,26 @@
1313
def get_system_instruction():
1414
categories_str = " | ".join([f'"{cat}"' for cat in config.TRANSACTION_CATEGORIES])
1515
instruction = f"""
16-
Kamu adalah asisten AI spesialis pencatatan keuangan pribadi. Tugasmu adalah mengekstrak data dari pesan pengguna ke format JSON murni.
16+
Kamu adalah asisten AI spesialis pencatatan keuangan pribadi. Tugasmu adalah mengekstrak data dari pesan pengguna ke format JSON MURNI dalam bentuk LIST (ARRAY).
1717
1818
SKEMA JSON:
19-
{{
20-
"tipe": "pemasukan" | "pengeluaran" | "investasi",
21-
"item": "deskripsi singkat transaksi",
22-
"nominal": angka integer,
23-
"kategori": {categories_str}
24-
}}
19+
[
20+
{{
21+
"tipe": "pemasukan" | "pengeluaran" | "investasi" | "saldo",
22+
"item": "deskripsi singkat transaksi",
23+
"nominal": angka integer,
24+
"kategori": {categories_str}
25+
}},
26+
...
27+
]
2528
2629
ATURAN KETAT:
27-
1. Konversi singkatan angka: "k" / "rb" = 1000, "jt" / "juta" = 1000000.
28-
2. Jika nominal dalam teks (misal: "setengah juta", "seratus ribu"), ubah ke angka integer (misal: 500000, 100000).
29-
3. Fokus pada uang masuk, keluar, atau investasi.
30-
4. Jawaban HANYA berupa JSON murni tanpa teks pengantar. Jika error, kembalikan {{"error": true}}.
30+
1. Gunakan format LIST [ ... ] meskipun hanya ada satu transaksi.
31+
2. Gunakan tipe "saldo" jika pengguna menyebutkan total uang/saldo saat ini (misal: "saldo saya 1 juta").
32+
3. Konversi singkatan angka: "k" / "rb" = 1000, "jt" / "juta" = 1000000.
33+
4. Jika nominal dalam teks (misal: "setengah juta", "seratus ribu"), ubah ke angka integer (misal: 500000, 100000).
34+
5. Fokus pada uang masuk, keluar, atau investasi/saldo.
35+
6. Jawaban HANYA berupa JSON murni tanpa teks pengantar. Jika tidak ada transaksi sama sekali, kembalikan [ {{ "error": true }} ].
3136
"""
3237
return instruction
3338

@@ -123,11 +128,17 @@ def extract_json_from_text(text: str) -> str:
123128
return text.split("```")[1].split("```")[0].strip()
124129
except: pass
125130

126-
# 3. Cari dari kurung kurawal pertama sampai terakhir (paling robust)
127-
start_index = text.find('{')
128-
end_index = text.rfind('}')
129-
if start_index != -1 and end_index != -1 and end_index > start_index:
130-
return text[start_index:end_index+1].strip()
131+
# 3. Cari dari kurung siku pertama sampai terakhir (untuk list)
132+
start_array = text.find('[')
133+
end_array = text.rfind(']')
134+
if start_array != -1 and end_array != -1 and end_array > start_array:
135+
return text[start_array:end_array+1].strip()
136+
137+
# 4. Cari dari kurung kurawal pertama sampai terakhir (fallback jika AI mengembalikan single object)
138+
start_obj = text.find('{')
139+
end_obj = text.rfind('}')
140+
if start_obj != -1 and end_obj != -1 and end_obj > start_obj:
141+
return text[start_obj:end_obj+1].strip()
131142

132143
return text
133144

@@ -167,12 +178,26 @@ def get_json_data_from_text(text: str) -> dict:
167178

168179
data = json.loads(json_text)
169180

170-
# Validasi minimal struktur data
171-
if not any(k in data for k in ["tipe", "nominal", "item"]):
172-
raise ValueError("JSON berhasil diparsing tapi struktur tidak sesuai")
181+
# Normalisasi ke format LIST jika AI mengembalikan single object
182+
if isinstance(data, dict):
183+
data = [data]
184+
185+
if not isinstance(data, list):
186+
raise ValueError("Format respons AI bukan merupakan list JSON")
187+
188+
# Validasi minimal struktur data untuk setiap item
189+
valid_items = []
190+
for item in data:
191+
if item.get("error"):
192+
continue
193+
if any(k in item for k in ["tipe", "nominal", "item"]):
194+
valid_items.append(item)
195+
196+
if not valid_items and any(item.get("error") for item in data):
197+
return {"error": True}
173198

174-
print(f"[ai_brain] Memanfaatkan tenaga {provider.capitalize()} berhasil!")
175-
return data
199+
print(f"[ai_brain] Memanfaatkan tenaga {provider.capitalize()} berhasil! ({len(valid_items)} transaksi)")
200+
return valid_items
176201

177202
except Exception as e:
178203
# Jika KeyError / Network Error / Rate Limit / Parsing Error dll

dashboard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import io
22
import json
33
from flask import Flask, render_template, request, redirect, url_for, flash, send_file, Response
4-
from utils.excel_generator import generate_excel_report
4+
from utils.excel_builder import build_excel
55
import database
66
from datetime import datetime
77

@@ -160,7 +160,7 @@ def export_excel():
160160
flash("Gagal mengambil data transaksi untuk export.", "error")
161161
return redirect(url_for('index'))
162162

163-
wb = generate_excel_report(transactions, user_label, month_label, include_user_info=True)
163+
wb = build_excel(transactions, user_label, month_label, include_user_info=True)
164164

165165
# Simpan ke buffer memori
166166
buf = io.BytesIO()

database.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import mysql.connector
3+
from mysql.connector import pooling
34
from datetime import datetime
45
from dotenv import load_dotenv
56

@@ -9,6 +10,27 @@
910
DB_USER = os.getenv("DB_USER", "root")
1011
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
1112
DB_NAME = os.getenv("DB_NAME", "finance_bot")
13+
DB_POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "5"))
14+
15+
# Konfigurasi Koneksi
16+
db_config = {
17+
"host": DB_HOST,
18+
"user": DB_USER,
19+
"password": DB_PASSWORD,
20+
"database": DB_NAME
21+
}
22+
23+
# Inisialisasi Connection Pool
24+
try:
25+
db_pool = pooling.MySQLConnectionPool(
26+
pool_name="finance_bot_pool",
27+
pool_size=DB_POOL_SIZE,
28+
**db_config
29+
)
30+
print(f"Connection pool created with size: {DB_POOL_SIZE}")
31+
except mysql.connector.Error as err:
32+
print(f"Error creating connection pool: {err}")
33+
db_pool = None
1234

1335
def get_base_connection():
1436
return mysql.connector.connect(
@@ -18,12 +40,9 @@ def get_base_connection():
1840
)
1941

2042
def get_connection():
21-
return mysql.connector.connect(
22-
host=DB_HOST,
23-
user=DB_USER,
24-
password=DB_PASSWORD,
25-
database=DB_NAME
26-
)
43+
if db_pool:
44+
return db_pool.get_connection()
45+
return mysql.connector.connect(**db_config)
2746

2847
def init_db():
2948
# Buat database jika belum ada
@@ -669,3 +688,25 @@ def get_all_transactions_export(month_str=None, user_id=None):
669688
finally:
670689
cursor.close()
671690
conn.close()
691+
692+
def get_user_balance(user_id):
693+
"""
694+
Menghitung total saldo saat ini dari user (Pemasukan - Pengeluaran - Investasi).
695+
"""
696+
conn = get_connection()
697+
try:
698+
cursor = conn.cursor(dictionary=True)
699+
# Hitung saldo: sum income - sum expense - sum investment
700+
cursor.execute('''
701+
SELECT
702+
SUM(CASE WHEN tipe = 'pemasukan' THEN nominal ELSE 0 END) -
703+
SUM(CASE WHEN tipe = 'pengeluaran' THEN nominal ELSE 0 END) -
704+
SUM(CASE WHEN tipe = 'investasi' THEN nominal ELSE 0 END) as balance
705+
FROM transactions
706+
WHERE user_id = %s
707+
''', (user_id,))
708+
res = cursor.fetchone()
709+
return res['balance'] if res['balance'] is not None else 0
710+
finally:
711+
cursor.close()
712+
conn.close()

handlers/export.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from openpyxl.utils import get_column_letter
1515

16-
from utils.excel_generator import generate_excel_report
16+
from utils.excel_builder import build_excel
1717
import database
1818
from config import MONTH_NAMES
1919

@@ -100,7 +100,7 @@ def export_excel(message):
100100

101101
# ── Buat file Excel ────────────────────────────────────────────
102102
try:
103-
wb = generate_excel_report(transactions, first_name, period_label, include_user_info=False)
103+
wb = build_excel(transactions, first_name, period_label, include_user_info=False)
104104
buf = io.BytesIO()
105105
wb.save(buf)
106106
buf.seek(0)

handlers/nlp_message.py

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import database
22
from ai_brain import get_json_data_from_text
3+
import time
4+
5+
# Dictionary untuk menyimpan timestamp pemrosesan AI terakhir per user (Rate Limiting)
6+
last_ai_calls = {}
7+
RATE_LIMIT_COOLDOWN = 10 # detik
38

49
def register_handlers(bot):
510
# Gunakan lambda yang menangkap semua teks, tapi pastikan bukan command
@@ -20,44 +25,85 @@ def handle_text(message):
2025
# Show typing status while processing
2126
bot.send_chat_action(message.chat.id, 'typing')
2227

28+
# --- RATE LIMITING CHECK ---
29+
now = time.time()
30+
if user_id in last_ai_calls:
31+
elapsed = now - last_ai_calls[user_id]
32+
if elapsed < RATE_LIMIT_COOLDOWN:
33+
remaining = int(RATE_LIMIT_COOLDOWN - elapsed)
34+
bot.reply_to(message, f"⚠️ *Terlalu cepat!* Mohon tunggu {remaining} detik lagi sebelum mencatat transaksi baru... 🙏", parse_mode='Markdown')
35+
return
36+
37+
# Update timestamp pemrosesan terakhir
38+
last_ai_calls[user_id] = now
39+
2340
# Process text with AI
24-
data = get_json_data_from_text(text)
41+
results = get_json_data_from_text(text)
2542

26-
if data.get("error"):
43+
# Jika hasil bukan list (misal {"error": true}), handle error
44+
if isinstance(results, dict) and results.get("error"):
2745
bot.reply_to(message, "❌ Maaf, saya tidak menangkap adanya transaksi keuangan. Pastikan formatnya jelas (contoh: 'beli makan 20rb' atau 'gajian 4 juta').")
2846
return
2947

30-
tipe = data.get("tipe", "").lower()
31-
item = data.get("item", "")
32-
nominal = float(data.get("nominal", 0))
33-
kategori = data.get("kategori", "")
48+
success_count = 0
49+
summary_lines = []
3450

35-
# Save to database based on type
3651
try:
37-
# Sekarang hanya fokus pada pemasukan, pengeluaran, dan investasi
38-
if tipe in ["pemasukan", "pengeluaran", "investasi"]:
39-
last_id = database.insert_transaction(user_id, tipe, item, nominal, kategori)
52+
for data in results:
53+
tipe = data.get("tipe", "").lower()
54+
item = data.get("item", "")
55+
nominal = float(data.get("nominal", 0))
56+
kategori = data.get("kategori", "")
4057

41-
if tipe == "pemasukan":
42-
icon = "🟢"
43-
elif tipe == "investasi":
44-
icon = "🔵"
45-
else:
46-
icon = "🔴"
47-
48-
reply = (
49-
f"{icon} *Berhasil dicatat!*\n\n"
50-
f"🔹 *Tipe:* {tipe.capitalize()}\n"
51-
f"🔹 *Item:* {item}\n"
52-
f"🔹 *Nominal:* Rp {nominal:,.0f}\n"
53-
f"🔹 *Kategori:* {kategori.replace('_', ' ').capitalize()}\n\n"
54-
f"🆔 *ID:* `T-{last_id}`"
55-
)
58+
# Save to database based on type
59+
try:
60+
if tipe == "saldo":
61+
# Reconcile balance
62+
current_balance = database.get_user_balance(user_id)
63+
diff = nominal - current_balance
64+
65+
if diff == 0:
66+
summary_lines.append(f"💰 *Saldo:* Sudah sesuai di angka Rp {nominal:,.0f}")
67+
success_count += 1
68+
continue
69+
70+
# Determine if reconciliation is income or expense
71+
rec_tipe = "pemasukan" if diff > 0 else "pengeluaran"
72+
rec_nominal = abs(diff)
73+
rec_item = "Penyesuaian Saldo"
74+
75+
last_id = database.insert_transaction(user_id, rec_tipe, rec_item, rec_nominal, "lainnya")
76+
77+
icon = "💰"
78+
summary_lines.append(
79+
f"{icon} *Saldo disesuaikan:* Rp {current_balance:,.0f} ➔ Rp {nominal:,.0f} (`T-{last_id}`)"
80+
)
81+
success_count += 1
82+
83+
elif tipe in ["pemasukan", "pengeluaran", "investasi"]:
84+
last_id = database.insert_transaction(user_id, tipe, item, nominal, kategori)
85+
86+
if tipe == "pemasukan":
87+
icon = "🟢"
88+
elif tipe == "investasi":
89+
icon = "🔵"
90+
else:
91+
icon = "🔴"
92+
93+
summary_lines.append(
94+
f"{icon} *{tipe.capitalize()}:* {item} — Rp {nominal:,.0f} (`T-{last_id}`)"
95+
)
96+
success_count += 1
97+
except Exception as e:
98+
print(f"Error inserting transaction: {e}")
99+
100+
if success_count > 0:
101+
reply = "✅ *Berhasil mencatat " + (f"{success_count} transaksi" if success_count > 1 else "transaksi") + ":*\n\n"
102+
reply += "\n".join(summary_lines)
56103
bot.reply_to(message, reply, parse_mode='Markdown')
57-
58104
else:
59-
bot.reply_to(message, "❌ Tipe data tidak dikenali dari hasil AI. Pastikan AI mengembalikan format yang benar.")
60-
105+
bot.reply_to(message, "❌ Gagal mencatat transaksi. Pastikan format pesan sudah benar.")
106+
61107
except Exception as e:
62108
print(f"Error in handle_text: {e}")
63109
bot.reply_to(message, "❌ Terjadi kesalahan sistem saat memproses permintaan Anda. Silakan coba lagi nanti.")

issue_body.tmp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## 🔴 Prioritas: High | Kategori: Bug
2+
3+
**File:** `database.py`, baris 85-86
4+
5+
## Masalah
6+
Blok migration menggunakan `except: pass` tanpa logging sama sekali. Ini menyembunyikan error nyata selain "kolom sudah ada".
7+
8+
## Kode Bermasalah
9+
```python
10+
try:
11+
cursor.execute("ALTER TABLE users ADD COLUMN has_accepted_disclaimer TINYINT(1) DEFAULT 0")
12+
conn.commit()
13+
except:
14+
pass # Kolom sudah ada
15+
```
16+
17+
## Solusi
18+
Tangkap exception spesifik MySQL error 1060 (Duplicate column):
19+
20+
```python
21+
import mysql.connector
22+
23+
try:
24+
cursor.execute("ALTER TABLE users ADD COLUMN has_accepted_disclaimer TINYINT(1) DEFAULT 0")
25+
conn.commit()
26+
except mysql.connector.errors.DatabaseError as e:
27+
if e.errno == 1060: # Duplicate column name — normal, abaikan
28+
pass
29+
else:
30+
print(f"[database] Migration error: {e}")
31+
raise # Re-raise error yang tidak dikenal
32+
```
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
from openpyxl.utils import get_column_letter
44
from datetime import datetime
55

6-
def generate_excel_report(transactions, user_label, period_label, include_user_info=False):
6+
def build_excel(transactions, user_label, period_label, include_user_info=False):
77
"""
8-
Unified Excel generator for transaction reports.
8+
Unified Excel builder for transaction reports.
99
1010
Args:
1111
transactions (list): List of transaction dictionaries.

0 commit comments

Comments
 (0)