File size: 8,454 Bytes
2dddd1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const db_1 = require("./db");
const telegram_1 = require("./telegram");
require("express-async-errors");
const zod_1 = require("zod");
const xlsx = __importStar(require("xlsx"));
const app = (0, express_1.default)();
const PORT = 3001;
app.use((0, cors_1.default)());
app.use(express_1.default.json());
app.get('/api/rates', (req, res) => {
    res.json((0, telegram_1.getCurrentRates)());
});
// Initialize Telegram Client in background
(0, telegram_1.initTelegramAuth)().catch(console.error);
// Add Validation Schemas
const transactionSchema = zod_1.z.object({
    type: zod_1.z.enum(['income', 'expense', 'transfer']),
    amount: zod_1.z.number().positive(),
    currency: zod_1.z.string().min(2),
    wallet_id: zod_1.z.number().int().positive(),
    to_wallet_id: zod_1.z.number().int().positive().nullable().optional(),
    category: zod_1.z.string().optional(),
    note: zod_1.z.string().optional(),
    date: zod_1.z.string(),
    country_id: zod_1.z.string().optional()
});
const exchangeSchema = zod_1.z.object({
    from_amount: zod_1.z.number().positive(),
    from_currency: zod_1.z.string().min(2),
    to_amount: zod_1.z.number().positive(),
    to_currency: zod_1.z.string().min(2),
    rate: zod_1.z.number().positive(),
    note: zod_1.z.string().optional(),
    date: zod_1.z.string()
});
const loanSchema = zod_1.z.object({
    person: zod_1.z.string().min(1),
    type: zod_1.z.enum(['borrowed_from_me', 'owed_by_me']),
    amount: zod_1.z.number().positive(),
    currency: zod_1.z.string().min(2),
    note: zod_1.z.string().optional(),
    date: zod_1.z.string()
});
app.get('/api/wallets', async (req, res) => {
    const db = await (0, db_1.getDb)();
    const wallets = await db.all('SELECT * FROM wallets');
    res.json(wallets);
});
app.get('/api/transactions', async (req, res) => {
    const db = await (0, db_1.getDb)();
    const transactions = await db.all('SELECT * FROM transactions ORDER BY date DESC');
    res.json(transactions);
});
app.post('/api/transactions', async (req, res) => {
    const validatedData = transactionSchema.parse(req.body);
    const { type, amount, currency, wallet_id, to_wallet_id, category, note, date, country_id } = validatedData;
    const db = await (0, db_1.getDb)();
    const result = await db.run(`INSERT INTO transactions (type, amount, currency, wallet_id, to_wallet_id, category, note, date, country_id) 
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [type, amount, currency, wallet_id, to_wallet_id, category, note, date, country_id]);
    res.json({ id: result.lastID });
});
app.get('/api/exchanges', async (req, res) => {
    const db = await (0, db_1.getDb)();
    const exchanges = await db.all('SELECT * FROM exchanges ORDER BY date DESC');
    res.json(exchanges);
});
app.post('/api/exchanges', async (req, res) => {
    const validatedData = exchangeSchema.parse(req.body);
    const { from_amount, from_currency, to_amount, to_currency, rate, note, date } = validatedData;
    const db = await (0, db_1.getDb)();
    const result = await db.run(`INSERT INTO exchanges (from_amount, from_currency, to_amount, to_currency, rate, note, date) 
    VALUES (?, ?, ?, ?, ?, ?, ?)`, [from_amount, from_currency, to_amount, to_currency, rate, note, date]);
    res.json({ id: result.lastID });
});
app.get('/api/loans', async (req, res) => {
    const db = await (0, db_1.getDb)();
    const loans = await db.all('SELECT * FROM loans ORDER BY date DESC');
    res.json(loans);
});
app.post('/api/loans', async (req, res) => {
    const validatedData = loanSchema.parse(req.body);
    const { person, type, amount, currency, note, date } = validatedData;
    const db = await (0, db_1.getDb)();
    const result = await db.run(`INSERT INTO loans (person, type, amount, currency, note, date) 
    VALUES (?, ?, ?, ?, ?, ?)`, [person, type, amount, currency, note, date]);
    res.json({ id: result.lastID });
});
app.get('/api/export', async (req, res) => {
    const db = await (0, db_1.getDb)();
    const transactions = await db.all('SELECT * FROM transactions');
    const wallets = await db.all('SELECT * FROM wallets');
    const exchanges = await db.all('SELECT * FROM exchanges');
    const loans = await db.all('SELECT * FROM loans');
    const wb = xlsx.utils.book_new();
    xlsx.utils.book_append_sheet(wb, xlsx.utils.json_to_sheet(transactions), 'Transactions');
    xlsx.utils.book_append_sheet(wb, xlsx.utils.json_to_sheet(wallets), 'Wallets');
    xlsx.utils.book_append_sheet(wb, xlsx.utils.json_to_sheet(exchanges), 'Exchanges');
    xlsx.utils.book_append_sheet(wb, xlsx.utils.json_to_sheet(loans), 'Loans');
    const buf = xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
    res.set('Content-Disposition', 'attachment; filename="backup.xlsx"');
    res.type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    res.send(buf);
});
app.get('/api/analytics/dashboard', async (req, res) => {
    const db = await (0, db_1.getDb)();
    // Using SQLite to aggregate the balances
    // Since currency conversion changes, we return the raw grouped data and calculate total dynamically
    const walletBalancesRaw = await db.all(`
        SELECT 
            w.id, w.name, w.type, w.currency,
            COALESCE(SUM(CASE WHEN t.type = 'income' THEN t.amount ELSE 0 END), 0) -
            COALESCE(SUM(CASE WHEN t.type = 'expense' THEN t.amount ELSE 0 END), 0) -
            COALESCE(SUM(CASE WHEN t.type = 'transfer' AND t.wallet_id = w.id THEN t.amount ELSE 0 END), 0) +
            COALESCE(SUM(CASE WHEN t.type = 'transfer' AND t.to_wallet_id = w.id THEN t.amount ELSE 0 END), 0) as balance
        FROM wallets w
        LEFT JOIN transactions t ON t.wallet_id = w.id OR t.to_wallet_id = w.id
        GROUP BY w.id
    `);
    // Fetch exchanges to merge into currency pools later on the frontend
    const exchangesRaw = await db.all('SELECT from_amount, from_currency, to_amount, to_currency FROM exchanges');
    // Fetch loans for net worth calculation
    const loansRaw = await db.all('SELECT type, amount, paid, currency FROM loans');
    // We also need transactions for the recent activity feed
    const recentActivity = await db.all('SELECT * FROM transactions ORDER BY date DESC LIMIT 5');
    res.json({
        walletBalancesRaw,
        exchangesRaw,
        loansRaw,
        recentActivity
    });
});
app.listen(PORT, () => {
    console.log(`Backend running on http://localhost:${PORT}`);
});
// Global Error Handler
app.use((err, req, res, next) => {
    if (err instanceof zod_1.z.ZodError) {
        res.status(400).json({ error: 'Validation Error', details: err.issues });
    }
    else {
        console.error('Unhandled Error:', err);
        res.status(500).json({ error: err.message || 'Internal Server Error' });
    }
});