David Prince commited on
Commit
d077e09
·
1 Parent(s): 0638f8d

Add KYC, PIN, airtime, data, purity memory

Browse files
app.py CHANGED
@@ -1,7 +1,10 @@
1
  import os
2
  from flask import Flask, jsonify
3
  from database import init_db, create_tables
4
- from endpoints import register, login, transfer, external_transfer, balance, history, alerts, lookup, jwt
 
 
 
5
  from flask_cors import CORS
6
 
7
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -24,6 +27,14 @@ app.add_url_rule("/balance", "balance", balance, methods=["GET"])
24
  app.add_url_rule("/history", "history", history, methods=["GET"])
25
  app.add_url_rule("/alerts", "alerts", alerts, methods=["GET"])
26
  app.add_url_rule("/lookup", "lookup", lookup, methods=["GET"])
 
 
 
 
 
 
 
 
27
 
28
  @app.route("/health")
29
  def health():
 
1
  import os
2
  from flask import Flask, jsonify
3
  from database import init_db, create_tables
4
+ from endpoints import (register, login, transfer, external_transfer,
5
+ balance, history, alerts, lookup, set_pin, verify_pin,
6
+ kyc_upgrade, profile, buy_airtime, buy_data,
7
+ purity_memory, get_purity_memory, jwt)
8
  from flask_cors import CORS
9
 
10
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
27
  app.add_url_rule("/history", "history", history, methods=["GET"])
28
  app.add_url_rule("/alerts", "alerts", alerts, methods=["GET"])
29
  app.add_url_rule("/lookup", "lookup", lookup, methods=["GET"])
30
+ app.add_url_rule("/set-pin", "set_pin", set_pin, methods=["POST"])
31
+ app.add_url_rule("/verify-pin", "verify_pin", verify_pin, methods=["POST"])
32
+ app.add_url_rule("/kyc-upgrade", "kyc_upgrade", kyc_upgrade, methods=["POST"])
33
+ app.add_url_rule("/profile", "profile", profile, methods=["GET"])
34
+ app.add_url_rule("/buy-airtime", "buy_airtime", buy_airtime, methods=["POST"])
35
+ app.add_url_rule("/buy-data", "buy_data", buy_data, methods=["POST"])
36
+ app.add_url_rule("/purity-memory", "purity_memory", purity_memory, methods=["POST"])
37
+ app.add_url_rule("/purity-memory", "get_purity_memory", get_purity_memory, methods=["GET"])
38
 
39
  @app.route("/health")
40
  def health():
dolor3v-frontend/src/components/PurityAI.css CHANGED
@@ -95,3 +95,16 @@
95
  }
96
 
97
  .purity-hints span:hover { border-color: #7c3aed; color: #a78bfa; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
 
97
  .purity-hints span:hover { border-color: #7c3aed; color: #a78bfa; }
98
+
99
+ .purity-response {
100
+ background: #130d24;
101
+ border: 1px solid #7c3aed44;
102
+ border-radius: 10px;
103
+ padding: 10px 16px;
104
+ color: #a78bfa;
105
+ font-size: 0.85rem;
106
+ font-style: italic;
107
+ text-align: center;
108
+ max-width: 300px;
109
+ animation: slideUp 0.3s ease;
110
+ }
dolor3v-frontend/src/components/PurityAI.jsx CHANGED
@@ -1,17 +1,19 @@
1
  import { useState, useRef } from "react";
 
2
  import "./PurityAI.css";
3
 
4
  const ELEVEN_KEY = "sk_2d090a3d6c13a30f2a89c5913a30c3f83ac82ff99a46c9b8";
5
- const VOICE_ID = "EXAVITQu4vr4xnSDxMaL"; // Bella - warm female voice
6
 
7
- export default function PurityAI({ balance, onTransfer }) {
8
  const [active, setActive] = useState(false);
9
  const [status, setStatus] = useState("idle");
10
  const [transcript, setTranscript] = useState("");
11
- const recog = useRef(null);
12
 
13
  const speak = async (text) => {
14
  setStatus("speaking");
 
15
  try {
16
  const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`, {
17
  method: "POST",
@@ -25,53 +27,74 @@ export default function PurityAI({ balance, onTransfer }) {
25
  voice_settings: { stability: 0.5, similarity_boost: 0.75 },
26
  }),
27
  });
 
28
  const blob = await res.blob();
29
  const url = URL.createObjectURL(blob);
30
  const audio = new Audio(url);
31
  audio.onended = () => setStatus("idle");
32
- audio.play();
33
  } catch {
34
- // fallback to browser TTS
35
  const u = new SpeechSynthesisUtterance(text);
36
- u.rate = 0.95;
 
 
 
37
  u.onend = () => setStatus("idle");
38
  speechSynthesis.speak(u);
39
  }
40
  };
41
 
42
- const handleCommand = (text) => {
43
  const lower = text.toLowerCase();
44
- if (lower.includes("balance")) {
45
- speak(`Your current balance is ${balance?.balance?.toLocaleString("en-NG")} Naira.`);
46
- } else if (lower.includes("transfer") || lower.includes("send")) {
47
- speak("Opening transfer screen for you.");
48
- onTransfer();
49
- } else if (lower.includes("hello") || lower.includes("hi")) {
50
- speak("Hello! I'm Purity, your dolor3v banking assistant. I can check your balance or help you send money.");
 
 
 
 
 
 
51
  } else {
52
- speak("I can help you check your balance or make a transfer. Just ask me!");
53
  }
54
  };
55
 
56
  const startListening = () => {
57
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
58
- if (!SR) { speak("Voice recognition is not supported on this browser."); return; }
59
- recog.current = new SR();
60
- recog.current.lang = "en-US";
61
- recog.current.onresult = (e) => {
 
 
 
 
62
  const text = e.results[0][0].transcript;
63
  setTranscript(text);
64
  handleCommand(text);
65
  };
66
- recog.current.onerror = () => setStatus("idle");
67
- recog.current.onend = () => setStatus("idle");
68
- recog.current.start();
69
- setStatus("listening");
 
 
 
 
 
 
 
 
70
  };
71
 
72
  return (
73
  <div className="purity-wrap">
74
- <div className={`purity-orb ${status}`} onClick={active ? startListening : () => { setActive(true); speak("Hi, I'm Purity! Tap the orb and ask me anything."); }}>
75
  <div className="orb-ring" />
76
  <div className="orb-ring r2" />
77
  <div className="orb-core">
@@ -84,12 +107,18 @@ export default function PurityAI({ balance, onTransfer }) {
84
  {status === "listening" ? "Listening..." : status === "speaking" ? "Speaking..." : active ? "Tap to speak" : "Tap to wake up"}
85
  </p>
86
 
87
- {transcript && <div className="purity-transcript">"{transcript}"</div>}
 
 
 
 
 
88
 
89
  <div className="purity-hints">
90
- <span onClick={() => handleCommand("check my balance")}>Check balance</span>
91
- <span onClick={() => handleCommand("make a transfer")}>Send money</span>
92
- <span onClick={() => handleCommand("hello")}>Say hi</span>
 
93
  </div>
94
  </div>
95
  );
 
1
  import { useState, useRef } from "react";
2
+ import API from "../api";
3
  import "./PurityAI.css";
4
 
5
  const ELEVEN_KEY = "sk_2d090a3d6c13a30f2a89c5913a30c3f83ac82ff99a46c9b8";
6
+ const VOICE_ID = "EXAVITQu4vr4xnSDxMaL";
7
 
8
+ export default function PurityAI({ balance, onTransfer, onExternalTransfer }) {
9
  const [active, setActive] = useState(false);
10
  const [status, setStatus] = useState("idle");
11
  const [transcript, setTranscript] = useState("");
12
+ const [response, setResponse] = useState("");
13
 
14
  const speak = async (text) => {
15
  setStatus("speaking");
16
+ setResponse(text);
17
  try {
18
  const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`, {
19
  method: "POST",
 
27
  voice_settings: { stability: 0.5, similarity_boost: 0.75 },
28
  }),
29
  });
30
+ if (!res.ok) throw new Error("ElevenLabs failed");
31
  const blob = await res.blob();
32
  const url = URL.createObjectURL(blob);
33
  const audio = new Audio(url);
34
  audio.onended = () => setStatus("idle");
35
+ await audio.play();
36
  } catch {
 
37
  const u = new SpeechSynthesisUtterance(text);
38
+ u.rate = 0.9; u.pitch = 1.1;
39
+ const voices = speechSynthesis.getVoices();
40
+ const female = voices.find(v => v.name.includes("Female") || v.name.includes("Samantha") || v.name.includes("Google UK English Female"));
41
+ if (female) u.voice = female;
42
  u.onend = () => setStatus("idle");
43
  speechSynthesis.speak(u);
44
  }
45
  };
46
 
47
+ const handleCommand = async (text) => {
48
  const lower = text.toLowerCase();
49
+ if (lower.includes("balance") || lower.includes("how much")) {
50
+ const bal = balance?.balance?.toLocaleString("en-NG") ?? "unavailable";
51
+ speak(`Your available balance is ${bal} Naira.`);
52
+ } else if (lower.includes("send") || lower.includes("transfer") || lower.includes("pay")) {
53
+ speak("Opening the transfer screen for you right now.");
54
+ setTimeout(() => onTransfer && onTransfer(), 1500);
55
+ } else if (lower.includes("bank") || lower.includes("external") || lower.includes("moniepoint")) {
56
+ speak("Opening external bank transfer.");
57
+ setTimeout(() => onExternalTransfer && onExternalTransfer(), 1500);
58
+ } else if (lower.includes("hello") || lower.includes("hi") || lower.includes("hey")) {
59
+ speak(`Hello! I'm Purity, your dolor3v assistant. I can check your balance, help you send money, or transfer to other banks. What would you like to do?`);
60
+ } else if (lower.includes("account") || lower.includes("number")) {
61
+ speak(`Your account number is ${balance?.account_number ?? "unavailable"}.`);
62
  } else {
63
+ speak("I can help you check your balance, send money, or make a bank transfer. Just tell me what you need!");
64
  }
65
  };
66
 
67
  const startListening = () => {
68
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
69
+ if (!SR) {
70
+ speak("Voice recognition isn't available on this browser. Try Chrome.");
71
+ return;
72
+ }
73
+ const r = new SR();
74
+ r.lang = "en-US";
75
+ r.onstart = () => setStatus("listening");
76
+ r.onresult = (e) => {
77
  const text = e.results[0][0].transcript;
78
  setTranscript(text);
79
  handleCommand(text);
80
  };
81
+ r.onerror = () => { setStatus("idle"); speak("I didn't catch that. Please try again."); };
82
+ r.onend = () => { if (status === "listening") setStatus("idle"); };
83
+ r.start();
84
+ };
85
+
86
+ const handleTap = () => {
87
+ if (!active) {
88
+ setActive(true);
89
+ speak("Hi! I'm Purity, your dolor3v banking assistant. How can I help you today?");
90
+ } else {
91
+ startListening();
92
+ }
93
  };
94
 
95
  return (
96
  <div className="purity-wrap">
97
+ <div className={`purity-orb ${status}`} onClick={handleTap}>
98
  <div className="orb-ring" />
99
  <div className="orb-ring r2" />
100
  <div className="orb-core">
 
107
  {status === "listening" ? "Listening..." : status === "speaking" ? "Speaking..." : active ? "Tap to speak" : "Tap to wake up"}
108
  </p>
109
 
110
+ {transcript && (
111
+ <div className="purity-transcript">You: "{transcript}"</div>
112
+ )}
113
+ {response && (
114
+ <div className="purity-response">Purity: "{response}"</div>
115
+ )}
116
 
117
  <div className="purity-hints">
118
+ <span onClick={() => handleCommand("check my balance")}>💰 Balance</span>
119
+ <span onClick={() => handleCommand("send money")}> Send</span>
120
+ <span onClick={() => handleCommand("transfer to bank")}>🏦 Bank</span>
121
+ <span onClick={() => handleCommand("hello")}>👋 Hi</span>
122
  </div>
123
  </div>
124
  );
dolor3v-frontend/src/pages/Dashboard.jsx CHANGED
@@ -3,6 +3,8 @@ import API from "../api";
3
  import { AuthContext } from "../auth/AuthContext";
4
  import { useNavigate } from "react-router-dom";
5
  import PurityAI from "../components/PurityAI";
 
 
6
  import "./Dashboard.css";
7
 
8
  export default function Dashboard() {
@@ -10,11 +12,7 @@ export default function Dashboard() {
10
  const [history, setHistory] = useState([]);
11
  const [alerts, setAlerts] = useState([]);
12
  const [tab, setTab] = useState("home");
13
- const [transfer, setTransfer] = useState({ recipient_account_number: "", amount: "" });
14
- const [extTransfer, setExtTransfer] = useState({ bank_name: "", recipient_name: "", recipient_account: "", amount: "" });
15
- const [msg, setMsg] = useState("");
16
- const [loading, setLoading] = useState(false);
17
- const { logout, accountNumber } = useContext(AuthContext);
18
  const nav = useNavigate();
19
 
20
  useEffect(() => { load(); }, []);
@@ -32,37 +30,11 @@ export default function Dashboard() {
32
  } catch { nav("/"); }
33
  };
34
 
35
- const doTransfer = async () => {
36
- setLoading(true); setMsg("");
37
- try {
38
- const res = await API.post("/transfer", { ...transfer, amount: parseFloat(transfer.amount) });
39
- setMsg(`✅ ${res.data.message} — Ref: ${res.data.reference}`);
40
- load();
41
- } catch (e) {
42
- setMsg(`❌ ${e.response?.data?.message || "Transfer failed"}`);
43
- }
44
- setLoading(false);
45
- };
46
-
47
- const doExtTransfer = async () => {
48
- setLoading(true); setMsg("");
49
- try {
50
- const res = await API.post("/external-transfer", { ...extTransfer, amount: parseFloat(extTransfer.amount) });
51
- setMsg(`✅ ${res.data.message} — Ref: ${res.data.reference}`);
52
- load();
53
- } catch (e) {
54
- setMsg(`❌ ${e.response?.data?.message || "Transfer failed"}`);
55
- }
56
- setLoading(false);
57
- };
58
-
59
- const handleLogout = () => { logout(); nav("/"); };
60
-
61
  return (
62
  <div className="db-bg">
63
  <div className="db-header">
64
  <div className="db-logo">dolor<span>3v</span></div>
65
- <button className="db-logout" onClick={handleLogout}>Logout</button>
66
  </div>
67
 
68
  {tab === "home" && (
@@ -86,26 +58,33 @@ export default function Dashboard() {
86
 
87
  <div className="quick-actions">
88
  <button onClick={() => setTab("transfer")} className="qa-btn">
89
- <span>↑</span> Send
90
  </button>
91
  <button onClick={() => setTab("external")} className="qa-btn">
92
- <span>🏦</span> Bank
93
  </button>
94
  <button onClick={() => setTab("history")} className="qa-btn">
95
- <span>≡</span> History
96
  </button>
97
  <button onClick={() => setTab("purity")} className="qa-btn purity-btn">
98
- <span>🎙</span> Purity
99
  </button>
100
  </div>
101
 
102
  <h3 className="section-title">Recent Transactions</h3>
 
 
 
 
 
103
  {history.slice(0, 5).map(t => (
104
  <div key={t.id} className={`txn-item ${t.type}`}>
105
  <div className="txn-left">
106
  <span className="txn-icon">{t.type === "sent" ? "↑" : "↓"}</span>
107
  <div>
108
- <p className="txn-ref">Ref: {t.reference}</p>
 
 
109
  <p className="txn-status">{t.status} • {new Date(t.timestamp).toLocaleDateString()}</p>
110
  </div>
111
  </div>
@@ -116,60 +95,36 @@ export default function Dashboard() {
116
  )}
117
 
118
  {tab === "transfer" && (
119
- <div className="db-content">
120
- <button className="back-btn" onClick={() => { setTab("home"); setMsg(""); }}>← Back</button>
121
- <h2 className="page-title">Send Money</h2>
122
- {msg && <div className={`result-msg ${msg.startsWith("✅") ? "ok" : "err"}`}>{msg}</div>}
123
- <div className="form-group">
124
- <label>Recipient Account</label>
125
- <input placeholder="ACC1234567890" value={transfer.recipient_account_number}
126
- onChange={e => setTransfer({ ...transfer, recipient_account_number: e.target.value })} />
127
- </div>
128
- <div className="form-group">
129
- <label>Amount (₦)</label>
130
- <input type="number" placeholder="0.00" value={transfer.amount}
131
- onChange={e => setTransfer({ ...transfer, amount: e.target.value })} />
132
- </div>
133
- <button className="submit-btn" onClick={doTransfer} disabled={loading}>
134
- {loading ? <span className="spinner" /> : "Send Money"}
135
- </button>
136
- </div>
137
  )}
138
 
139
  {tab === "external" && (
140
- <div className="db-content">
141
- <button className="back-btn" onClick={() => { setTab("home"); setMsg(""); }}>← Back</button>
142
- <h2 className="page-title">External Bank Transfer</h2>
143
- {msg && <div className={`result-msg ${msg.startsWith("✅") ? "ok" : "err"}`}>{msg}</div>}
144
- {[
145
- { key: "bank_name", label: "Bank Name", ph: "GTBank" },
146
- { key: "recipient_name", label: "Recipient Name", ph: "John Doe" },
147
- { key: "recipient_account", label: "Account Number", ph: "0123456789" },
148
- { key: "amount", label: "Amount (₦)", ph: "0.00", type: "number" },
149
- ].map(f => (
150
- <div className="form-group" key={f.key}>
151
- <label>{f.label}</label>
152
- <input type={f.type || "text"} placeholder={f.ph}
153
- value={extTransfer[f.key]}
154
- onChange={e => setExtTransfer({ ...extTransfer, [f.key]: e.target.value })} />
155
- </div>
156
- ))}
157
- <button className="submit-btn" onClick={doExtTransfer} disabled={loading}>
158
- {loading ? <span className="spinner" /> : "Transfer"}
159
- </button>
160
- </div>
161
  )}
162
 
163
  {tab === "history" && (
164
  <div className="db-content">
165
  <button className="back-btn" onClick={() => setTab("home")}>← Back</button>
166
  <h2 className="page-title">Transaction History</h2>
 
 
 
167
  {history.map(t => (
168
  <div key={t.id} className={`txn-item ${t.type}`}>
169
  <div className="txn-left">
170
  <span className="txn-icon">{t.type === "sent" ? "↑" : "↓"}</span>
171
  <div>
172
- <p className="txn-ref">{t.is_external ? `🏦 ${t.recipient}` : `Ref: ${t.reference}`}</p>
 
 
 
 
173
  <p className="txn-status">{t.status} • {new Date(t.timestamp).toLocaleDateString()}</p>
174
  <p className="txn-ai">{t.ai_review}</p>
175
  </div>
@@ -183,7 +138,11 @@ export default function Dashboard() {
183
  {tab === "purity" && (
184
  <div className="db-content">
185
  <button className="back-btn" onClick={() => setTab("home")}>← Back</button>
186
- <PurityAI balance={balance} onTransfer={() => setTab("transfer")} />
 
 
 
 
187
  </div>
188
  )}
189
  </div>
 
3
  import { AuthContext } from "../auth/AuthContext";
4
  import { useNavigate } from "react-router-dom";
5
  import PurityAI from "../components/PurityAI";
6
+ import Transfer from "./Transfer";
7
+ import ExternalTransfer from "./ExternalTransfer";
8
  import "./Dashboard.css";
9
 
10
  export default function Dashboard() {
 
12
  const [history, setHistory] = useState([]);
13
  const [alerts, setAlerts] = useState([]);
14
  const [tab, setTab] = useState("home");
15
+ const { logout } = useContext(AuthContext);
 
 
 
 
16
  const nav = useNavigate();
17
 
18
  useEffect(() => { load(); }, []);
 
30
  } catch { nav("/"); }
31
  };
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return (
34
  <div className="db-bg">
35
  <div className="db-header">
36
  <div className="db-logo">dolor<span>3v</span></div>
37
+ <button className="db-logout" onClick={() => { logout(); nav("/"); }}>Logout</button>
38
  </div>
39
 
40
  {tab === "home" && (
 
58
 
59
  <div className="quick-actions">
60
  <button onClick={() => setTab("transfer")} className="qa-btn">
61
+ <span>↑</span>Send
62
  </button>
63
  <button onClick={() => setTab("external")} className="qa-btn">
64
+ <span>🏦</span>Banks
65
  </button>
66
  <button onClick={() => setTab("history")} className="qa-btn">
67
+ <span>≡</span>History
68
  </button>
69
  <button onClick={() => setTab("purity")} className="qa-btn purity-btn">
70
+ <span></span>Purity
71
  </button>
72
  </div>
73
 
74
  <h3 className="section-title">Recent Transactions</h3>
75
+ {history.length === 0 && (
76
+ <p style={{ color: "#334155", fontSize: "0.85rem", textAlign: "center", padding: "20px 0" }}>
77
+ No transactions yet
78
+ </p>
79
+ )}
80
  {history.slice(0, 5).map(t => (
81
  <div key={t.id} className={`txn-item ${t.type}`}>
82
  <div className="txn-left">
83
  <span className="txn-icon">{t.type === "sent" ? "↑" : "↓"}</span>
84
  <div>
85
+ <p className="txn-ref">
86
+ {t.is_external ? `🏦 ${t.recipient.replace("EXT:", "").split(":")[0]}` : `To: ${t.recipient}`}
87
+ </p>
88
  <p className="txn-status">{t.status} • {new Date(t.timestamp).toLocaleDateString()}</p>
89
  </div>
90
  </div>
 
95
  )}
96
 
97
  {tab === "transfer" && (
98
+ <Transfer
99
+ onBack={() => setTab("home")}
100
+ onDone={() => { setTab("home"); load(); }}
101
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  )}
103
 
104
  {tab === "external" && (
105
+ <ExternalTransfer
106
+ onBack={() => setTab("home")}
107
+ onDone={() => { setTab("home"); load(); }}
108
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  )}
110
 
111
  {tab === "history" && (
112
  <div className="db-content">
113
  <button className="back-btn" onClick={() => setTab("home")}>← Back</button>
114
  <h2 className="page-title">Transaction History</h2>
115
+ {history.length === 0 && (
116
+ <p style={{ color: "#334155", textAlign: "center", padding: "40px 0" }}>No transactions yet</p>
117
+ )}
118
  {history.map(t => (
119
  <div key={t.id} className={`txn-item ${t.type}`}>
120
  <div className="txn-left">
121
  <span className="txn-icon">{t.type === "sent" ? "↑" : "↓"}</span>
122
  <div>
123
+ <p className="txn-ref">
124
+ {t.is_external
125
+ ? `🏦 ${t.recipient.replace("EXT:", "").split(":").slice(0,2).join(" • ")}`
126
+ : `Ref: ${t.reference}`}
127
+ </p>
128
  <p className="txn-status">{t.status} • {new Date(t.timestamp).toLocaleDateString()}</p>
129
  <p className="txn-ai">{t.ai_review}</p>
130
  </div>
 
138
  {tab === "purity" && (
139
  <div className="db-content">
140
  <button className="back-btn" onClick={() => setTab("home")}>← Back</button>
141
+ <PurityAI
142
+ balance={balance}
143
+ onTransfer={() => setTab("transfer")}
144
+ onExternalTransfer={() => setTab("external")}
145
+ />
146
  </div>
147
  )}
148
  </div>
endpoints.py CHANGED
@@ -1,13 +1,20 @@
1
  from flask import request, jsonify
2
- from models import db, User, Transaction, Alert
3
  from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
4
  from werkzeug.security import generate_password_hash, check_password_hash
5
- from datetime import datetime
6
  from ai_review import ai_review_transaction
 
7
  import random, string, uuid
8
 
9
  jwt = JWTManager()
10
 
 
 
 
 
 
 
 
11
  def generate_account_number():
12
  while True:
13
  number = "ACC" + "".join(random.choices(string.digits, k=10))
@@ -15,8 +22,7 @@ def generate_account_number():
15
  return number
16
 
17
  def create_alert(account_number, message):
18
- alert = Alert(account_number=account_number, message=message)
19
- db.session.add(alert)
20
 
21
  def register():
22
  data = request.get_json()
@@ -27,10 +33,11 @@ def register():
27
  email=data["email"],
28
  password=generate_password_hash(data["password"]),
29
  account_number=generate_account_number(),
 
30
  )
31
  db.session.add(user)
32
  db.session.commit()
33
- create_alert(user.account_number, f"Welcome to dolor3v, {user.name}! Your account {user.account_number} is ready.")
34
  db.session.commit()
35
  return jsonify({"message": "User registered successfully", "account_number": user.account_number})
36
 
@@ -40,19 +47,110 @@ def login():
40
  if not user or not check_password_hash(user.password, data["password"]):
41
  return jsonify({"message": "Invalid credentials"}), 401
42
  access_token = create_access_token(identity=user.account_number)
43
- return jsonify({"access_token": access_token, "account_number": user.account_number})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  @jwt_required()
46
  def transfer():
47
  data = request.get_json()
48
  sender_acc = get_jwt_identity()
49
  sender = User.query.filter_by(account_number=sender_acc).first()
 
 
 
 
 
 
 
 
50
  recipient = User.query.filter_by(account_number=data["recipient_account_number"]).first()
51
  if not sender or not recipient:
52
  return jsonify({"message": "Invalid account numbers"}), 404
53
  if sender.account_number == recipient.account_number:
54
  return jsonify({"message": "Cannot transfer to same account"}), 400
 
55
  amount = float(data["amount"])
 
 
 
56
  if sender.balance < amount:
57
  return jsonify({"message": "Insufficient funds"}), 400
58
 
@@ -66,15 +164,12 @@ def transfer():
66
  t = Transaction(
67
  sender_account_number=sender.account_number,
68
  recipient_account_number=recipient.account_number,
69
- amount=amount,
70
- status=status,
71
- is_external=False,
72
- reference=str(uuid.uuid4())[:8].upper(),
73
- ai_review=reason,
74
  )
75
  db.session.add(t)
76
- create_alert(sender.account_number, f"Transfer of {amount} to {recipient.account_number} — {status}. Ref: {t.reference}")
77
- create_alert(recipient.account_number, f"You received {amount} from {sender.account_number}. Ref: {t.reference}")
78
  db.session.commit()
79
  return jsonify({"message": f"Transfer {status}", "reference": t.reference, "ai_review": reason})
80
 
@@ -83,15 +178,23 @@ def external_transfer():
83
  data = request.get_json()
84
  sender_acc = get_jwt_identity()
85
  sender = User.query.filter_by(account_number=sender_acc).first()
86
- if not sender:
87
- return jsonify({"message": "User not found"}), 404
 
 
 
 
 
 
88
  amount = float(data["amount"])
 
 
 
89
  if sender.balance < amount:
90
  return jsonify({"message": "Insufficient funds"}), 400
91
 
92
  bank_name = data.get("bank_name", "External Bank")
93
  recipient_name = data.get("recipient_name", "Unknown")
94
-
95
  decision, reason = ai_review_transaction(sender, recipient_name, amount, True)
96
  status = "pending" if decision == "hold" else "processing"
97
 
@@ -102,44 +205,102 @@ def external_transfer():
102
  t = Transaction(
103
  sender_account_number=sender.account_number,
104
  recipient_account_number=f"EXT:{bank_name}:{data.get('recipient_account')}",
105
- amount=amount,
106
- status=status,
107
- is_external=True,
108
- reference=ref,
109
- ai_review=reason,
110
  )
111
  db.session.add(t)
112
- create_alert(sender.account_number, f"External transfer of {amount} to {recipient_name} at {bank_name} — {status}. Ref: {ref}")
113
  db.session.commit()
114
- return jsonify({"message": f"External transfer {status}", "reference": ref, "ai_review": reason})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  @jwt_required()
117
  def balance():
118
- account_number = get_jwt_identity()
119
- user = User.query.filter_by(account_number=account_number).first()
120
  if not user:
121
  return jsonify({"message": "Invalid account number"}), 404
122
- return jsonify({"balance": user.balance, "account_number": user.account_number})
123
 
124
  @jwt_required()
125
  def history():
126
- account_number = get_jwt_identity()
127
  txns = Transaction.query.filter(
128
- (Transaction.sender_account_number == account_number) |
129
- (Transaction.recipient_account_number == account_number)
130
  ).order_by(Transaction.timestamp.desc()).all()
131
-
132
  result = []
133
  for t in txns:
134
  result.append({
135
  "id": t.id,
136
- "type": "sent" if t.sender_account_number == account_number else "received",
137
  "amount": t.amount,
138
  "sender": t.sender_account_number,
139
  "recipient": t.recipient_account_number,
140
  "status": t.status,
141
  "reference": t.reference,
142
  "is_external": t.is_external,
 
143
  "ai_review": t.ai_review,
144
  "timestamp": t.timestamp.isoformat(),
145
  })
@@ -147,9 +308,8 @@ def history():
147
 
148
  @jwt_required()
149
  def alerts():
150
- account_number = get_jwt_identity()
151
- unread = Alert.query.filter_by(account_number=account_number, read=False)\
152
- .order_by(Alert.timestamp.desc()).all()
153
  result = [{"id": a.id, "message": a.message, "timestamp": a.timestamp.isoformat()} for a in unread]
154
  for a in unread:
155
  a.read = True
@@ -158,8 +318,23 @@ def alerts():
158
 
159
  @jwt_required()
160
  def lookup():
161
- account_number = request.args.get("account_number")
162
- user = User.query.filter_by(account_number=account_number).first()
163
  if not user:
164
  return jsonify({"message": "Account not found"}), 404
165
  return jsonify({"name": user.name, "account_number": user.account_number, "bank": "dolor3v"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from flask import request, jsonify
2
+ from models import db, User, Transaction, Alert, PurityMemory
3
  from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
4
  from werkzeug.security import generate_password_hash, check_password_hash
 
5
  from ai_review import ai_review_transaction
6
+ from datetime import datetime
7
  import random, string, uuid
8
 
9
  jwt = JWTManager()
10
 
11
+ TIER_LIMITS = {
12
+ 0: {"daily": 0, "max_balance": 0, "label": "Unverified"},
13
+ 1: {"daily": 50000, "max_balance": 300000, "label": "Tier 1 - NIN"},
14
+ 2: {"daily": 200000, "max_balance": 500000, "label": "Tier 2 - NIN+BVN"},
15
+ 3: {"daily": 5000000, "max_balance": float('inf'), "label": "Tier 3 - Full KYC"},
16
+ }
17
+
18
  def generate_account_number():
19
  while True:
20
  number = "ACC" + "".join(random.choices(string.digits, k=10))
 
22
  return number
23
 
24
  def create_alert(account_number, message):
25
+ db.session.add(Alert(account_number=account_number, message=message))
 
26
 
27
  def register():
28
  data = request.get_json()
 
33
  email=data["email"],
34
  password=generate_password_hash(data["password"]),
35
  account_number=generate_account_number(),
36
+ phone=data.get("phone", ""),
37
  )
38
  db.session.add(user)
39
  db.session.commit()
40
+ create_alert(user.account_number, f"Welcome to dolor3v, {user.name}! Complete KYC to unlock transfers.")
41
  db.session.commit()
42
  return jsonify({"message": "User registered successfully", "account_number": user.account_number})
43
 
 
47
  if not user or not check_password_hash(user.password, data["password"]):
48
  return jsonify({"message": "Invalid credentials"}), 401
49
  access_token = create_access_token(identity=user.account_number)
50
+ return jsonify({
51
+ "access_token": access_token,
52
+ "account_number": user.account_number,
53
+ "name": user.name,
54
+ "tier": user.tier,
55
+ })
56
+
57
+ @jwt_required()
58
+ def set_pin():
59
+ data = request.get_json()
60
+ acc = get_jwt_identity()
61
+ user = User.query.filter_by(account_number=acc).first()
62
+ pin = data.get("pin", "")
63
+ if len(pin) != 4 or not pin.isdigit():
64
+ return jsonify({"message": "PIN must be 4 digits"}), 400
65
+ user.pin = generate_password_hash(pin)
66
+ db.session.commit()
67
+ return jsonify({"message": "PIN set successfully"})
68
+
69
+ @jwt_required()
70
+ def verify_pin():
71
+ data = request.get_json()
72
+ acc = get_jwt_identity()
73
+ user = User.query.filter_by(account_number=acc).first()
74
+ if not user.pin:
75
+ return jsonify({"valid": False, "message": "No PIN set"}), 400
76
+ valid = check_password_hash(user.pin, data.get("pin", ""))
77
+ return jsonify({"valid": valid})
78
+
79
+ @jwt_required()
80
+ def kyc_upgrade():
81
+ acc = get_jwt_identity()
82
+ user = User.query.filter_by(account_number=acc).first()
83
+ data = request.get_json()
84
+ tier = data.get("tier", 1)
85
+
86
+ if tier == 1:
87
+ if not data.get("nin"):
88
+ return jsonify({"message": "NIN required for Tier 1"}), 400
89
+ user.nin = data["nin"]
90
+ user.tier = 1
91
+ user.daily_limit = 50000
92
+ user.max_balance = 300000
93
+ create_alert(acc, "KYC Tier 1 approved! Daily limit: ₦50,000")
94
+
95
+ elif tier == 2:
96
+ if not data.get("bvn"):
97
+ return jsonify({"message": "BVN required for Tier 2"}), 400
98
+ user.bvn = data["bvn"]
99
+ user.tier = 2
100
+ user.daily_limit = 200000
101
+ user.max_balance = 500000
102
+ create_alert(acc, "KYC Tier 2 approved! Daily limit: ₦200,000")
103
+
104
+ elif tier == 3:
105
+ user.tier = 3
106
+ user.daily_limit = 5000000
107
+ user.max_balance = float('inf')
108
+ create_alert(acc, "KYC Tier 3 approved! Daily limit: ₦5,000,000. Unlimited balance.")
109
+
110
+ db.session.commit()
111
+ return jsonify({"message": f"Tier {tier} upgrade successful", "tier": user.tier})
112
+
113
+ @jwt_required()
114
+ def profile():
115
+ acc = get_jwt_identity()
116
+ user = User.query.filter_by(account_number=acc).first()
117
+ limits = TIER_LIMITS.get(user.tier, TIER_LIMITS[0])
118
+ return jsonify({
119
+ "name": user.name,
120
+ "email": user.email,
121
+ "phone": user.phone,
122
+ "account_number": user.account_number,
123
+ "tier": user.tier,
124
+ "tier_label": limits["label"],
125
+ "daily_limit": limits["daily"],
126
+ "max_balance": limits["max_balance"] if limits["max_balance"] != float('inf') else -1,
127
+ "balance": user.balance,
128
+ "pin_set": user.pin is not None,
129
+ })
130
 
131
  @jwt_required()
132
  def transfer():
133
  data = request.get_json()
134
  sender_acc = get_jwt_identity()
135
  sender = User.query.filter_by(account_number=sender_acc).first()
136
+
137
+ if sender.tier == 0:
138
+ return jsonify({"message": "Complete KYC to make transfers"}), 403
139
+ if not sender.pin:
140
+ return jsonify({"message": "Set a PIN before making transfers"}), 403
141
+ if not check_password_hash(sender.pin, data.get("pin", "")):
142
+ return jsonify({"message": "Invalid PIN"}), 401
143
+
144
  recipient = User.query.filter_by(account_number=data["recipient_account_number"]).first()
145
  if not sender or not recipient:
146
  return jsonify({"message": "Invalid account numbers"}), 404
147
  if sender.account_number == recipient.account_number:
148
  return jsonify({"message": "Cannot transfer to same account"}), 400
149
+
150
  amount = float(data["amount"])
151
+ limit = TIER_LIMITS[sender.tier]["daily"]
152
+ if amount > limit:
153
+ return jsonify({"message": f"Amount exceeds your daily limit of ₦{limit:,.0f}"}), 400
154
  if sender.balance < amount:
155
  return jsonify({"message": "Insufficient funds"}), 400
156
 
 
164
  t = Transaction(
165
  sender_account_number=sender.account_number,
166
  recipient_account_number=recipient.account_number,
167
+ amount=amount, status=status, is_external=False,
168
+ reference=str(uuid.uuid4())[:8].upper(), ai_review=reason,
 
 
 
169
  )
170
  db.session.add(t)
171
+ create_alert(sender.account_number, f"Debit: {amount:,.0f} to {recipient.name}. Ref: {t.reference}")
172
+ create_alert(recipient.account_number, f"Credit: {amount:,.0f} from {sender.name}. Ref: {t.reference}")
173
  db.session.commit()
174
  return jsonify({"message": f"Transfer {status}", "reference": t.reference, "ai_review": reason})
175
 
 
178
  data = request.get_json()
179
  sender_acc = get_jwt_identity()
180
  sender = User.query.filter_by(account_number=sender_acc).first()
181
+
182
+ if sender.tier == 0:
183
+ return jsonify({"message": "Complete KYC to make transfers"}), 403
184
+ if not sender.pin:
185
+ return jsonify({"message": "Set a PIN before making transfers"}), 403
186
+ if not check_password_hash(sender.pin, data.get("pin", "")):
187
+ return jsonify({"message": "Invalid PIN"}), 401
188
+
189
  amount = float(data["amount"])
190
+ limit = TIER_LIMITS[sender.tier]["daily"]
191
+ if amount > limit:
192
+ return jsonify({"message": f"Exceeds daily limit of ₦{limit:,.0f}"}), 400
193
  if sender.balance < amount:
194
  return jsonify({"message": "Insufficient funds"}), 400
195
 
196
  bank_name = data.get("bank_name", "External Bank")
197
  recipient_name = data.get("recipient_name", "Unknown")
 
198
  decision, reason = ai_review_transaction(sender, recipient_name, amount, True)
199
  status = "pending" if decision == "hold" else "processing"
200
 
 
205
  t = Transaction(
206
  sender_account_number=sender.account_number,
207
  recipient_account_number=f"EXT:{bank_name}:{data.get('recipient_account')}",
208
+ amount=amount, status=status, is_external=True,
209
+ reference=ref, ai_review=reason,
 
 
 
210
  )
211
  db.session.add(t)
212
+ create_alert(sender.account_number, f"External transfer: {amount:,.0f} to {recipient_name} @ {bank_name}. Ref: {ref}")
213
  db.session.commit()
214
+ return jsonify({"message": f"Transfer {status}", "reference": ref, "ai_review": reason})
215
+
216
+ @jwt_required()
217
+ def buy_airtime():
218
+ data = request.get_json()
219
+ acc = get_jwt_identity()
220
+ user = User.query.filter_by(account_number=acc).first()
221
+
222
+ if not user.pin or not check_password_hash(user.pin, data.get("pin", "")):
223
+ return jsonify({"message": "Invalid PIN"}), 401
224
+
225
+ amount = float(data["amount"])
226
+ phone = data.get("phone")
227
+ network = data.get("network", "MTN")
228
+
229
+ if user.balance < amount:
230
+ return jsonify({"message": "Insufficient funds"}), 400
231
+
232
+ user.balance -= amount
233
+ ref = "AIR" + str(uuid.uuid4())[:6].upper()
234
+ t = Transaction(
235
+ sender_account_number=user.account_number,
236
+ recipient_account_number=f"AIRTIME:{network}:{phone}",
237
+ amount=amount, status="completed", is_external=True,
238
+ reference=ref, ai_review="Airtime purchase",
239
+ transaction_type="airtime",
240
+ )
241
+ db.session.add(t)
242
+ create_alert(user.account_number, f"Airtime: ₦{amount:,.0f} {network} sent to {phone}. Ref: {ref}")
243
+ db.session.commit()
244
+ return jsonify({"message": f"₦{amount:,.0f} {network} airtime sent to {phone}", "reference": ref})
245
+
246
+ @jwt_required()
247
+ def buy_data():
248
+ data = request.get_json()
249
+ acc = get_jwt_identity()
250
+ user = User.query.filter_by(account_number=acc).first()
251
+
252
+ if not user.pin or not check_password_hash(user.pin, data.get("pin", "")):
253
+ return jsonify({"message": "Invalid PIN"}), 401
254
+
255
+ amount = float(data["amount"])
256
+ phone = data.get("phone")
257
+ network = data.get("network", "MTN")
258
+ plan = data.get("plan", "1GB")
259
+
260
+ if user.balance < amount:
261
+ return jsonify({"message": "Insufficient funds"}), 400
262
+
263
+ user.balance -= amount
264
+ ref = "DATA" + str(uuid.uuid4())[:6].upper()
265
+ t = Transaction(
266
+ sender_account_number=user.account_number,
267
+ recipient_account_number=f"DATA:{network}:{phone}",
268
+ amount=amount, status="completed", is_external=True,
269
+ reference=ref, ai_review="Data purchase",
270
+ transaction_type="data",
271
+ )
272
+ db.session.add(t)
273
+ create_alert(user.account_number, f"Data: {plan} {network} sent to {phone}. Ref: {ref}")
274
+ db.session.commit()
275
+ return jsonify({"message": f"{plan} {network} data sent to {phone}", "reference": ref})
276
 
277
  @jwt_required()
278
  def balance():
279
+ acc = get_jwt_identity()
280
+ user = User.query.filter_by(account_number=acc).first()
281
  if not user:
282
  return jsonify({"message": "Invalid account number"}), 404
283
+ return jsonify({"balance": user.balance, "account_number": user.account_number, "name": user.name})
284
 
285
  @jwt_required()
286
  def history():
287
+ acc = get_jwt_identity()
288
  txns = Transaction.query.filter(
289
+ (Transaction.sender_account_number == acc) |
290
+ (Transaction.recipient_account_number == acc)
291
  ).order_by(Transaction.timestamp.desc()).all()
 
292
  result = []
293
  for t in txns:
294
  result.append({
295
  "id": t.id,
296
+ "type": "sent" if t.sender_account_number == acc else "received",
297
  "amount": t.amount,
298
  "sender": t.sender_account_number,
299
  "recipient": t.recipient_account_number,
300
  "status": t.status,
301
  "reference": t.reference,
302
  "is_external": t.is_external,
303
+ "transaction_type": t.transaction_type,
304
  "ai_review": t.ai_review,
305
  "timestamp": t.timestamp.isoformat(),
306
  })
 
308
 
309
  @jwt_required()
310
  def alerts():
311
+ acc = get_jwt_identity()
312
+ unread = Alert.query.filter_by(account_number=acc, read=False).order_by(Alert.timestamp.desc()).all()
 
313
  result = [{"id": a.id, "message": a.message, "timestamp": a.timestamp.isoformat()} for a in unread]
314
  for a in unread:
315
  a.read = True
 
318
 
319
  @jwt_required()
320
  def lookup():
321
+ acc_num = request.args.get("account_number")
322
+ user = User.query.filter_by(account_number=acc_num).first()
323
  if not user:
324
  return jsonify({"message": "Account not found"}), 404
325
  return jsonify({"name": user.name, "account_number": user.account_number, "bank": "dolor3v"})
326
+
327
+ @jwt_required()
328
+ def purity_memory():
329
+ acc = get_jwt_identity()
330
+ data = request.get_json()
331
+ mem = PurityMemory(account_number=acc, memory=data.get("memory", ""))
332
+ db.session.add(mem)
333
+ db.session.commit()
334
+ return jsonify({"message": "Memory saved"})
335
+
336
+ @jwt_required()
337
+ def get_purity_memory():
338
+ acc = get_jwt_identity()
339
+ mems = PurityMemory.query.filter_by(account_number=acc).order_by(PurityMemory.timestamp.desc()).limit(20).all()
340
+ return jsonify({"memories": [m.memory for m in mems]})
models.py CHANGED
@@ -7,9 +7,21 @@ class User(db.Model):
7
  id = db.Column(db.Integer, primary_key=True)
8
  name = db.Column(db.String(100), nullable=False)
9
  email = db.Column(db.String(100), nullable=False, unique=True)
10
- password = db.Column(db.String(100), nullable=False)
11
  account_number = db.Column(db.String(20), nullable=False, unique=True)
12
  balance = db.Column(db.Float, nullable=False, default=0.0)
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  class Transaction(db.Model):
15
  id = db.Column(db.Integer, primary_key=True)
@@ -20,6 +32,7 @@ class Transaction(db.Model):
20
  is_external = db.Column(db.Boolean, default=False)
21
  reference = db.Column(db.String(50), nullable=True)
22
  ai_review = db.Column(db.String(500), nullable=True)
 
23
  timestamp = db.Column(db.DateTime, default=datetime.utcnow)
24
 
25
  class Alert(db.Model):
@@ -28,3 +41,9 @@ class Alert(db.Model):
28
  message = db.Column(db.String(500), nullable=False)
29
  read = db.Column(db.Boolean, default=False)
30
  timestamp = db.Column(db.DateTime, default=datetime.utcnow)
 
 
 
 
 
 
 
7
  id = db.Column(db.Integer, primary_key=True)
8
  name = db.Column(db.String(100), nullable=False)
9
  email = db.Column(db.String(100), nullable=False, unique=True)
10
+ password = db.Column(db.String(200), nullable=False)
11
  account_number = db.Column(db.String(20), nullable=False, unique=True)
12
  balance = db.Column(db.Float, nullable=False, default=0.0)
13
+ phone = db.Column(db.String(20), nullable=True)
14
+ pin = db.Column(db.String(200), nullable=True)
15
+ tier = db.Column(db.Integer, default=0)
16
+ # KYC fields
17
+ nin = db.Column(db.String(20), nullable=True)
18
+ bvn = db.Column(db.String(20), nullable=True)
19
+ id_card_url = db.Column(db.String(200), nullable=True)
20
+ nepa_bill_url = db.Column(db.String(200), nullable=True)
21
+ kyc_status = db.Column(db.String(20), default="none")
22
+ daily_limit = db.Column(db.Float, default=0.0)
23
+ max_balance = db.Column(db.Float, default=200000.0)
24
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
25
 
26
  class Transaction(db.Model):
27
  id = db.Column(db.Integer, primary_key=True)
 
32
  is_external = db.Column(db.Boolean, default=False)
33
  reference = db.Column(db.String(50), nullable=True)
34
  ai_review = db.Column(db.String(500), nullable=True)
35
+ transaction_type = db.Column(db.String(20), default="transfer")
36
  timestamp = db.Column(db.DateTime, default=datetime.utcnow)
37
 
38
  class Alert(db.Model):
 
41
  message = db.Column(db.String(500), nullable=False)
42
  read = db.Column(db.Boolean, default=False)
43
  timestamp = db.Column(db.DateTime, default=datetime.utcnow)
44
+
45
+ class PurityMemory(db.Model):
46
+ id = db.Column(db.Integer, primary_key=True)
47
+ account_number = db.Column(db.String(20), nullable=False)
48
+ memory = db.Column(db.Text, nullable=False)
49
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)