@maxwellmezadre/kotas-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/SKILL.md +122 -0
- package/dist/bin.js +3920 -0
- package/dist/mcp-bin.js +3717 -0
- package/docs/TOOLS.md +216 -0
- package/package.json +33 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,3920 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
20
|
+
var exports_config = {};
|
|
21
|
+
__export(exports_config, {
|
|
22
|
+
loadConfig: () => loadConfig,
|
|
23
|
+
SESSION_KEY_BYTES: () => SESSION_KEY_BYTES,
|
|
24
|
+
MAX_PAGE_SIZE: () => MAX_PAGE_SIZE,
|
|
25
|
+
IMPORT_BROWSERS: () => IMPORT_BROWSERS,
|
|
26
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
27
|
+
DEFAULT_APP_VERSION: () => DEFAULT_APP_VERSION,
|
|
28
|
+
DEFAULT_API_TOKEN: () => DEFAULT_API_TOKEN,
|
|
29
|
+
ConfigSchema: () => ConfigSchema,
|
|
30
|
+
ConfigError: () => ConfigError,
|
|
31
|
+
APP_ORIGIN: () => APP_ORIGIN
|
|
32
|
+
});
|
|
33
|
+
import { homedir } from "os";
|
|
34
|
+
import { join } from "path";
|
|
35
|
+
import { Type } from "@sinclair/typebox";
|
|
36
|
+
import { Value } from "@sinclair/typebox/value";
|
|
37
|
+
function readOptional(env, key) {
|
|
38
|
+
const raw = env[key]?.trim();
|
|
39
|
+
return raw ? raw : undefined;
|
|
40
|
+
}
|
|
41
|
+
function expandHome(path) {
|
|
42
|
+
return path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
43
|
+
}
|
|
44
|
+
function readBool(problems, env, key, fallback) {
|
|
45
|
+
const raw = readOptional(env, key);
|
|
46
|
+
if (raw === undefined)
|
|
47
|
+
return fallback;
|
|
48
|
+
const value = raw.toLowerCase();
|
|
49
|
+
if (["1", "true", "yes", "on"].includes(value))
|
|
50
|
+
return true;
|
|
51
|
+
if (["0", "false", "no", "off"].includes(value))
|
|
52
|
+
return false;
|
|
53
|
+
problems.push(`${key} deve ser boolean (1/0, true/false, yes/no, on/off), veio "${raw}"`);
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
function readInt(problems, env, key, fallback, min) {
|
|
57
|
+
const raw = readOptional(env, key);
|
|
58
|
+
if (raw === undefined)
|
|
59
|
+
return fallback;
|
|
60
|
+
const parsed = Number(raw);
|
|
61
|
+
if (!Number.isInteger(parsed) || parsed < min) {
|
|
62
|
+
problems.push(`${key} deve ser inteiro >= ${min}, veio "${raw}"`);
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
function readEnum(problems, env, key, allowed, fallback) {
|
|
68
|
+
const raw = readOptional(env, key);
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return fallback;
|
|
71
|
+
const value = raw.toLowerCase();
|
|
72
|
+
if (allowed.includes(value))
|
|
73
|
+
return value;
|
|
74
|
+
problems.push(`${key} deve ser ${allowed.join("|")}, veio "${raw}"`);
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
function readSessionKey(problems, env) {
|
|
78
|
+
const raw = readOptional(env, "KOTAS_SESSION_KEY");
|
|
79
|
+
if (raw === undefined)
|
|
80
|
+
return;
|
|
81
|
+
if (Buffer.from(raw, "base64").length !== SESSION_KEY_BYTES) {
|
|
82
|
+
problems.push(`KOTAS_SESSION_KEY deve ser base64 de ${SESSION_KEY_BYTES} bytes (gere com: openssl rand -base64 32)`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
return raw;
|
|
86
|
+
}
|
|
87
|
+
function readUrl(problems, env, key, fallback) {
|
|
88
|
+
const raw = readOptional(env, key);
|
|
89
|
+
if (raw === undefined)
|
|
90
|
+
return fallback;
|
|
91
|
+
try {
|
|
92
|
+
const url = new URL(raw);
|
|
93
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
94
|
+
throw new Error("protocol");
|
|
95
|
+
} catch {
|
|
96
|
+
problems.push(`${key} deve ser uma URL http(s), veio "${raw}"`);
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
return raw.replace(/\/+$/, "");
|
|
100
|
+
}
|
|
101
|
+
function loadConfig(env = process.env) {
|
|
102
|
+
const problems = [];
|
|
103
|
+
const configDir = expandHome(readOptional(env, "KOTAS_CONFIG_DIR") ?? join(homedir(), ".config", "kotas-mcp"));
|
|
104
|
+
const config = {
|
|
105
|
+
configDir,
|
|
106
|
+
sessionPath: join(configDir, "session.enc"),
|
|
107
|
+
keyPath: join(configDir, "session.key"),
|
|
108
|
+
dbPath: join(configDir, "cache.db"),
|
|
109
|
+
exportDir: expandHome(readOptional(env, "KOTAS_EXPORT_DIR") ?? join(homedir(), "Downloads", "kotas-export")),
|
|
110
|
+
sessionKey: readSessionKey(problems, env),
|
|
111
|
+
readOnly: readBool(problems, env, "KOTAS_READ_ONLY", false),
|
|
112
|
+
compact: readBool(problems, env, "KOTAS_COMPACT", false),
|
|
113
|
+
logFile: readOptional(env, "KOTAS_LOG_FILE"),
|
|
114
|
+
importBrowser: readEnum(problems, env, "KOTAS_IMPORT_BROWSER", IMPORT_BROWSERS, undefined),
|
|
115
|
+
email: readOptional(env, "KOTAS_EMAIL"),
|
|
116
|
+
password: readOptional(env, "KOTAS_SENHA") ?? readOptional(env, "KOTAS_PASSWORD"),
|
|
117
|
+
apiToken: readOptional(env, "KOTAS_API_TOKEN") ?? DEFAULT_API_TOKEN,
|
|
118
|
+
appVersion: readOptional(env, "KOTAS_APP_VERSION") ?? DEFAULT_APP_VERSION,
|
|
119
|
+
minIntervalMs: readInt(problems, env, "KOTAS_MIN_INTERVAL_MS", 300, 0),
|
|
120
|
+
jitterMs: readInt(problems, env, "KOTAS_JITTER_MS", 200, 0),
|
|
121
|
+
httpTimeoutMs: readInt(problems, env, "KOTAS_HTTP_TIMEOUT_MS", 30000, 1000),
|
|
122
|
+
baseUrl: readUrl(problems, env, "KOTAS_BASE_URL", DEFAULT_BASE_URL)
|
|
123
|
+
};
|
|
124
|
+
if (!Value.Check(ConfigSchema, config)) {
|
|
125
|
+
for (const error of Value.Errors(ConfigSchema, config)) {
|
|
126
|
+
problems.push(`${error.path || "/"}: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (problems.length > 0)
|
|
130
|
+
throw new ConfigError(problems);
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
var IMPORT_BROWSERS, DEFAULT_BASE_URL = "https://api-front.kotas.com.br", APP_ORIGIN = "https://app.kotas.com.br", DEFAULT_API_TOKEN = "72a69d5467f157206616b46624597607424c05bdabcbf83ddbe79625d46efcd1", DEFAULT_APP_VERSION = "1.127.0.0", SESSION_KEY_BYTES = 32, MAX_PAGE_SIZE = 50, ConfigSchema, ConfigError;
|
|
134
|
+
var init_config = __esm(() => {
|
|
135
|
+
IMPORT_BROWSERS = ["arc", "chrome", "chromium", "brave", "edge"];
|
|
136
|
+
ConfigSchema = Type.Object({
|
|
137
|
+
configDir: Type.String({ minLength: 1 }),
|
|
138
|
+
sessionPath: Type.String({ minLength: 1 }),
|
|
139
|
+
keyPath: Type.String({ minLength: 1 }),
|
|
140
|
+
dbPath: Type.String({ minLength: 1 }),
|
|
141
|
+
exportDir: Type.String({ minLength: 1 }),
|
|
142
|
+
sessionKey: Type.Optional(Type.String({ minLength: 1 })),
|
|
143
|
+
readOnly: Type.Boolean(),
|
|
144
|
+
compact: Type.Boolean(),
|
|
145
|
+
logFile: Type.Optional(Type.String({ minLength: 1 })),
|
|
146
|
+
importBrowser: Type.Optional(Type.Union([
|
|
147
|
+
Type.Literal("arc"),
|
|
148
|
+
Type.Literal("chrome"),
|
|
149
|
+
Type.Literal("chromium"),
|
|
150
|
+
Type.Literal("brave"),
|
|
151
|
+
Type.Literal("edge")
|
|
152
|
+
])),
|
|
153
|
+
email: Type.Optional(Type.String({ minLength: 3 })),
|
|
154
|
+
password: Type.Optional(Type.String({ minLength: 1 })),
|
|
155
|
+
apiToken: Type.String({ minLength: 1 }),
|
|
156
|
+
appVersion: Type.String({ minLength: 1 }),
|
|
157
|
+
minIntervalMs: Type.Integer({ minimum: 0 }),
|
|
158
|
+
jitterMs: Type.Integer({ minimum: 0 }),
|
|
159
|
+
httpTimeoutMs: Type.Integer({ minimum: 1000 }),
|
|
160
|
+
baseUrl: Type.String({ minLength: 1 })
|
|
161
|
+
});
|
|
162
|
+
ConfigError = class ConfigError extends Error {
|
|
163
|
+
problems;
|
|
164
|
+
constructor(problems) {
|
|
165
|
+
super(`Configura\xE7\xE3o inv\xE1lida do kotas-mcp:
|
|
166
|
+
${problems.map((problem) => ` - ${problem}`).join(`
|
|
167
|
+
`)}`);
|
|
168
|
+
this.problems = problems;
|
|
169
|
+
this.name = "ConfigError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/core/sqlite.ts
|
|
175
|
+
import { Database } from "bun:sqlite";
|
|
176
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
177
|
+
import { dirname } from "path";
|
|
178
|
+
function openDatabase(path) {
|
|
179
|
+
if (path === ":memory:") {
|
|
180
|
+
const memory = new Database(":memory:");
|
|
181
|
+
memory.exec("PRAGMA foreign_keys = ON;");
|
|
182
|
+
return memory;
|
|
183
|
+
}
|
|
184
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
185
|
+
const previous = process.umask(63);
|
|
186
|
+
let db;
|
|
187
|
+
try {
|
|
188
|
+
db = new Database(path, { create: true });
|
|
189
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
190
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
191
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
192
|
+
} finally {
|
|
193
|
+
process.umask(previous);
|
|
194
|
+
}
|
|
195
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
196
|
+
if (existsSync(path + suffix))
|
|
197
|
+
chmodSync(path + suffix, 384);
|
|
198
|
+
}
|
|
199
|
+
return db;
|
|
200
|
+
}
|
|
201
|
+
function inTx(db, fn) {
|
|
202
|
+
db.exec("BEGIN IMMEDIATE");
|
|
203
|
+
try {
|
|
204
|
+
const result = fn();
|
|
205
|
+
db.exec("COMMIT");
|
|
206
|
+
return result;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
db.exec("ROLLBACK");
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class Where {
|
|
214
|
+
clauses = [];
|
|
215
|
+
values = [];
|
|
216
|
+
add(clause, ...values) {
|
|
217
|
+
this.clauses.push(clause);
|
|
218
|
+
this.values.push(...values);
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
maybe(value, clause, ...values) {
|
|
222
|
+
if (value === undefined || value === null || value === "")
|
|
223
|
+
return this;
|
|
224
|
+
return this.add(clause, ...values);
|
|
225
|
+
}
|
|
226
|
+
sql() {
|
|
227
|
+
return this.clauses.length > 0 ? `WHERE ${this.clauses.join(" AND ")}` : "";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
var init_sqlite = () => {};
|
|
231
|
+
|
|
232
|
+
// src/cache/db.ts
|
|
233
|
+
function migrate(db) {
|
|
234
|
+
const row = db.query("PRAGMA user_version").get();
|
|
235
|
+
for (let version = row.user_version;version < MIGRATIONS.length; version += 1) {
|
|
236
|
+
const sql = MIGRATIONS[version];
|
|
237
|
+
db.transaction(() => {
|
|
238
|
+
db.exec(sql);
|
|
239
|
+
db.exec(`PRAGMA user_version = ${version + 1}`);
|
|
240
|
+
})();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function openCache(path) {
|
|
244
|
+
const db = openDatabase(path);
|
|
245
|
+
migrate(db);
|
|
246
|
+
return db;
|
|
247
|
+
}
|
|
248
|
+
var MIGRATIONS, META;
|
|
249
|
+
var init_db = __esm(() => {
|
|
250
|
+
init_sqlite();
|
|
251
|
+
MIGRATIONS = [
|
|
252
|
+
`
|
|
253
|
+
CREATE TABLE invoices (
|
|
254
|
+
id INTEGER PRIMARY KEY,
|
|
255
|
+
description TEXT NOT NULL DEFAULT '',
|
|
256
|
+
/* Parsed out of the description; the only surviving link to a dead group. */
|
|
257
|
+
product TEXT,
|
|
258
|
+
group_id INTEGER,
|
|
259
|
+
status_id INTEGER,
|
|
260
|
+
status TEXT,
|
|
261
|
+
type_id INTEGER,
|
|
262
|
+
type TEXT,
|
|
263
|
+
overdue INTEGER NOT NULL DEFAULT 0,
|
|
264
|
+
amount_units INTEGER,
|
|
265
|
+
interest_units INTEGER,
|
|
266
|
+
surcharge_units INTEGER,
|
|
267
|
+
discount_units INTEGER,
|
|
268
|
+
fee_units INTEGER,
|
|
269
|
+
/* What actually left the account. The only column that may be summed. */
|
|
270
|
+
total_units INTEGER,
|
|
271
|
+
due_date TEXT,
|
|
272
|
+
paid_date TEXT,
|
|
273
|
+
is_group_subscription INTEGER NOT NULL DEFAULT 0,
|
|
274
|
+
gateway_id INTEGER,
|
|
275
|
+
item_count INTEGER NOT NULL DEFAULT 0,
|
|
276
|
+
raw_list TEXT,
|
|
277
|
+
raw_detail TEXT,
|
|
278
|
+
detail_fetched_at TEXT,
|
|
279
|
+
detail_error TEXT,
|
|
280
|
+
parser_version INTEGER,
|
|
281
|
+
updated_at TEXT NOT NULL
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
CREATE TABLE invoice_items (
|
|
285
|
+
invoice_id INTEGER NOT NULL,
|
|
286
|
+
position INTEGER NOT NULL,
|
|
287
|
+
item_id INTEGER,
|
|
288
|
+
description TEXT NOT NULL DEFAULT '',
|
|
289
|
+
quantity INTEGER NOT NULL DEFAULT 1,
|
|
290
|
+
amount_units INTEGER,
|
|
291
|
+
PRIMARY KEY (invoice_id, position),
|
|
292
|
+
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
CREATE TABLE groups (
|
|
296
|
+
id INTEGER PRIMARY KEY,
|
|
297
|
+
name TEXT NOT NULL DEFAULT '',
|
|
298
|
+
service TEXT,
|
|
299
|
+
service_id INTEGER,
|
|
300
|
+
plan TEXT,
|
|
301
|
+
category_id INTEGER,
|
|
302
|
+
category TEXT,
|
|
303
|
+
status_id INTEGER,
|
|
304
|
+
status TEXT,
|
|
305
|
+
is_administrator INTEGER NOT NULL DEFAULT 0,
|
|
306
|
+
admin_name TEXT,
|
|
307
|
+
amount_units INTEGER,
|
|
308
|
+
service_total_units INTEGER,
|
|
309
|
+
amount_without_fee_units INTEGER,
|
|
310
|
+
fee_units INTEGER,
|
|
311
|
+
slots_total INTEGER,
|
|
312
|
+
slots_taken INTEGER,
|
|
313
|
+
slots_free INTEGER,
|
|
314
|
+
loyalty_months INTEGER,
|
|
315
|
+
loyalty_ends_at TEXT,
|
|
316
|
+
auto_renew INTEGER,
|
|
317
|
+
access_method TEXT,
|
|
318
|
+
joined_at TEXT,
|
|
319
|
+
cancelled_at TEXT,
|
|
320
|
+
scheduled_cancellation INTEGER NOT NULL DEFAULT 0,
|
|
321
|
+
waiting_list INTEGER NOT NULL DEFAULT 0,
|
|
322
|
+
private INTEGER NOT NULL DEFAULT 0,
|
|
323
|
+
raw_summary TEXT,
|
|
324
|
+
raw_detail TEXT,
|
|
325
|
+
detail_fetched_at TEXT,
|
|
326
|
+
/* A closed group answers HTTP 400; the reason is recorded, not retried. */
|
|
327
|
+
detail_error TEXT,
|
|
328
|
+
parser_version INTEGER,
|
|
329
|
+
updated_at TEXT NOT NULL
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
CREATE TABLE group_participants (
|
|
333
|
+
group_id INTEGER NOT NULL,
|
|
334
|
+
position INTEGER NOT NULL,
|
|
335
|
+
name TEXT,
|
|
336
|
+
amount_units INTEGER,
|
|
337
|
+
slots INTEGER,
|
|
338
|
+
joined_at TEXT,
|
|
339
|
+
access_sent_at TEXT,
|
|
340
|
+
left_at TEXT,
|
|
341
|
+
waiting_list INTEGER NOT NULL DEFAULT 0,
|
|
342
|
+
PRIMARY KEY (group_id, position),
|
|
343
|
+
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
CREATE TABLE credits (
|
|
347
|
+
id INTEGER PRIMARY KEY,
|
|
348
|
+
description TEXT NOT NULL DEFAULT '',
|
|
349
|
+
status_id INTEGER,
|
|
350
|
+
status TEXT,
|
|
351
|
+
type_id INTEGER,
|
|
352
|
+
type TEXT,
|
|
353
|
+
type_label TEXT,
|
|
354
|
+
amount_units INTEGER,
|
|
355
|
+
available_units INTEGER,
|
|
356
|
+
fee_units INTEGER,
|
|
357
|
+
credited_at TEXT,
|
|
358
|
+
invoice_id INTEGER,
|
|
359
|
+
group_id INTEGER,
|
|
360
|
+
refundable INTEGER NOT NULL DEFAULT 0,
|
|
361
|
+
raw TEXT,
|
|
362
|
+
updated_at TEXT NOT NULL
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
/* One row per administered group: the payload's "id" IS the group id. */
|
|
366
|
+
CREATE TABLE payouts (
|
|
367
|
+
group_id INTEGER PRIMARY KEY,
|
|
368
|
+
description TEXT NOT NULL DEFAULT '',
|
|
369
|
+
amount_units INTEGER,
|
|
370
|
+
member_share_units INTEGER,
|
|
371
|
+
status_id INTEGER,
|
|
372
|
+
status TEXT,
|
|
373
|
+
next_payment_date TEXT,
|
|
374
|
+
/* NULL means the statement was never fetched. An empty statement is a fact,
|
|
375
|
+
so it must be distinguishable from "not fetched yet". */
|
|
376
|
+
statement_fetched_at TEXT,
|
|
377
|
+
raw TEXT,
|
|
378
|
+
updated_at TEXT NOT NULL
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
CREATE TABLE payout_entries (
|
|
382
|
+
group_id INTEGER NOT NULL,
|
|
383
|
+
entry_id INTEGER NOT NULL,
|
|
384
|
+
description TEXT NOT NULL DEFAULT '',
|
|
385
|
+
participant TEXT,
|
|
386
|
+
amount_units INTEGER,
|
|
387
|
+
status_id INTEGER,
|
|
388
|
+
status TEXT,
|
|
389
|
+
date TEXT,
|
|
390
|
+
PRIMARY KEY (group_id, entry_id)
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
|
|
394
|
+
|
|
395
|
+
CREATE INDEX idx_invoices_paid ON invoices(paid_date DESC);
|
|
396
|
+
CREATE INDEX idx_invoices_status ON invoices(status_id);
|
|
397
|
+
CREATE INDEX idx_invoices_group ON invoices(group_id);
|
|
398
|
+
CREATE INDEX idx_invoices_due ON invoices(due_date DESC);
|
|
399
|
+
CREATE INDEX idx_items_invoice ON invoice_items(invoice_id);
|
|
400
|
+
CREATE INDEX idx_groups_admin ON groups(is_administrator);
|
|
401
|
+
CREATE INDEX idx_groups_status ON groups(status_id);
|
|
402
|
+
CREATE INDEX idx_credits_type ON credits(type_id);
|
|
403
|
+
CREATE INDEX idx_credits_group ON credits(group_id);
|
|
404
|
+
CREATE INDEX idx_credits_date ON credits(credited_at DESC);
|
|
405
|
+
|
|
406
|
+
/* remove_diacritics 2 is what makes "credito" find "Cr\xE9dito". */
|
|
407
|
+
CREATE VIRTUAL TABLE invoices_fts USING fts5(
|
|
408
|
+
description, product,
|
|
409
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
410
|
+
);
|
|
411
|
+
CREATE VIRTUAL TABLE groups_fts USING fts5(
|
|
412
|
+
name, service, plan,
|
|
413
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
414
|
+
);
|
|
415
|
+
`
|
|
416
|
+
];
|
|
417
|
+
META = {
|
|
418
|
+
cursor: "sync.cursor",
|
|
419
|
+
lastCompleted: "sync.last_completed_at",
|
|
420
|
+
lastFull: "sync.last_full_at",
|
|
421
|
+
balance: "balance.snapshot",
|
|
422
|
+
balanceAt: "balance.fetched_at"
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// src/domain/enums.ts
|
|
427
|
+
function labelOf(table, code) {
|
|
428
|
+
if (code === null || code === undefined)
|
|
429
|
+
return null;
|
|
430
|
+
const key = Number(code);
|
|
431
|
+
if (!Number.isFinite(key))
|
|
432
|
+
return null;
|
|
433
|
+
return table[key] ?? String(key);
|
|
434
|
+
}
|
|
435
|
+
var INVOICE_STATUS, INVOICE_STATUS_IDS, FINAL_INVOICE_STATUS_IDS, PAID_STATUS_ID = 13, INVOICE_TYPE, CREDIT_STATUS, CREDIT_TYPE, CREDIT_TYPE_LABEL, GROUP_STATUS, PAYOUT_STATUS, ACCESS_METHOD, SOCIAL_AUTH, invoiceStatusOf = (code) => labelOf(INVOICE_STATUS, code), creditStatusOf = (code) => labelOf(CREDIT_STATUS, code), creditTypeOf = (code) => labelOf(CREDIT_TYPE, code), groupStatusOf = (code) => labelOf(GROUP_STATUS, code), payoutStatusOf = (code) => labelOf(PAYOUT_STATUS, code), accessMethodOf = (code) => labelOf(ACCESS_METHOD, code), isFinalInvoice = (statusId) => statusId !== null && FINAL_INVOICE_STATUS_IDS.includes(statusId);
|
|
436
|
+
var init_enums = __esm(() => {
|
|
437
|
+
INVOICE_STATUS = {
|
|
438
|
+
11: "pendente",
|
|
439
|
+
13: "pago",
|
|
440
|
+
19: "cancelado",
|
|
441
|
+
63: "atraso",
|
|
442
|
+
67: "estornado"
|
|
443
|
+
};
|
|
444
|
+
INVOICE_STATUS_IDS = [11, 13, 19, 63, 67];
|
|
445
|
+
FINAL_INVOICE_STATUS_IDS = [13, 19, 67];
|
|
446
|
+
INVOICE_TYPE = {
|
|
447
|
+
14: "Cobran\xE7a",
|
|
448
|
+
37: "Compra de Credito Avulso"
|
|
449
|
+
};
|
|
450
|
+
CREDIT_STATUS = {
|
|
451
|
+
33: "bloqueado",
|
|
452
|
+
34: "disponivel",
|
|
453
|
+
35: "utilizado",
|
|
454
|
+
36: "estornado",
|
|
455
|
+
41: "pendentePagamento",
|
|
456
|
+
46: "cancelado",
|
|
457
|
+
65: "sacado"
|
|
458
|
+
};
|
|
459
|
+
CREDIT_TYPE = {
|
|
460
|
+
94: "adicaoDeSaldo",
|
|
461
|
+
96: "caucaoDaInscricao",
|
|
462
|
+
97: "repasseAdministrador",
|
|
463
|
+
98: "estornoCancelamento"
|
|
464
|
+
};
|
|
465
|
+
CREDIT_TYPE_LABEL = {
|
|
466
|
+
94: "Adi\xE7\xE3o de saldo",
|
|
467
|
+
96: "Cau\xE7\xE3o da inscri\xE7\xE3o (fica bloqueado enquanto a assinatura durar)",
|
|
468
|
+
97: "Repasse recebido como administrador",
|
|
469
|
+
98: "Estorno de cancelamento"
|
|
470
|
+
};
|
|
471
|
+
GROUP_STATUS = {
|
|
472
|
+
1: "aguardandoMembros",
|
|
473
|
+
3: "aguardandoAssinatura",
|
|
474
|
+
4: "ativo",
|
|
475
|
+
50: "pendenteAprovacao",
|
|
476
|
+
51: "reprovado",
|
|
477
|
+
52: "assinadoComVagas",
|
|
478
|
+
55: "cancelado",
|
|
479
|
+
68: "aguardandoEdicao",
|
|
480
|
+
69: "aguardandoLiberacao",
|
|
481
|
+
76: "filaEspera",
|
|
482
|
+
77: "cancelamentoAgendado",
|
|
483
|
+
110: "reprovadoAutomaticamente",
|
|
484
|
+
111: "triagem"
|
|
485
|
+
};
|
|
486
|
+
PAYOUT_STATUS = {
|
|
487
|
+
4: "agendado",
|
|
488
|
+
55: "cancelado"
|
|
489
|
+
};
|
|
490
|
+
ACCESS_METHOD = {
|
|
491
|
+
239: "loginSenha",
|
|
492
|
+
240: "convite",
|
|
493
|
+
241: "codigoAtivacao",
|
|
494
|
+
242: "cookie",
|
|
495
|
+
308: "combinar"
|
|
496
|
+
};
|
|
497
|
+
SOCIAL_AUTH = { kotas: 1, facebook: 2, google: 3 };
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// src/cache/statements.ts
|
|
501
|
+
function prepareStatements(db, stamp) {
|
|
502
|
+
const upsertInvoiceStmt = db.prepare(`
|
|
503
|
+
INSERT INTO invoices (
|
|
504
|
+
id, description, product, group_id, status_id, status, type_id, type, overdue,
|
|
505
|
+
amount_units, interest_units, surcharge_units, discount_units, fee_units, total_units,
|
|
506
|
+
due_date, paid_date, is_group_subscription, gateway_id, raw_list, parser_version, updated_at
|
|
507
|
+
) VALUES (
|
|
508
|
+
$id, $description, $product, $group_id, $status_id, $status, $type_id, $type, $overdue,
|
|
509
|
+
$amount_units, $interest_units, $surcharge_units, $discount_units, $fee_units, $total_units,
|
|
510
|
+
$due_date, $paid_date, $is_group_subscription, $gateway_id, $raw_list, $parser_version, $updated_at
|
|
511
|
+
)
|
|
512
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
513
|
+
description = excluded.description,
|
|
514
|
+
product = COALESCE(excluded.product, invoices.product),
|
|
515
|
+
group_id = COALESCE(excluded.group_id, invoices.group_id),
|
|
516
|
+
status_id = COALESCE(excluded.status_id, invoices.status_id),
|
|
517
|
+
status = COALESCE(excluded.status, invoices.status),
|
|
518
|
+
type_id = COALESCE(excluded.type_id, invoices.type_id),
|
|
519
|
+
type = COALESCE(excluded.type, invoices.type),
|
|
520
|
+
overdue = excluded.overdue,
|
|
521
|
+
amount_units = COALESCE(excluded.amount_units, invoices.amount_units),
|
|
522
|
+
interest_units = COALESCE(excluded.interest_units, invoices.interest_units),
|
|
523
|
+
surcharge_units = COALESCE(excluded.surcharge_units, invoices.surcharge_units),
|
|
524
|
+
discount_units = COALESCE(excluded.discount_units, invoices.discount_units),
|
|
525
|
+
fee_units = COALESCE(excluded.fee_units, invoices.fee_units),
|
|
526
|
+
total_units = COALESCE(excluded.total_units, invoices.total_units),
|
|
527
|
+
due_date = COALESCE(excluded.due_date, invoices.due_date),
|
|
528
|
+
paid_date = COALESCE(excluded.paid_date, invoices.paid_date),
|
|
529
|
+
is_group_subscription = excluded.is_group_subscription,
|
|
530
|
+
gateway_id = COALESCE(excluded.gateway_id, invoices.gateway_id),
|
|
531
|
+
raw_list = COALESCE(excluded.raw_list, invoices.raw_list),
|
|
532
|
+
parser_version = excluded.parser_version,
|
|
533
|
+
updated_at = excluded.updated_at
|
|
534
|
+
`);
|
|
535
|
+
const upsertGroupStmt = db.prepare(`
|
|
536
|
+
INSERT INTO groups (
|
|
537
|
+
id, name, service, service_id, plan, category_id, category, status_id, status,
|
|
538
|
+
is_administrator, admin_name, amount_units, service_total_units,
|
|
539
|
+
amount_without_fee_units, fee_units, slots_total, slots_taken, slots_free,
|
|
540
|
+
loyalty_months, loyalty_ends_at, auto_renew, access_method, joined_at, cancelled_at,
|
|
541
|
+
scheduled_cancellation, waiting_list, private, raw_summary, updated_at
|
|
542
|
+
) VALUES (
|
|
543
|
+
$id, $name, $service, $service_id, $plan, $category_id, $category, $status_id, $status,
|
|
544
|
+
$is_administrator, $admin_name, $amount_units, $service_total_units,
|
|
545
|
+
$amount_without_fee_units, $fee_units, $slots_total, $slots_taken, $slots_free,
|
|
546
|
+
$loyalty_months, $loyalty_ends_at, $auto_renew, $access_method, $joined_at, $cancelled_at,
|
|
547
|
+
$scheduled_cancellation, $waiting_list, $private, $raw_summary, $updated_at
|
|
548
|
+
)
|
|
549
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
550
|
+
name = excluded.name,
|
|
551
|
+
service = COALESCE(excluded.service, groups.service),
|
|
552
|
+
service_id = COALESCE(excluded.service_id, groups.service_id),
|
|
553
|
+
plan = COALESCE(excluded.plan, groups.plan),
|
|
554
|
+
category_id = COALESCE(excluded.category_id, groups.category_id),
|
|
555
|
+
category = COALESCE(excluded.category, groups.category),
|
|
556
|
+
status_id = COALESCE(excluded.status_id, groups.status_id),
|
|
557
|
+
status = COALESCE(excluded.status, groups.status),
|
|
558
|
+
is_administrator = excluded.is_administrator,
|
|
559
|
+
admin_name = COALESCE(excluded.admin_name, groups.admin_name),
|
|
560
|
+
amount_units = COALESCE(excluded.amount_units, groups.amount_units),
|
|
561
|
+
service_total_units = COALESCE(excluded.service_total_units, groups.service_total_units),
|
|
562
|
+
amount_without_fee_units =
|
|
563
|
+
COALESCE(excluded.amount_without_fee_units, groups.amount_without_fee_units),
|
|
564
|
+
fee_units = COALESCE(excluded.fee_units, groups.fee_units),
|
|
565
|
+
slots_total = COALESCE(excluded.slots_total, groups.slots_total),
|
|
566
|
+
slots_taken = COALESCE(excluded.slots_taken, groups.slots_taken),
|
|
567
|
+
slots_free = COALESCE(excluded.slots_free, groups.slots_free),
|
|
568
|
+
loyalty_months = COALESCE(excluded.loyalty_months, groups.loyalty_months),
|
|
569
|
+
loyalty_ends_at = COALESCE(excluded.loyalty_ends_at, groups.loyalty_ends_at),
|
|
570
|
+
auto_renew = COALESCE(excluded.auto_renew, groups.auto_renew),
|
|
571
|
+
access_method = COALESCE(excluded.access_method, groups.access_method),
|
|
572
|
+
joined_at = COALESCE(excluded.joined_at, groups.joined_at),
|
|
573
|
+
cancelled_at = COALESCE(excluded.cancelled_at, groups.cancelled_at),
|
|
574
|
+
scheduled_cancellation = excluded.scheduled_cancellation,
|
|
575
|
+
waiting_list = excluded.waiting_list,
|
|
576
|
+
private = excluded.private,
|
|
577
|
+
raw_summary = COALESCE(excluded.raw_summary, groups.raw_summary),
|
|
578
|
+
updated_at = excluded.updated_at
|
|
579
|
+
`);
|
|
580
|
+
const invoiceParams = (invoice, raw, parserVersion) => ({
|
|
581
|
+
$id: invoice.id,
|
|
582
|
+
$description: invoice.description,
|
|
583
|
+
$product: invoice.product,
|
|
584
|
+
$group_id: invoice.groupId,
|
|
585
|
+
$status_id: invoice.statusId,
|
|
586
|
+
$status: invoice.status,
|
|
587
|
+
$type_id: invoice.typeId,
|
|
588
|
+
$type: invoice.type,
|
|
589
|
+
$overdue: flag(invoice.overdue),
|
|
590
|
+
$amount_units: invoice.amount,
|
|
591
|
+
$interest_units: invoice.interest,
|
|
592
|
+
$surcharge_units: invoice.surcharge,
|
|
593
|
+
$discount_units: invoice.discount,
|
|
594
|
+
$fee_units: invoice.fee,
|
|
595
|
+
$total_units: invoice.total,
|
|
596
|
+
$due_date: invoice.dueDate,
|
|
597
|
+
$paid_date: invoice.paidDate,
|
|
598
|
+
$is_group_subscription: flag(invoice.isGroupSubscription),
|
|
599
|
+
$gateway_id: invoice.gatewayId,
|
|
600
|
+
$raw_list: raw,
|
|
601
|
+
$parser_version: parserVersion,
|
|
602
|
+
$updated_at: stamp()
|
|
603
|
+
});
|
|
604
|
+
const groupParams = (group, raw) => ({
|
|
605
|
+
$id: group.id,
|
|
606
|
+
$name: group.name,
|
|
607
|
+
$service: group.service,
|
|
608
|
+
$service_id: group.serviceId,
|
|
609
|
+
$plan: group.plan,
|
|
610
|
+
$category_id: group.categoryId,
|
|
611
|
+
$category: group.category,
|
|
612
|
+
$status_id: group.statusId,
|
|
613
|
+
$status: group.status,
|
|
614
|
+
$is_administrator: flag(group.isAdministrator),
|
|
615
|
+
$admin_name: group.adminName,
|
|
616
|
+
$amount_units: group.amount,
|
|
617
|
+
$service_total_units: group.serviceTotal,
|
|
618
|
+
$amount_without_fee_units: group.amountWithoutFee,
|
|
619
|
+
$fee_units: group.fee,
|
|
620
|
+
$slots_total: group.slotsTotal,
|
|
621
|
+
$slots_taken: group.slotsTaken,
|
|
622
|
+
$slots_free: group.slotsFree,
|
|
623
|
+
$loyalty_months: group.loyaltyMonths,
|
|
624
|
+
$loyalty_ends_at: group.loyaltyEndsAt,
|
|
625
|
+
$auto_renew: group.autoRenew === null ? null : flag(group.autoRenew),
|
|
626
|
+
$access_method: group.accessMethod,
|
|
627
|
+
$joined_at: group.joinedAt,
|
|
628
|
+
$cancelled_at: group.cancelledAt,
|
|
629
|
+
$scheduled_cancellation: flag(group.scheduledCancellation),
|
|
630
|
+
$waiting_list: flag(group.waitingList),
|
|
631
|
+
$private: flag(group.private),
|
|
632
|
+
$raw_summary: raw,
|
|
633
|
+
$updated_at: stamp()
|
|
634
|
+
});
|
|
635
|
+
return { upsertInvoiceStmt, upsertGroupStmt, invoiceParams, groupParams };
|
|
636
|
+
}
|
|
637
|
+
var flag = (value) => value ? 1 : 0;
|
|
638
|
+
|
|
639
|
+
// src/cache/repo.ts
|
|
640
|
+
function invoiceWhere(filters) {
|
|
641
|
+
const where = new Where;
|
|
642
|
+
where.maybe(filters.statusId, "status_id = ?", filters.statusId);
|
|
643
|
+
where.maybe(filters.groupId, "group_id = ?", filters.groupId);
|
|
644
|
+
where.maybe(filters.from, `${INVOICE_DATE} >= ?`, filters.from);
|
|
645
|
+
where.maybe(filters.to, `${INVOICE_DATE} <= ?`, filters.to);
|
|
646
|
+
where.maybe(filters.product, "product = ?", filters.product);
|
|
647
|
+
if (filters.onlyPaid)
|
|
648
|
+
where.add("status_id = 13");
|
|
649
|
+
return where;
|
|
650
|
+
}
|
|
651
|
+
function createCacheRepo(db, now) {
|
|
652
|
+
const stamp = () => new Date(now()).toISOString();
|
|
653
|
+
const { upsertInvoiceStmt, upsertGroupStmt, invoiceParams, groupParams } = prepareStatements(db, stamp);
|
|
654
|
+
return {
|
|
655
|
+
upsertInvoices(invoices, raws, parserVersion) {
|
|
656
|
+
return inTx(db, () => {
|
|
657
|
+
invoices.forEach((invoice, index) => {
|
|
658
|
+
const raw = raws[index];
|
|
659
|
+
upsertInvoiceStmt.run(invoiceParams(invoice, raw === undefined ? null : JSON.stringify(raw), parserVersion));
|
|
660
|
+
});
|
|
661
|
+
return invoices.length;
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
invoicesToReparse(parserVersion) {
|
|
665
|
+
return db.query(`SELECT id, raw_list FROM invoices
|
|
666
|
+
WHERE raw_list IS NOT NULL
|
|
667
|
+
AND (parser_version IS NULL OR parser_version < ?)`).all(parserVersion);
|
|
668
|
+
},
|
|
669
|
+
setInvoiceItems(invoiceId, items, raw) {
|
|
670
|
+
inTx(db, () => {
|
|
671
|
+
db.query("DELETE FROM invoice_items WHERE invoice_id = ?").run(invoiceId);
|
|
672
|
+
const insert = db.prepare(`INSERT INTO invoice_items (invoice_id, position, item_id, description, quantity, amount_units)
|
|
673
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
674
|
+
for (const item of items) {
|
|
675
|
+
insert.run(item.invoiceId, item.position, item.id, item.description, item.quantity, item.amount);
|
|
676
|
+
}
|
|
677
|
+
db.query(`UPDATE invoices SET item_count = ?, raw_detail = ?, detail_fetched_at = ?,
|
|
678
|
+
detail_error = NULL, updated_at = ? WHERE id = ?`).run(items.length, JSON.stringify(raw ?? null), stamp(), stamp(), invoiceId);
|
|
679
|
+
});
|
|
680
|
+
},
|
|
681
|
+
markInvoiceDetailError(invoiceId, message) {
|
|
682
|
+
db.query("UPDATE invoices SET detail_error = ?, detail_fetched_at = ?, updated_at = ? WHERE id = ?").run(message.slice(0, 300), stamp(), stamp(), invoiceId);
|
|
683
|
+
},
|
|
684
|
+
pendingInvoiceDetails(limit) {
|
|
685
|
+
const threshold = new Date(now() - DETAIL_TTL_MS).toISOString();
|
|
686
|
+
const rows = db.query(`SELECT id, status_id, detail_fetched_at FROM invoices
|
|
687
|
+
WHERE detail_error IS NULL
|
|
688
|
+
AND (detail_fetched_at IS NULL OR detail_fetched_at < ?)
|
|
689
|
+
ORDER BY ${INVOICE_DATE} DESC
|
|
690
|
+
LIMIT ?`).all(threshold, limit * 4);
|
|
691
|
+
const wanted = [];
|
|
692
|
+
for (const row of rows) {
|
|
693
|
+
if (row.detail_fetched_at !== null && isFinalInvoice(row.status_id))
|
|
694
|
+
continue;
|
|
695
|
+
wanted.push(row.id);
|
|
696
|
+
if (wanted.length >= limit)
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
return wanted;
|
|
700
|
+
},
|
|
701
|
+
upsertGroups(groups, raws) {
|
|
702
|
+
return inTx(db, () => {
|
|
703
|
+
groups.forEach((group, index) => {
|
|
704
|
+
upsertGroupStmt.run(groupParams(group, JSON.stringify(raws[index] ?? null)));
|
|
705
|
+
});
|
|
706
|
+
return groups.length;
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
setGroupDetail(group, participants, raw) {
|
|
710
|
+
inTx(db, () => {
|
|
711
|
+
upsertGroupStmt.run(groupParams(group, null));
|
|
712
|
+
db.query("DELETE FROM group_participants WHERE group_id = ?").run(group.id);
|
|
713
|
+
const insert = db.prepare(`INSERT INTO group_participants
|
|
714
|
+
(group_id, position, name, amount_units, slots, joined_at, access_sent_at, left_at, waiting_list)
|
|
715
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
716
|
+
for (const person of participants) {
|
|
717
|
+
insert.run(person.groupId, person.position, person.name, person.amount, person.slots, person.joinedAt, person.accessSentAt, person.leftAt, flag2(person.waitingList));
|
|
718
|
+
}
|
|
719
|
+
db.query(`UPDATE groups SET raw_detail = ?, detail_fetched_at = ?, detail_error = NULL,
|
|
720
|
+
updated_at = ? WHERE id = ?`).run(JSON.stringify(raw ?? null), stamp(), stamp(), group.id);
|
|
721
|
+
});
|
|
722
|
+
},
|
|
723
|
+
pendingGroupDetails(limit) {
|
|
724
|
+
return db.query(`SELECT id FROM groups
|
|
725
|
+
WHERE detail_fetched_at IS NULL AND detail_error IS NULL
|
|
726
|
+
ORDER BY id LIMIT ?`).all(limit).map((row) => row.id);
|
|
727
|
+
},
|
|
728
|
+
markGroupDetailError(groupId, message) {
|
|
729
|
+
db.query("UPDATE groups SET detail_error = ?, detail_fetched_at = ?, updated_at = ? WHERE id = ?").run(message.slice(0, 300), stamp(), stamp(), groupId);
|
|
730
|
+
},
|
|
731
|
+
upsertCredits(credits, raws) {
|
|
732
|
+
return inTx(db, () => {
|
|
733
|
+
const insert = db.prepare(`INSERT INTO credits (id, description, status_id, status, type_id, type, type_label,
|
|
734
|
+
amount_units, available_units, fee_units, credited_at,
|
|
735
|
+
invoice_id, group_id, refundable, raw, updated_at)
|
|
736
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
737
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
738
|
+
description = excluded.description, status_id = excluded.status_id,
|
|
739
|
+
status = excluded.status, type_id = excluded.type_id, type = excluded.type,
|
|
740
|
+
type_label = excluded.type_label, amount_units = excluded.amount_units,
|
|
741
|
+
/* The two halves of the ledger are disjoint, but a value already
|
|
742
|
+
known must never be replaced by an absent one. */
|
|
743
|
+
available_units = COALESCE(excluded.available_units, credits.available_units),
|
|
744
|
+
fee_units = excluded.fee_units,
|
|
745
|
+
credited_at = COALESCE(excluded.credited_at, credits.credited_at),
|
|
746
|
+
invoice_id = COALESCE(excluded.invoice_id, credits.invoice_id),
|
|
747
|
+
group_id = COALESCE(excluded.group_id, credits.group_id),
|
|
748
|
+
refundable = excluded.refundable, raw = excluded.raw,
|
|
749
|
+
updated_at = excluded.updated_at`);
|
|
750
|
+
credits.forEach((credit, index) => {
|
|
751
|
+
insert.run(credit.id, credit.description, credit.statusId, credit.status, credit.typeId, credit.type, credit.typeLabel, credit.amount, credit.available, credit.fee, credit.creditedAt, credit.invoiceId, credit.groupId, flag2(credit.refundable), JSON.stringify(raws[index] ?? null), stamp());
|
|
752
|
+
});
|
|
753
|
+
return credits.length;
|
|
754
|
+
});
|
|
755
|
+
},
|
|
756
|
+
upsertPayouts(payouts, raws) {
|
|
757
|
+
return inTx(db, () => {
|
|
758
|
+
const insert = db.prepare(`INSERT INTO payouts (group_id, description, amount_units, member_share_units,
|
|
759
|
+
status_id, status, next_payment_date, raw, updated_at)
|
|
760
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
761
|
+
ON CONFLICT(group_id) DO UPDATE SET
|
|
762
|
+
description = excluded.description, amount_units = excluded.amount_units,
|
|
763
|
+
member_share_units = excluded.member_share_units, status_id = excluded.status_id,
|
|
764
|
+
status = excluded.status, next_payment_date = excluded.next_payment_date,
|
|
765
|
+
raw = excluded.raw, updated_at = excluded.updated_at`);
|
|
766
|
+
payouts.forEach((payout, index) => {
|
|
767
|
+
insert.run(payout.groupId, payout.description, payout.amount, payout.memberShare, payout.statusId, payout.status, payout.nextPaymentDate, JSON.stringify(raws[index] ?? null), stamp());
|
|
768
|
+
});
|
|
769
|
+
return payouts.length;
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
pendingPayoutStatements(limit) {
|
|
773
|
+
return db.query("SELECT group_id FROM payouts WHERE statement_fetched_at IS NULL ORDER BY group_id LIMIT ?").all(limit).map((row) => row.group_id);
|
|
774
|
+
},
|
|
775
|
+
setPayoutEntries(groupId, entries) {
|
|
776
|
+
inTx(db, () => {
|
|
777
|
+
db.query("DELETE FROM payout_entries WHERE group_id = ?").run(groupId);
|
|
778
|
+
const insert = db.prepare(`INSERT INTO payout_entries
|
|
779
|
+
(group_id, entry_id, description, participant, amount_units, status_id, status, date)
|
|
780
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
781
|
+
for (const entry of entries) {
|
|
782
|
+
insert.run(entry.groupId, entry.id, entry.description, entry.participant, entry.amount, entry.statusId, entry.status, entry.date);
|
|
783
|
+
}
|
|
784
|
+
db.query("UPDATE payouts SET statement_fetched_at = ? WHERE group_id = ?").run(stamp(), groupId);
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
listInvoices(filters) {
|
|
788
|
+
const where = invoiceWhere(filters);
|
|
789
|
+
return db.query(`SELECT * FROM invoices ${where.sql()}
|
|
790
|
+
ORDER BY ${INVOICE_DATE} DESC, id DESC LIMIT ? OFFSET ?`).all(...where.values, filters.limit ?? 50, filters.offset ?? 0);
|
|
791
|
+
},
|
|
792
|
+
countInvoices(filters) {
|
|
793
|
+
const where = invoiceWhere(filters);
|
|
794
|
+
const row = db.query(`SELECT COUNT(*) AS total FROM invoices ${where.sql()}`).get(...where.values);
|
|
795
|
+
return row.total;
|
|
796
|
+
},
|
|
797
|
+
getInvoice(id) {
|
|
798
|
+
return db.query("SELECT * FROM invoices WHERE id = ?").get(id) ?? null;
|
|
799
|
+
},
|
|
800
|
+
getInvoiceItems(invoiceId) {
|
|
801
|
+
return db.query("SELECT * FROM invoice_items WHERE invoice_id = ? ORDER BY position").all(invoiceId);
|
|
802
|
+
},
|
|
803
|
+
listGroups(options = {}) {
|
|
804
|
+
const where = new Where;
|
|
805
|
+
where.maybe(options.administrator === undefined ? undefined : 1, "is_administrator = ?", options.administrator ? 1 : 0);
|
|
806
|
+
where.maybe(options.statusId, "status_id = ?", options.statusId);
|
|
807
|
+
return db.query(`SELECT * FROM groups ${where.sql()} ORDER BY name COLLATE NOCASE LIMIT ?`).all(...where.values, options.limit ?? 200);
|
|
808
|
+
},
|
|
809
|
+
getGroup(id) {
|
|
810
|
+
return db.query("SELECT * FROM groups WHERE id = ?").get(id) ?? null;
|
|
811
|
+
},
|
|
812
|
+
getParticipants(groupId) {
|
|
813
|
+
return db.query("SELECT * FROM group_participants WHERE group_id = ? ORDER BY position").all(groupId);
|
|
814
|
+
},
|
|
815
|
+
listCredits(options = {}) {
|
|
816
|
+
const where = new Where;
|
|
817
|
+
where.maybe(options.typeId, "type_id = ?", options.typeId);
|
|
818
|
+
where.maybe(options.statusId, "status_id = ?", options.statusId);
|
|
819
|
+
return db.query(`SELECT * FROM credits ${where.sql()} ORDER BY credited_at DESC, id DESC LIMIT ?`).all(...where.values, options.limit ?? 100);
|
|
820
|
+
},
|
|
821
|
+
listPayouts() {
|
|
822
|
+
return db.query("SELECT * FROM payouts ORDER BY next_payment_date IS NULL, next_payment_date").all();
|
|
823
|
+
},
|
|
824
|
+
getPayoutEntries(groupId) {
|
|
825
|
+
return db.query("SELECT * FROM payout_entries WHERE group_id = ? ORDER BY date DESC, entry_id DESC").all(groupId);
|
|
826
|
+
},
|
|
827
|
+
rebuildFts() {
|
|
828
|
+
inTx(db, () => {
|
|
829
|
+
db.exec("DELETE FROM invoices_fts");
|
|
830
|
+
db.exec(`INSERT INTO invoices_fts (rowid, description, product)
|
|
831
|
+
SELECT id, description, COALESCE(product, '') FROM invoices`);
|
|
832
|
+
db.exec("DELETE FROM groups_fts");
|
|
833
|
+
db.exec(`INSERT INTO groups_fts (rowid, name, service, plan)
|
|
834
|
+
SELECT id, name, COALESCE(service, ''), COALESCE(plan, '') FROM groups`);
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
getMeta(key) {
|
|
838
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
839
|
+
return row?.value ?? null;
|
|
840
|
+
},
|
|
841
|
+
setMeta(key, value) {
|
|
842
|
+
if (value === null) {
|
|
843
|
+
db.query("DELETE FROM meta WHERE key = ?").run(key);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
847
|
+
},
|
|
848
|
+
query(sql, ...values) {
|
|
849
|
+
return db.query(sql).all(...values);
|
|
850
|
+
},
|
|
851
|
+
stats() {
|
|
852
|
+
const one = (sql) => db.query(sql).get();
|
|
853
|
+
const counts = one(`SELECT COUNT(*) AS invoices,
|
|
854
|
+
SUM(CASE WHEN detail_fetched_at IS NOT NULL THEN 1 ELSE 0 END) AS withItems,
|
|
855
|
+
SUM(CASE WHEN detail_error IS NOT NULL THEN 1 ELSE 0 END) AS errors,
|
|
856
|
+
MIN(${INVOICE_DATE}) AS oldest, MAX(${INVOICE_DATE}) AS newest
|
|
857
|
+
FROM invoices`);
|
|
858
|
+
const groups = one("SELECT COUNT(*) AS total FROM groups").total;
|
|
859
|
+
const credits = one("SELECT COUNT(*) AS total FROM credits").total;
|
|
860
|
+
const payouts = one("SELECT COUNT(*) AS total FROM payouts").total;
|
|
861
|
+
return {
|
|
862
|
+
invoices: counts.invoices,
|
|
863
|
+
invoicesWithItems: counts.withItems ?? 0,
|
|
864
|
+
pendingDetails: this.pendingInvoiceDetails(1e4).length,
|
|
865
|
+
detailErrors: counts.errors ?? 0,
|
|
866
|
+
groups,
|
|
867
|
+
credits,
|
|
868
|
+
payouts,
|
|
869
|
+
oldest: counts.oldest,
|
|
870
|
+
newest: counts.newest,
|
|
871
|
+
lastSyncAt: this.getMeta(META.lastCompleted),
|
|
872
|
+
lastFullSyncAt: this.getMeta(META.lastFull)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
var DETAIL_TTL_MS, flag2 = (value) => value ? 1 : 0, INVOICE_DATE = "COALESCE(paid_date, due_date)";
|
|
878
|
+
var init_repo = __esm(() => {
|
|
879
|
+
init_sqlite();
|
|
880
|
+
init_enums();
|
|
881
|
+
init_db();
|
|
882
|
+
DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// src/core/errors.ts
|
|
886
|
+
var LOGIN_HINT = "Rode `kotas login` no terminal (e-mail e senha) ou `kotas login --from-browser chrome`.", KotasApiError, KotasHttpError, KotasAuthError, DeviceChallengeError, TwoFactorError, RateLimitError, SessionError, LoginError, ParseError, STATUS_HINTS;
|
|
887
|
+
var init_errors = __esm(() => {
|
|
888
|
+
KotasApiError = class KotasApiError extends Error {
|
|
889
|
+
status;
|
|
890
|
+
apiMessage;
|
|
891
|
+
path;
|
|
892
|
+
data;
|
|
893
|
+
constructor(status, apiMessage, path, hint, data) {
|
|
894
|
+
super(`O Kotas respondeu ${status} em ${path}: ${apiMessage || "sem mensagem"}` + (hint ? ` \u2014 ${hint}` : ""));
|
|
895
|
+
this.status = status;
|
|
896
|
+
this.apiMessage = apiMessage;
|
|
897
|
+
this.path = path;
|
|
898
|
+
this.data = data;
|
|
899
|
+
this.name = "KotasApiError";
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
KotasHttpError = class KotasHttpError extends Error {
|
|
903
|
+
status;
|
|
904
|
+
constructor(status, message) {
|
|
905
|
+
super(message);
|
|
906
|
+
this.status = status;
|
|
907
|
+
this.name = "KotasHttpError";
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
KotasAuthError = class KotasAuthError extends Error {
|
|
911
|
+
constructor(message) {
|
|
912
|
+
super(`${message} ${LOGIN_HINT}`);
|
|
913
|
+
this.name = "KotasAuthError";
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
DeviceChallengeError = class DeviceChallengeError extends Error {
|
|
917
|
+
hash;
|
|
918
|
+
tipoSolicitacao;
|
|
919
|
+
constructor(hash, tipoSolicitacao) {
|
|
920
|
+
const what = tipoSolicitacao === 2 ? "o IP" : tipoSolicitacao === 1 ? "o dispositivo" : "o acesso";
|
|
921
|
+
super(`O Kotas pediu a libera\xE7\xE3o d${what === "o IP" ? "o" : "o"} ${what}. ` + "Confirme pelo e-mail, SMS ou Telegram que a Kotas acabou de enviar e rode `kotas login` de novo. " + "O identificador do dispositivo foi salvo; n\xE3o apague a sess\xE3o nem troque de m\xE1quina no meio do processo.");
|
|
922
|
+
this.hash = hash;
|
|
923
|
+
this.tipoSolicitacao = tipoSolicitacao;
|
|
924
|
+
this.name = "DeviceChallengeError";
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
TwoFactorError = class TwoFactorError extends Error {
|
|
928
|
+
constructor(message) {
|
|
929
|
+
super(`${message} Rode \`kotas login --pin 123456\` com o c\xF3digo de 6 d\xEDgitos.`);
|
|
930
|
+
this.name = "TwoFactorError";
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
RateLimitError = class RateLimitError extends Error {
|
|
934
|
+
attempts;
|
|
935
|
+
constructor(attempts, message) {
|
|
936
|
+
super(message);
|
|
937
|
+
this.attempts = attempts;
|
|
938
|
+
this.name = "RateLimitError";
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
SessionError = class SessionError extends Error {
|
|
942
|
+
constructor(message) {
|
|
943
|
+
super(message);
|
|
944
|
+
this.name = "SessionError";
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
LoginError = class LoginError extends Error {
|
|
948
|
+
constructor(message) {
|
|
949
|
+
super(message);
|
|
950
|
+
this.name = "LoginError";
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
ParseError = class ParseError extends Error {
|
|
954
|
+
constructor(message) {
|
|
955
|
+
super(`${message} Rode \`kotas doctor\` para ver qual camada quebrou.`);
|
|
956
|
+
this.name = "ParseError";
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
STATUS_HINTS = {
|
|
960
|
+
400: "o Kotas devolve 400 tamb\xE9m para grupo encerrado, que some da API depois do cancelamento",
|
|
961
|
+
401: "token expirado ou recusado",
|
|
962
|
+
404: "rota inexistente, ou faltou algum par\xE2metro obrigat\xF3rio de query",
|
|
963
|
+
412: "libera\xE7\xE3o de dispositivo ou de IP pendente",
|
|
964
|
+
426: "a vers\xE3o do front (header `versao`) est\xE1 velha demais; ajuste KOTAS_APP_VERSION",
|
|
965
|
+
429: "limite de requisi\xE7\xF5es"
|
|
966
|
+
};
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// src/session/jwt.ts
|
|
970
|
+
function decodeSegment(segment) {
|
|
971
|
+
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
972
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
973
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
974
|
+
}
|
|
975
|
+
function decodeJwt(token) {
|
|
976
|
+
const parts = token.split(".");
|
|
977
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
978
|
+
throw new SessionError("O token salvo n\xE3o \xE9 um JWT (esperado 3 partes separadas por ponto).");
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const parsed = JSON.parse(decodeSegment(parts[1]));
|
|
982
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
983
|
+
throw new Error("not a claims object");
|
|
984
|
+
}
|
|
985
|
+
return parsed;
|
|
986
|
+
} catch {
|
|
987
|
+
throw new SessionError("O payload do token salvo n\xE3o \xE9 um JSON v\xE1lido.");
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function expiresAt(token) {
|
|
991
|
+
const { exp } = decodeJwt(token);
|
|
992
|
+
return typeof exp === "number" ? exp * 1000 : null;
|
|
993
|
+
}
|
|
994
|
+
function isExpired(token, now, skewMs = REFRESH_SKEW_MS) {
|
|
995
|
+
const at = expiresAt(token);
|
|
996
|
+
return at === null || now > at - skewMs;
|
|
997
|
+
}
|
|
998
|
+
function profileFrom(claims) {
|
|
999
|
+
const flag3 = (value) => value === undefined ? undefined : String(value).toLowerCase() === "true";
|
|
1000
|
+
const count = (value) => {
|
|
1001
|
+
const parsed = Number(value);
|
|
1002
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
userId: claims.ID_USUARIO,
|
|
1006
|
+
name: claims.NOME_COMPLETO_USUARIO ?? claims.NOME,
|
|
1007
|
+
nickname: claims.TX_NICKNAME,
|
|
1008
|
+
email: claims.TX_EMAIL_USUARIO,
|
|
1009
|
+
purchases: count(claims.QN_COMPRAS),
|
|
1010
|
+
twoFactorEnabled: flag3(claims.TWFAHABILITADO),
|
|
1011
|
+
isMember: flag3(claims.IS_MBA),
|
|
1012
|
+
isAdministrator: flag3(claims.IS_ADA)
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
var REFRESH_SKEW_MS = 30000;
|
|
1016
|
+
var init_jwt = __esm(() => {
|
|
1017
|
+
init_errors();
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// src/core/http.ts
|
|
1021
|
+
function createHttp(opts, deps = {}) {
|
|
1022
|
+
const doFetch = deps.fetch ?? ((url2, init) => fetch(url2, init));
|
|
1023
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
1024
|
+
const now = deps.now ?? (() => Date.now());
|
|
1025
|
+
const random = deps.random ?? Math.random;
|
|
1026
|
+
const { session, log } = opts;
|
|
1027
|
+
let jar = null;
|
|
1028
|
+
let loadedMtime = null;
|
|
1029
|
+
let authDead = false;
|
|
1030
|
+
let requests = 0;
|
|
1031
|
+
let refreshes = 0;
|
|
1032
|
+
let lastRequestAt = null;
|
|
1033
|
+
let refreshing = null;
|
|
1034
|
+
let chain = Promise.resolve();
|
|
1035
|
+
const serial = (fn) => {
|
|
1036
|
+
const run = chain.then(fn);
|
|
1037
|
+
chain = run.then(() => {
|
|
1038
|
+
return;
|
|
1039
|
+
}, () => {
|
|
1040
|
+
return;
|
|
1041
|
+
});
|
|
1042
|
+
return run;
|
|
1043
|
+
};
|
|
1044
|
+
function markAuthDead(message) {
|
|
1045
|
+
authDead = true;
|
|
1046
|
+
throw new KotasAuthError(message);
|
|
1047
|
+
}
|
|
1048
|
+
function ensureSession() {
|
|
1049
|
+
const mtime = session.mtimeMs();
|
|
1050
|
+
if (jar === null || authDead && mtime !== loadedMtime) {
|
|
1051
|
+
jar = session.load();
|
|
1052
|
+
loadedMtime = mtime;
|
|
1053
|
+
authDead = false;
|
|
1054
|
+
}
|
|
1055
|
+
if (jar === null)
|
|
1056
|
+
markAuthDead("Nenhuma sess\xE3o do Kotas salva.");
|
|
1057
|
+
if (authDead)
|
|
1058
|
+
markAuthDead("A sess\xE3o do Kotas expirou ou foi recusada.");
|
|
1059
|
+
return jar;
|
|
1060
|
+
}
|
|
1061
|
+
async function gap() {
|
|
1062
|
+
const earliest = (lastRequestAt ?? 0) + opts.minIntervalMs + random() * opts.jitterMs;
|
|
1063
|
+
const wait = earliest - now();
|
|
1064
|
+
if (wait > 0)
|
|
1065
|
+
await sleep(wait);
|
|
1066
|
+
lastRequestAt = now();
|
|
1067
|
+
}
|
|
1068
|
+
function url(request) {
|
|
1069
|
+
const target = new URL(request.path.replace(/^\/+/, ""), `${opts.baseUrl}/`);
|
|
1070
|
+
for (const [key, value] of Object.entries(request.query ?? {})) {
|
|
1071
|
+
if (value !== undefined && value !== null)
|
|
1072
|
+
target.searchParams.set(key, String(value));
|
|
1073
|
+
}
|
|
1074
|
+
return target.toString();
|
|
1075
|
+
}
|
|
1076
|
+
function headersFor(request, current) {
|
|
1077
|
+
return {
|
|
1078
|
+
"content-type": "application/json",
|
|
1079
|
+
accept: "application/json, text/plain, */*",
|
|
1080
|
+
token: opts.apiToken,
|
|
1081
|
+
versao: opts.appVersion,
|
|
1082
|
+
hashDispositivo: current?.hashDispositivo ?? "",
|
|
1083
|
+
...current && request.auth !== "none" ? { authorization: `Bearer ${current.accessToken}` } : {},
|
|
1084
|
+
...request.headers
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
async function dispatch(request, current) {
|
|
1088
|
+
const label = request.label ?? request.path;
|
|
1089
|
+
const target = url(request);
|
|
1090
|
+
const started = now();
|
|
1091
|
+
for (let attempt = 1;; attempt += 1) {
|
|
1092
|
+
await gap();
|
|
1093
|
+
let response;
|
|
1094
|
+
try {
|
|
1095
|
+
response = await doFetch(target, {
|
|
1096
|
+
method: request.method,
|
|
1097
|
+
headers: headersFor(request, current),
|
|
1098
|
+
...request.body === undefined ? {} : { body: JSON.stringify(request.body) },
|
|
1099
|
+
redirect: "manual",
|
|
1100
|
+
signal: AbortSignal.timeout(opts.timeoutMs)
|
|
1101
|
+
});
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
requests += 1;
|
|
1104
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1105
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
1106
|
+
log.warn(`${label} network error on attempt ${attempt}: ${reason}`);
|
|
1107
|
+
await sleep(backoffMs(attempt));
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
throw new KotasHttpError(0, `Falha de rede ao chamar o Kotas em ${label}: ${reason}`);
|
|
1111
|
+
}
|
|
1112
|
+
requests += 1;
|
|
1113
|
+
if (isTransient(response.status) && attempt < MAX_ATTEMPTS) {
|
|
1114
|
+
log.warn(`${label} HTTP ${response.status} on attempt ${attempt}; backing off`);
|
|
1115
|
+
await sleep(backoffMs(attempt));
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (isTransient(response.status)) {
|
|
1119
|
+
throw new RateLimitError(attempt, `O Kotas respondeu HTTP ${response.status} em ${label} ap\xF3s ${attempt} tentativas. Espere alguns minutos.`);
|
|
1120
|
+
}
|
|
1121
|
+
log.debug(`${label} ${response.status} ${now() - started}ms`);
|
|
1122
|
+
return { status: response.status, headers: response.headers, text: await response.text() };
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function doRefresh(current) {
|
|
1126
|
+
const response = await dispatch({
|
|
1127
|
+
method: "POST",
|
|
1128
|
+
path: REFRESH_PATH,
|
|
1129
|
+
headers: { role: "refresh-token" },
|
|
1130
|
+
body: { token: current.accessToken, refreshToken: current.refreshToken },
|
|
1131
|
+
label: "autenticacao/refresh-token"
|
|
1132
|
+
}, current);
|
|
1133
|
+
if (response.status !== 200) {
|
|
1134
|
+
markAuthDead(`O Kotas recusou o refresh token (HTTP ${response.status}).`);
|
|
1135
|
+
}
|
|
1136
|
+
const token = parseRefreshBody(response.text);
|
|
1137
|
+
if (!token)
|
|
1138
|
+
markAuthDead("O refresh do Kotas devolveu um corpo que n\xE3o \xE9 um token.");
|
|
1139
|
+
refreshes += 1;
|
|
1140
|
+
const next = { ...current, accessToken: token, savedAt: now() };
|
|
1141
|
+
session.save(next);
|
|
1142
|
+
jar = next;
|
|
1143
|
+
loadedMtime = session.mtimeMs();
|
|
1144
|
+
log.debug("access token refreshed");
|
|
1145
|
+
return next;
|
|
1146
|
+
}
|
|
1147
|
+
function refresh(current) {
|
|
1148
|
+
if (!refreshing) {
|
|
1149
|
+
refreshing = doRefresh(current).finally(() => {
|
|
1150
|
+
refreshing = null;
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return refreshing;
|
|
1154
|
+
}
|
|
1155
|
+
async function ensureToken() {
|
|
1156
|
+
const current = ensureSession();
|
|
1157
|
+
if (!isExpired(current.accessToken, now()))
|
|
1158
|
+
return current;
|
|
1159
|
+
return refresh(current);
|
|
1160
|
+
}
|
|
1161
|
+
async function send(request) {
|
|
1162
|
+
if (request.auth === "none")
|
|
1163
|
+
return serial(() => dispatch(request, jar));
|
|
1164
|
+
return serial(async () => {
|
|
1165
|
+
let current = await ensureToken();
|
|
1166
|
+
let response = await dispatch(request, current);
|
|
1167
|
+
if (response.status === 401) {
|
|
1168
|
+
current = await refresh(current);
|
|
1169
|
+
response = await dispatch(request, current);
|
|
1170
|
+
if (response.status === 401) {
|
|
1171
|
+
markAuthDead("O Kotas recusou a sess\xE3o mesmo depois de renovar o token.");
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return response;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
async function json(request) {
|
|
1178
|
+
const label = request.label ?? request.path;
|
|
1179
|
+
const response = await send(request);
|
|
1180
|
+
const parsed = parseJson(response.text);
|
|
1181
|
+
if (response.status >= 400) {
|
|
1182
|
+
throw new KotasApiError(response.status, messageOf(parsed) ?? response.text.slice(0, 200), label, STATUS_HINTS[response.status], parsed);
|
|
1183
|
+
}
|
|
1184
|
+
return parsed;
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
serial,
|
|
1188
|
+
send,
|
|
1189
|
+
get: (path, query, label) => json({ method: "GET", path, ...query ? { query } : {}, label: label ?? path }),
|
|
1190
|
+
post: (path, body, options = {}) => json({ method: "POST", path, body, label: path, ...options }),
|
|
1191
|
+
adopt(data) {
|
|
1192
|
+
jar = data;
|
|
1193
|
+
loadedMtime = session.mtimeMs();
|
|
1194
|
+
authDead = false;
|
|
1195
|
+
},
|
|
1196
|
+
resetSession() {
|
|
1197
|
+
jar = null;
|
|
1198
|
+
loadedMtime = null;
|
|
1199
|
+
authDead = false;
|
|
1200
|
+
},
|
|
1201
|
+
markAuthDead,
|
|
1202
|
+
state: () => ({ authDead, requests, refreshes, lastRequestAt })
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function parseRefreshBody(text) {
|
|
1206
|
+
const trimmed = text.trim();
|
|
1207
|
+
if (trimmed === "")
|
|
1208
|
+
return null;
|
|
1209
|
+
const parsed = parseJson(trimmed);
|
|
1210
|
+
if (typeof parsed === "string")
|
|
1211
|
+
return parsed.trim() || null;
|
|
1212
|
+
if (parsed && typeof parsed === "object") {
|
|
1213
|
+
const token = parsed.token;
|
|
1214
|
+
if (typeof token === "string" && token !== "")
|
|
1215
|
+
return token;
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
return /^[\w-]+\.[\w-]+\.[\w-]+$/.test(trimmed) ? trimmed : null;
|
|
1219
|
+
}
|
|
1220
|
+
function parseJson(text) {
|
|
1221
|
+
try {
|
|
1222
|
+
return JSON.parse(text);
|
|
1223
|
+
} catch {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function messageOf(parsed) {
|
|
1228
|
+
if (parsed && typeof parsed === "object") {
|
|
1229
|
+
const message = parsed.message;
|
|
1230
|
+
if (typeof message === "string" && message !== "")
|
|
1231
|
+
return message;
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
var BACKOFF_BASE_MS = 2000, BACKOFF_MAX_MS = 60000, MAX_ATTEMPTS = 4, REFRESH_PATH = "autenticacao/refresh-token", backoffMs = (attempt) => Math.min(BACKOFF_BASE_MS * 2 ** (attempt - 1), BACKOFF_MAX_MS), isTransient = (status) => status === 429 || status >= 500;
|
|
1236
|
+
var init_http = __esm(() => {
|
|
1237
|
+
init_errors();
|
|
1238
|
+
init_jwt();
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// src/core/logger.ts
|
|
1242
|
+
import { appendFileSync } from "fs";
|
|
1243
|
+
function redactSecrets(message, secrets) {
|
|
1244
|
+
let output = message;
|
|
1245
|
+
for (const secret of secrets) {
|
|
1246
|
+
if (secret.length >= MIN_SECRET_LENGTH)
|
|
1247
|
+
output = output.split(secret).join("***");
|
|
1248
|
+
}
|
|
1249
|
+
return output;
|
|
1250
|
+
}
|
|
1251
|
+
function createLogger(opts) {
|
|
1252
|
+
const source = opts.secrets ?? [];
|
|
1253
|
+
const secrets = () => typeof source === "function" ? source() : source;
|
|
1254
|
+
const emit = (level, message) => {
|
|
1255
|
+
const line = `[${level}] ${redactSecrets(message, secrets())}
|
|
1256
|
+
`;
|
|
1257
|
+
(opts.sink ?? ((text) => process.stderr.write(text)))(line);
|
|
1258
|
+
if (opts.logFile) {
|
|
1259
|
+
try {
|
|
1260
|
+
appendFileSync(opts.logFile, line);
|
|
1261
|
+
} catch {}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
return {
|
|
1265
|
+
debug: (message) => emit("debug", message),
|
|
1266
|
+
info: (message) => emit("info", message),
|
|
1267
|
+
warn: (message) => emit("warn", message),
|
|
1268
|
+
error: (message) => emit("error", message)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
var MIN_SECRET_LENGTH = 8;
|
|
1272
|
+
var init_logger = () => {};
|
|
1273
|
+
|
|
1274
|
+
// src/kotas/api.ts
|
|
1275
|
+
async function fetchPage(http, path, style, page, pageSize, extra = {}) {
|
|
1276
|
+
const size = clampPageSize(pageSize);
|
|
1277
|
+
const payload = await http.get(path, { page, pageSize: size, ...extra }, path);
|
|
1278
|
+
if (style.envelope) {
|
|
1279
|
+
if (!payload || Array.isArray(payload) || !Array.isArray(payload.itens)) {
|
|
1280
|
+
throw new ParseError(`${path} devolveu um envelope sem \`itens\`.`);
|
|
1281
|
+
}
|
|
1282
|
+
const total = typeof payload.totalItens === "number" ? payload.totalItens : null;
|
|
1283
|
+
const hasMore = payload.temProximaPagina ?? (typeof payload.totalPaginas === "number" ? page - style.startPage + 1 < payload.totalPaginas : payload.itens.length >= size);
|
|
1284
|
+
return { items: payload.itens, hasMore, total };
|
|
1285
|
+
}
|
|
1286
|
+
if (!Array.isArray(payload)) {
|
|
1287
|
+
throw new ParseError(`${path} devolveu ${typeof payload} onde era esperado um array.`);
|
|
1288
|
+
}
|
|
1289
|
+
return { items: payload, hasMore: payload.length >= size, total: null };
|
|
1290
|
+
}
|
|
1291
|
+
function createKotasApi(http) {
|
|
1292
|
+
return {
|
|
1293
|
+
version: () => http.get(ENDPOINTS.version, undefined, ENDPOINTS.version),
|
|
1294
|
+
listGroups: (page, pageSize = MAX_PAGE_SIZE) => fetchPage(http, ENDPOINTS.groups, PAGE_STYLE.groups, page, pageSize),
|
|
1295
|
+
getGroup: (id, as) => http.get(as === "admin" ? ENDPOINTS.groupAsAdmin(id) : ENDPOINTS.groupAsMember(id), undefined, as === "admin" ? "grupo/admin/{id}" : "grupo/membro/{id}"),
|
|
1296
|
+
listInvoices: (statusId, page, pageSize = MAX_PAGE_SIZE) => fetchPage(http, ENDPOINTS.invoices, PAGE_STYLE.invoices, page, pageSize, {
|
|
1297
|
+
statusId
|
|
1298
|
+
}),
|
|
1299
|
+
async getInvoiceItems(invoiceId) {
|
|
1300
|
+
const payload = await http.get(ENDPOINTS.invoiceStatement, { faturaId: invoiceId }, ENDPOINTS.invoiceStatement);
|
|
1301
|
+
if (Array.isArray(payload))
|
|
1302
|
+
return payload;
|
|
1303
|
+
const items = payload?.itens;
|
|
1304
|
+
return Array.isArray(items) ? items : [];
|
|
1305
|
+
},
|
|
1306
|
+
listCredits: (page, pageSize = MAX_PAGE_SIZE, ativos = false) => fetchPage(http, ENDPOINTS.credits, PAGE_STYLE.credits, page, pageSize, {
|
|
1307
|
+
ativos
|
|
1308
|
+
}),
|
|
1309
|
+
getBalance: () => http.get(ENDPOINTS.balance, undefined, ENDPOINTS.balance),
|
|
1310
|
+
getSavings: () => http.get(ENDPOINTS.savings, undefined, ENDPOINTS.savings),
|
|
1311
|
+
listPayouts: (page, pageSize = MAX_PAGE_SIZE) => fetchPage(http, ENDPOINTS.payouts, PAGE_STYLE.payouts, page, pageSize),
|
|
1312
|
+
async getPayoutStatement(groupId) {
|
|
1313
|
+
const payload = await http.get(ENDPOINTS.payoutStatement, { grupoId: groupId }, ENDPOINTS.payoutStatement);
|
|
1314
|
+
return Array.isArray(payload) ? payload : [];
|
|
1315
|
+
},
|
|
1316
|
+
async searchServices(query) {
|
|
1317
|
+
const payload = await http.get(ENDPOINTS.searchServices(query), undefined, "servico/planos/pesquisar/{q}");
|
|
1318
|
+
if (Array.isArray(payload))
|
|
1319
|
+
return payload;
|
|
1320
|
+
return payload ? [payload] : [];
|
|
1321
|
+
},
|
|
1322
|
+
getRaw: (path, query) => http.get(path, query, path)
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
var ENDPOINTS, PAGE_STYLE, clampPageSize = (size) => Math.max(1, Math.min(Math.trunc(size), MAX_PAGE_SIZE));
|
|
1326
|
+
var init_api = __esm(() => {
|
|
1327
|
+
init_config();
|
|
1328
|
+
init_errors();
|
|
1329
|
+
ENDPOINTS = {
|
|
1330
|
+
version: "api/versao",
|
|
1331
|
+
groups: "grupo/usuario/",
|
|
1332
|
+
groupAsMember: (id) => `grupo/membro/${id}`,
|
|
1333
|
+
groupAsAdmin: (id) => `grupo/admin/${id}`,
|
|
1334
|
+
invoices: "fatura/obterlista",
|
|
1335
|
+
invoiceStatement: "fatura/extrato",
|
|
1336
|
+
credits: "credito/obterlista",
|
|
1337
|
+
balance: "credito",
|
|
1338
|
+
savings: "credito/economia",
|
|
1339
|
+
payouts: "recebimento/obtergrupos",
|
|
1340
|
+
payoutStatement: "recebimento/extrato",
|
|
1341
|
+
searchServices: (query) => `servico/planos/pesquisar/${encodeURIComponent(query)}`
|
|
1342
|
+
};
|
|
1343
|
+
PAGE_STYLE = {
|
|
1344
|
+
groups: { startPage: 0, envelope: false },
|
|
1345
|
+
payouts: { startPage: 0, envelope: true },
|
|
1346
|
+
invoices: { startPage: 1, envelope: false },
|
|
1347
|
+
credits: { startPage: 1, envelope: false }
|
|
1348
|
+
};
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// src/session/store.ts
|
|
1352
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
1353
|
+
import {
|
|
1354
|
+
chmodSync as chmodSync2,
|
|
1355
|
+
existsSync as existsSync2,
|
|
1356
|
+
mkdirSync as mkdirSync2,
|
|
1357
|
+
readFileSync,
|
|
1358
|
+
renameSync,
|
|
1359
|
+
statSync,
|
|
1360
|
+
unlinkSync,
|
|
1361
|
+
writeFileSync
|
|
1362
|
+
} from "fs";
|
|
1363
|
+
import { dirname as dirname2 } from "path";
|
|
1364
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
1365
|
+
import { Value as Value2 } from "@sinclair/typebox/value";
|
|
1366
|
+
function encrypt(key, plaintext) {
|
|
1367
|
+
const iv = randomBytes(IV_BYTES);
|
|
1368
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
1369
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1370
|
+
return Buffer.concat([Buffer.from([FORMAT_VERSION]), iv, cipher.getAuthTag(), ciphertext]);
|
|
1371
|
+
}
|
|
1372
|
+
function decrypt(key, blob) {
|
|
1373
|
+
if (blob.length < HEADER_BYTES || blob[0] !== FORMAT_VERSION) {
|
|
1374
|
+
throw new SessionError(`session.enc corrompido ou em formato desconhecido. ${LOGIN_HINT}`);
|
|
1375
|
+
}
|
|
1376
|
+
const iv = blob.subarray(1, 1 + IV_BYTES);
|
|
1377
|
+
const tag = blob.subarray(1 + IV_BYTES, HEADER_BYTES);
|
|
1378
|
+
const ciphertext = blob.subarray(HEADER_BYTES);
|
|
1379
|
+
try {
|
|
1380
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
1381
|
+
decipher.setAuthTag(tag);
|
|
1382
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1383
|
+
} catch {
|
|
1384
|
+
throw new SessionError(`N\xE3o foi poss\xEDvel decifrar session.enc: a chave mudou (KOTAS_SESSION_KEY ou session.key). ${LOGIN_HINT}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function resolveKey(source, createIfMissing) {
|
|
1388
|
+
if (source.sessionKey)
|
|
1389
|
+
return Buffer.from(source.sessionKey, "base64");
|
|
1390
|
+
if (existsSync2(source.keyPath)) {
|
|
1391
|
+
const key2 = Buffer.from(readFileSync(source.keyPath, "utf8").trim(), "base64");
|
|
1392
|
+
if (key2.length !== SESSION_KEY_BYTES) {
|
|
1393
|
+
throw new SessionError(`${source.keyPath} inv\xE1lida: esperado base64 de ${SESSION_KEY_BYTES} bytes. Apague o arquivo e rode \`kotas login\`.`);
|
|
1394
|
+
}
|
|
1395
|
+
return key2;
|
|
1396
|
+
}
|
|
1397
|
+
if (!createIfMissing)
|
|
1398
|
+
return null;
|
|
1399
|
+
const key = randomBytes(SESSION_KEY_BYTES);
|
|
1400
|
+
mkdirSync2(dirname2(source.keyPath), { recursive: true, mode: 448 });
|
|
1401
|
+
writeFileSync(source.keyPath, `${key.toString("base64")}
|
|
1402
|
+
`, { mode: 384 });
|
|
1403
|
+
chmodSync2(source.keyPath, 384);
|
|
1404
|
+
return key;
|
|
1405
|
+
}
|
|
1406
|
+
function createSessionStore(config) {
|
|
1407
|
+
let secrets = [];
|
|
1408
|
+
return {
|
|
1409
|
+
paths: { sessionPath: config.sessionPath, keyPath: config.keyPath },
|
|
1410
|
+
load() {
|
|
1411
|
+
if (!existsSync2(config.sessionPath))
|
|
1412
|
+
return null;
|
|
1413
|
+
const key = resolveKey(config, false);
|
|
1414
|
+
if (!key) {
|
|
1415
|
+
throw new SessionError(`${config.sessionPath} existe, mas n\xE3o h\xE1 chave para decifr\xE1-lo (KOTAS_SESSION_KEY ou ${config.keyPath}). ${LOGIN_HINT}`);
|
|
1416
|
+
}
|
|
1417
|
+
const plaintext = decrypt(key, readFileSync(config.sessionPath));
|
|
1418
|
+
let parsed;
|
|
1419
|
+
try {
|
|
1420
|
+
parsed = JSON.parse(plaintext.toString("utf8"));
|
|
1421
|
+
} catch {
|
|
1422
|
+
throw new SessionError(`session.enc decifrado, mas com conte\xFAdo inv\xE1lido. ${LOGIN_HINT}`);
|
|
1423
|
+
}
|
|
1424
|
+
if (!Value2.Check(SessionSchema, parsed)) {
|
|
1425
|
+
throw new SessionError(`session.enc em vers\xE3o n\xE3o suportada. ${LOGIN_HINT}`);
|
|
1426
|
+
}
|
|
1427
|
+
secrets = secretsOf(parsed);
|
|
1428
|
+
return parsed;
|
|
1429
|
+
},
|
|
1430
|
+
save(data) {
|
|
1431
|
+
mkdirSync2(config.configDir, { recursive: true, mode: 448 });
|
|
1432
|
+
const key = resolveKey(config, true);
|
|
1433
|
+
const temp = `${config.sessionPath}.tmp`;
|
|
1434
|
+
writeFileSync(temp, encrypt(key, Buffer.from(JSON.stringify(data))), { mode: 384 });
|
|
1435
|
+
renameSync(temp, config.sessionPath);
|
|
1436
|
+
chmodSync2(config.sessionPath, 384);
|
|
1437
|
+
secrets = secretsOf(data);
|
|
1438
|
+
},
|
|
1439
|
+
clear() {
|
|
1440
|
+
if (existsSync2(config.sessionPath))
|
|
1441
|
+
unlinkSync(config.sessionPath);
|
|
1442
|
+
secrets = [];
|
|
1443
|
+
},
|
|
1444
|
+
mtimeMs() {
|
|
1445
|
+
try {
|
|
1446
|
+
return statSync(config.sessionPath).mtimeMs;
|
|
1447
|
+
} catch {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1451
|
+
peekSecrets: () => secrets
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
var FORMAT_VERSION = 1, IV_BYTES = 12, TAG_BYTES = 16, HEADER_BYTES, SessionSchema, secretsOf = (data) => [data.accessToken, data.refreshToken, data.hashDispositivo].filter((value) => value !== "");
|
|
1455
|
+
var init_store = __esm(() => {
|
|
1456
|
+
init_config();
|
|
1457
|
+
init_errors();
|
|
1458
|
+
HEADER_BYTES = 1 + IV_BYTES + TAG_BYTES;
|
|
1459
|
+
SessionSchema = Type2.Object({
|
|
1460
|
+
version: Type2.Literal(1),
|
|
1461
|
+
accessToken: Type2.String({ minLength: 1 }),
|
|
1462
|
+
refreshToken: Type2.String({ minLength: 1 }),
|
|
1463
|
+
hashDispositivo: Type2.String(),
|
|
1464
|
+
email: Type2.Optional(Type2.String()),
|
|
1465
|
+
savedAt: Type2.Number()
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// src/context.ts
|
|
1470
|
+
var exports_context = {};
|
|
1471
|
+
__export(exports_context, {
|
|
1472
|
+
createContext: () => createContext,
|
|
1473
|
+
contextFromEnv: () => contextFromEnv
|
|
1474
|
+
});
|
|
1475
|
+
function createContext(config, deps = {}) {
|
|
1476
|
+
const now = deps.now ?? (() => Date.now());
|
|
1477
|
+
const session = deps.session ?? createSessionStore(config);
|
|
1478
|
+
const log = deps.log ?? createLogger({
|
|
1479
|
+
...config.logFile ? { logFile: config.logFile } : {},
|
|
1480
|
+
secrets: () => session.peekSecrets()
|
|
1481
|
+
});
|
|
1482
|
+
const http = createHttp({
|
|
1483
|
+
session,
|
|
1484
|
+
baseUrl: config.baseUrl,
|
|
1485
|
+
apiToken: config.apiToken,
|
|
1486
|
+
appVersion: config.appVersion,
|
|
1487
|
+
minIntervalMs: config.minIntervalMs,
|
|
1488
|
+
jitterMs: config.jitterMs,
|
|
1489
|
+
timeoutMs: config.httpTimeoutMs,
|
|
1490
|
+
log
|
|
1491
|
+
}, {
|
|
1492
|
+
...deps.fetch ? { fetch: deps.fetch } : {},
|
|
1493
|
+
...deps.sleep ? { sleep: deps.sleep } : {},
|
|
1494
|
+
...deps.random ? { random: deps.random } : {},
|
|
1495
|
+
now
|
|
1496
|
+
});
|
|
1497
|
+
const api = createKotasApi(http);
|
|
1498
|
+
let db;
|
|
1499
|
+
let repo;
|
|
1500
|
+
const cache = () => {
|
|
1501
|
+
if (!repo) {
|
|
1502
|
+
db = deps.db ?? openCache(config.dbPath);
|
|
1503
|
+
repo = createCacheRepo(db, now);
|
|
1504
|
+
}
|
|
1505
|
+
return repo;
|
|
1506
|
+
};
|
|
1507
|
+
return {
|
|
1508
|
+
config,
|
|
1509
|
+
log,
|
|
1510
|
+
now,
|
|
1511
|
+
session,
|
|
1512
|
+
http,
|
|
1513
|
+
api,
|
|
1514
|
+
cache,
|
|
1515
|
+
dispose: () => {
|
|
1516
|
+
if (deps.db === undefined)
|
|
1517
|
+
db?.close();
|
|
1518
|
+
db = undefined;
|
|
1519
|
+
repo = undefined;
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
var contextFromEnv = () => createContext(loadConfig());
|
|
1524
|
+
var init_context = __esm(() => {
|
|
1525
|
+
init_db();
|
|
1526
|
+
init_repo();
|
|
1527
|
+
init_config();
|
|
1528
|
+
init_http();
|
|
1529
|
+
init_logger();
|
|
1530
|
+
init_api();
|
|
1531
|
+
init_store();
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// src/tools/define.ts
|
|
1535
|
+
import { Value as Value3 } from "@sinclair/typebox/value";
|
|
1536
|
+
function defineTool(tool) {
|
|
1537
|
+
return tool;
|
|
1538
|
+
}
|
|
1539
|
+
function withDefaults(tool, args, ctx) {
|
|
1540
|
+
if (!ctx.config.compact)
|
|
1541
|
+
return args;
|
|
1542
|
+
const properties = tool.input.properties;
|
|
1543
|
+
if (!properties || !("compact" in properties))
|
|
1544
|
+
return args;
|
|
1545
|
+
const record = args;
|
|
1546
|
+
if (record.compact !== undefined)
|
|
1547
|
+
return args;
|
|
1548
|
+
return { ...record, compact: true };
|
|
1549
|
+
}
|
|
1550
|
+
async function runTool(tool, rawArgs, ctx) {
|
|
1551
|
+
if (!Value3.Check(tool.input, rawArgs)) {
|
|
1552
|
+
const problems = [...Value3.Errors(tool.input, rawArgs)].map((error) => `${error.path || "/"}: ${error.message}`);
|
|
1553
|
+
throw new ToolInputError(tool.name, problems);
|
|
1554
|
+
}
|
|
1555
|
+
return tool.run(withDefaults(tool, rawArgs, ctx), ctx);
|
|
1556
|
+
}
|
|
1557
|
+
function compactObject(value) {
|
|
1558
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
1559
|
+
}
|
|
1560
|
+
var ToolInputError;
|
|
1561
|
+
var init_define = __esm(() => {
|
|
1562
|
+
ToolInputError = class ToolInputError extends Error {
|
|
1563
|
+
tool;
|
|
1564
|
+
problems;
|
|
1565
|
+
constructor(tool, problems) {
|
|
1566
|
+
super(`Argumentos inv\xE1lidos para ${tool}:
|
|
1567
|
+
${problems.map((p) => ` - ${p}`).join(`
|
|
1568
|
+
`)}`);
|
|
1569
|
+
this.tool = tool;
|
|
1570
|
+
this.problems = problems;
|
|
1571
|
+
this.name = "ToolInputError";
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
// src/domain/money.ts
|
|
1577
|
+
function toUnits(value) {
|
|
1578
|
+
if (value === null || value === undefined || value === "")
|
|
1579
|
+
return null;
|
|
1580
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1581
|
+
if (!Number.isFinite(parsed))
|
|
1582
|
+
return null;
|
|
1583
|
+
return Math.round(parsed * SCALE);
|
|
1584
|
+
}
|
|
1585
|
+
function pickUnits(precise, fallback) {
|
|
1586
|
+
return toUnits(precise) ?? toUnits(fallback);
|
|
1587
|
+
}
|
|
1588
|
+
function toDecimal(units) {
|
|
1589
|
+
return Math.round(units) / SCALE;
|
|
1590
|
+
}
|
|
1591
|
+
function money(units) {
|
|
1592
|
+
return units === null || units === undefined ? null : { amount: toDecimal(units), currency: "BRL" };
|
|
1593
|
+
}
|
|
1594
|
+
function sumUnits(values) {
|
|
1595
|
+
let total = 0;
|
|
1596
|
+
let seen = false;
|
|
1597
|
+
for (const value of values) {
|
|
1598
|
+
if (value === null || value === undefined)
|
|
1599
|
+
continue;
|
|
1600
|
+
total += value;
|
|
1601
|
+
seen = true;
|
|
1602
|
+
}
|
|
1603
|
+
return seen ? total : null;
|
|
1604
|
+
}
|
|
1605
|
+
var SCALE = 1e4;
|
|
1606
|
+
|
|
1607
|
+
// src/cache/analytics.ts
|
|
1608
|
+
function spendingSummary(cache, options) {
|
|
1609
|
+
const expression = GROUP_EXPRESSION[options.by];
|
|
1610
|
+
const clauses = [`${INVOICE_DATE} IS NOT NULL`];
|
|
1611
|
+
const values = [];
|
|
1612
|
+
if (!options.includeUnpaid)
|
|
1613
|
+
clauses.push(`status_id = ${PAID_STATUS_ID}`);
|
|
1614
|
+
if (options.from) {
|
|
1615
|
+
clauses.push(`${INVOICE_DATE} >= ?`);
|
|
1616
|
+
values.push(options.from);
|
|
1617
|
+
}
|
|
1618
|
+
if (options.to) {
|
|
1619
|
+
clauses.push(`${INVOICE_DATE} <= ?`);
|
|
1620
|
+
values.push(options.to);
|
|
1621
|
+
}
|
|
1622
|
+
const where = `WHERE ${clauses.join(" AND ")}`;
|
|
1623
|
+
const rows = cache.query(`SELECT ${expression} AS key, SUM(total_units) AS total, COUNT(*) AS invoices
|
|
1624
|
+
FROM invoices ${where}
|
|
1625
|
+
GROUP BY key
|
|
1626
|
+
ORDER BY ${options.by === "product" || options.by === "status" ? "total DESC" : "key DESC"}`, ...values);
|
|
1627
|
+
let grand = 0;
|
|
1628
|
+
let count = 0;
|
|
1629
|
+
const out = rows.map((row) => {
|
|
1630
|
+
grand += row.total ?? 0;
|
|
1631
|
+
count += row.invoices;
|
|
1632
|
+
const key = row.key ?? "desconhecido";
|
|
1633
|
+
return { key, label: key, total: money(row.total), invoices: row.invoices };
|
|
1634
|
+
});
|
|
1635
|
+
return { rows: out, total: count > 0 ? money(grand) : null, invoices: count };
|
|
1636
|
+
}
|
|
1637
|
+
function purchaseHistory(cache, options = {}) {
|
|
1638
|
+
const rows = cache.query(`SELECT group_id,
|
|
1639
|
+
COALESCE(product, description) AS product,
|
|
1640
|
+
MIN(${INVOICE_DATE}) AS first,
|
|
1641
|
+
MAX(${INVOICE_DATE}) AS last,
|
|
1642
|
+
COUNT(DISTINCT substr(${INVOICE_DATE}, 1, 7)) AS months,
|
|
1643
|
+
SUM(total_units) AS total
|
|
1644
|
+
FROM invoices
|
|
1645
|
+
WHERE status_id = ${PAID_STATUS_ID} AND ${INVOICE_DATE} IS NOT NULL
|
|
1646
|
+
GROUP BY group_id, product
|
|
1647
|
+
ORDER BY last DESC`);
|
|
1648
|
+
const live = new Map(cache.listGroups({ limit: 1000 }).map((group) => [group.id, group.status]));
|
|
1649
|
+
const deposits = creditTotalsByGroup(cache, 96);
|
|
1650
|
+
const transfers = creditTotalsByGroup(cache, 97);
|
|
1651
|
+
const entries = [];
|
|
1652
|
+
const covered = new Set;
|
|
1653
|
+
for (const row of rows) {
|
|
1654
|
+
if (row.group_id === null && !options.includeCreditPurchases)
|
|
1655
|
+
continue;
|
|
1656
|
+
if (row.group_id !== null)
|
|
1657
|
+
covered.add(row.group_id);
|
|
1658
|
+
const status = row.group_id !== null ? live.get(row.group_id) ?? null : null;
|
|
1659
|
+
const deposit = row.group_id !== null ? deposits.get(row.group_id) : undefined;
|
|
1660
|
+
const received = row.group_id !== null ? transfers.get(row.group_id) : undefined;
|
|
1661
|
+
entries.push({
|
|
1662
|
+
groupId: row.group_id,
|
|
1663
|
+
product: row.product ?? "(sem descri\xE7\xE3o)",
|
|
1664
|
+
firstInvoice: row.first,
|
|
1665
|
+
lastInvoice: row.last,
|
|
1666
|
+
monthsPaid: row.months,
|
|
1667
|
+
totalPaid: money(row.total),
|
|
1668
|
+
activeToday: row.group_id !== null && live.has(row.group_id),
|
|
1669
|
+
status,
|
|
1670
|
+
...deposit === undefined ? {} : { deposit: money(deposit) },
|
|
1671
|
+
...received === undefined ? {} : { receivedAsAdmin: money(received) }
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
for (const group of cache.listGroups({ limit: 1000 })) {
|
|
1675
|
+
if (covered.has(group.id))
|
|
1676
|
+
continue;
|
|
1677
|
+
const received = transfers.get(group.id);
|
|
1678
|
+
const deposit = deposits.get(group.id);
|
|
1679
|
+
entries.push({
|
|
1680
|
+
groupId: group.id,
|
|
1681
|
+
product: group.service ?? group.name,
|
|
1682
|
+
firstInvoice: group.joined_at,
|
|
1683
|
+
lastInvoice: null,
|
|
1684
|
+
monthsPaid: 0,
|
|
1685
|
+
totalPaid: null,
|
|
1686
|
+
activeToday: true,
|
|
1687
|
+
status: group.status,
|
|
1688
|
+
role: group.is_administrator === 1 ? "administrador" : "membro",
|
|
1689
|
+
...deposit === undefined ? {} : { deposit: money(deposit) },
|
|
1690
|
+
...received === undefined ? {} : { receivedAsAdmin: money(received) }
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
return entries;
|
|
1694
|
+
}
|
|
1695
|
+
function creditTotalsByGroup(cache, typeId) {
|
|
1696
|
+
const rows = cache.query("SELECT group_id, SUM(available_units) AS total FROM credits WHERE type_id = ? GROUP BY group_id", typeId);
|
|
1697
|
+
const totals = new Map;
|
|
1698
|
+
for (const row of rows) {
|
|
1699
|
+
if (row.group_id !== null && row.total !== null)
|
|
1700
|
+
totals.set(row.group_id, row.total);
|
|
1701
|
+
}
|
|
1702
|
+
return totals;
|
|
1703
|
+
}
|
|
1704
|
+
var GROUP_EXPRESSION;
|
|
1705
|
+
var init_analytics = __esm(() => {
|
|
1706
|
+
init_enums();
|
|
1707
|
+
init_repo();
|
|
1708
|
+
GROUP_EXPRESSION = {
|
|
1709
|
+
month: `substr(${INVOICE_DATE}, 1, 7)`,
|
|
1710
|
+
year: `substr(${INVOICE_DATE}, 1, 4)`,
|
|
1711
|
+
product: "COALESCE(product, description)",
|
|
1712
|
+
status: "COALESCE(status, 'desconhecido')"
|
|
1713
|
+
};
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
// src/tools/fields.ts
|
|
1717
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
1718
|
+
var compactField, dayField = (description) => Type3.Optional(Type3.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}$", description })), limitField = (max, fallback) => Type3.Optional(Type3.Integer({ minimum: 1, maximum: max, description: `M\xE1ximo de itens (default ${fallback})` })), offsetField, invoiceIdField, groupIdField, invoiceStatusField, INVOICE_STATUS_BY_NAME;
|
|
1719
|
+
var init_fields = __esm(() => {
|
|
1720
|
+
compactField = Type3.Optional(Type3.Boolean({
|
|
1721
|
+
description: "Devolve apenas os campos essenciais, para economizar contexto (default KOTAS_COMPACT)"
|
|
1722
|
+
}));
|
|
1723
|
+
offsetField = Type3.Optional(Type3.Integer({ minimum: 0, description: "Itens a pular (pagina\xE7\xE3o)" }));
|
|
1724
|
+
invoiceIdField = Type3.Integer({
|
|
1725
|
+
minimum: 1,
|
|
1726
|
+
description: "Id da fatura no Kotas, como aparece em `list_invoices`"
|
|
1727
|
+
});
|
|
1728
|
+
groupIdField = Type3.Integer({
|
|
1729
|
+
minimum: 1,
|
|
1730
|
+
description: "Id do grupo no Kotas, como aparece em `list_subscriptions` ou na descri\xE7\xE3o da fatura"
|
|
1731
|
+
});
|
|
1732
|
+
invoiceStatusField = Type3.Optional(Type3.Union([
|
|
1733
|
+
Type3.Literal("pago"),
|
|
1734
|
+
Type3.Literal("pendente"),
|
|
1735
|
+
Type3.Literal("cancelado"),
|
|
1736
|
+
Type3.Literal("atraso"),
|
|
1737
|
+
Type3.Literal("estornado")
|
|
1738
|
+
], { description: "Filtra por situa\xE7\xE3o da fatura" }));
|
|
1739
|
+
INVOICE_STATUS_BY_NAME = {
|
|
1740
|
+
pendente: 11,
|
|
1741
|
+
pago: 13,
|
|
1742
|
+
cancelado: 19,
|
|
1743
|
+
atraso: 63,
|
|
1744
|
+
estornado: 67
|
|
1745
|
+
};
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// src/tools/analytics.ts
|
|
1749
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
1750
|
+
var purchaseHistory2, spendingSummary2;
|
|
1751
|
+
var init_analytics2 = __esm(() => {
|
|
1752
|
+
init_analytics();
|
|
1753
|
+
init_define();
|
|
1754
|
+
init_fields();
|
|
1755
|
+
purchaseHistory2 = defineTool({
|
|
1756
|
+
name: "purchase_history",
|
|
1757
|
+
description: "Reconstr\xF3i a linha do tempo de TODAS as assinaturas j\xE1 pagas, inclusive as encerradas, a partir " + "das faturas \u2014 que \xE9 a \xFAnica fonte que sobra, porque um grupo cancelado some da API. Para cada " + "produto devolve a primeira e a \xFAltima fatura, quantos meses foram pagos, o total pago, se ainda " + "est\xE1 ativo, a cau\xE7\xE3o retida e o que foi recebido como administrador. Grupos que o usu\xE1rio " + "ADMINISTRA aparecem com `monthsPaid: 0` e `totalPaid: null`: neles n\xE3o h\xE1 fatura, o administrador " + "paga o servi\xE7o por fora e recebe o rateio dos membros. N\xE3o usa a rede.",
|
|
1758
|
+
readOnly: true,
|
|
1759
|
+
input: Type4.Object({
|
|
1760
|
+
include_credit_purchases: Type4.Optional(Type4.Boolean({
|
|
1761
|
+
description: "Inclui as compras de cr\xE9dito avulso, que n\xE3o pertencem a nenhum grupo (default false)"
|
|
1762
|
+
})),
|
|
1763
|
+
active_only: Type4.Optional(Type4.Boolean({ description: "S\xF3 as assinaturas que ainda existem hoje (default false)" }))
|
|
1764
|
+
}),
|
|
1765
|
+
run: (args, ctx) => {
|
|
1766
|
+
const cache = ctx.cache();
|
|
1767
|
+
const all = purchaseHistory(cache, {
|
|
1768
|
+
includeCreditPurchases: args.include_credit_purchases === true
|
|
1769
|
+
});
|
|
1770
|
+
const entries = args.active_only ? all.filter((entry) => entry.activeToday) : all;
|
|
1771
|
+
return compactObject({
|
|
1772
|
+
total: entries.length,
|
|
1773
|
+
active: all.filter((entry) => entry.activeToday).length,
|
|
1774
|
+
ended: all.filter((entry) => !entry.activeToday).length,
|
|
1775
|
+
history: entries,
|
|
1776
|
+
...all.length === 0 ? { note: "O cache est\xE1 vazio. Rode `sync` antes de perguntar." } : {}
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
spendingSummary2 = defineTool({
|
|
1781
|
+
name: "spending_summary",
|
|
1782
|
+
description: "Soma os gastos no Kotas agrupados por m\xEAs, ano, produto ou situa\xE7\xE3o. Conta apenas faturas PAGAS " + "por padr\xE3o: cancelada e estornada n\xE3o s\xE3o dinheiro que saiu. N\xE3o usa a rede. A data usada \xE9 a do " + "pagamento e, na falta dela, a do vencimento.",
|
|
1783
|
+
readOnly: true,
|
|
1784
|
+
input: Type4.Object({
|
|
1785
|
+
by: Type4.Union([Type4.Literal("month"), Type4.Literal("year"), Type4.Literal("product"), Type4.Literal("status")], { description: "Como agrupar" }),
|
|
1786
|
+
from: dayField("Data inicial (YYYY-MM-DD)"),
|
|
1787
|
+
to: dayField("Data final (YYYY-MM-DD)"),
|
|
1788
|
+
include_unpaid: Type4.Optional(Type4.Boolean({
|
|
1789
|
+
description: "Inclui faturas n\xE3o pagas \u2014 s\xF3 faz sentido com by=status (default false)"
|
|
1790
|
+
}))
|
|
1791
|
+
}),
|
|
1792
|
+
run: (args, ctx) => {
|
|
1793
|
+
const result = spendingSummary(ctx.cache(), compactObject({
|
|
1794
|
+
by: args.by,
|
|
1795
|
+
from: args.from,
|
|
1796
|
+
to: args.to,
|
|
1797
|
+
includeUnpaid: args.include_unpaid === true
|
|
1798
|
+
}));
|
|
1799
|
+
return compactObject({
|
|
1800
|
+
by: args.by,
|
|
1801
|
+
paidOnly: args.include_unpaid !== true,
|
|
1802
|
+
total: result.total,
|
|
1803
|
+
invoices: result.invoices,
|
|
1804
|
+
rows: result.rows,
|
|
1805
|
+
...result.invoices === 0 ? { note: "Nenhuma fatura no per\xEDodo. Rode `sync` se o cache estiver vazio." } : {}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
// src/tools/auth.ts
|
|
1812
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
1813
|
+
function cacheFacts(ctx) {
|
|
1814
|
+
try {
|
|
1815
|
+
const stats = ctx.cache().stats();
|
|
1816
|
+
return {
|
|
1817
|
+
cache: {
|
|
1818
|
+
invoices: stats.invoices,
|
|
1819
|
+
groups: stats.groups,
|
|
1820
|
+
credits: stats.credits,
|
|
1821
|
+
payouts: stats.payouts,
|
|
1822
|
+
pendingDetails: stats.pendingDetails,
|
|
1823
|
+
lastSyncAt: stats.lastSyncAt,
|
|
1824
|
+
lastFullSyncAt: stats.lastFullSyncAt
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
} catch {
|
|
1828
|
+
return { cache: null };
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
var authStatus;
|
|
1832
|
+
var init_auth = __esm(() => {
|
|
1833
|
+
init_errors();
|
|
1834
|
+
init_jwt();
|
|
1835
|
+
init_define();
|
|
1836
|
+
authStatus = defineTool({
|
|
1837
|
+
name: "auth_status",
|
|
1838
|
+
description: "Diz se h\xE1 uma sess\xE3o do Kotas salva, de quem ela \xE9 e at\xE9 quando o token vale, e o que j\xE1 est\xE1 no " + "cache local. N\xE3o usa a rede por padr\xE3o. Com verify=true gasta 1 requisi\xE7\xE3o para confirmar que o " + "Kotas ainda aceita a sess\xE3o (renovando o token se preciso). Comece por aqui quando outra tool " + "reclamar de sess\xE3o. Nunca devolve o valor de um token.",
|
|
1839
|
+
readOnly: true,
|
|
1840
|
+
input: Type5.Object({
|
|
1841
|
+
verify: Type5.Optional(Type5.Boolean({
|
|
1842
|
+
description: "Tamb\xE9m faz 1 chamada ao Kotas para confirmar que a sess\xE3o \xE9 aceita"
|
|
1843
|
+
}))
|
|
1844
|
+
}),
|
|
1845
|
+
run: async (args, ctx) => {
|
|
1846
|
+
const { session, http, config } = ctx;
|
|
1847
|
+
let data = null;
|
|
1848
|
+
let loadError;
|
|
1849
|
+
try {
|
|
1850
|
+
data = session.load();
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
loadError = error instanceof Error ? error.message : String(error);
|
|
1853
|
+
}
|
|
1854
|
+
const state = http.state();
|
|
1855
|
+
const base = {
|
|
1856
|
+
loggedIn: data !== null,
|
|
1857
|
+
sessionFile: config.sessionPath,
|
|
1858
|
+
savedAt: data ? new Date(data.savedAt).toISOString() : null,
|
|
1859
|
+
deviceBound: Boolean(data?.hashDispositivo),
|
|
1860
|
+
refreshes: state.refreshes,
|
|
1861
|
+
...cacheFacts(ctx)
|
|
1862
|
+
};
|
|
1863
|
+
if (loadError)
|
|
1864
|
+
return compactObject({ ...base, loggedIn: false, error: loadError });
|
|
1865
|
+
if (data === null) {
|
|
1866
|
+
return compactObject({
|
|
1867
|
+
...base,
|
|
1868
|
+
hint: "Nenhuma sess\xE3o salva. Rode `kotas login` ou `kotas login --from-browser chrome`."
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
let profile = {};
|
|
1872
|
+
let expiry = null;
|
|
1873
|
+
let expired;
|
|
1874
|
+
try {
|
|
1875
|
+
profile = profileFrom(decodeJwt(data.accessToken));
|
|
1876
|
+
const at = expiresAt(data.accessToken);
|
|
1877
|
+
expiry = at === null ? null : new Date(at).toISOString();
|
|
1878
|
+
expired = isExpired(data.accessToken, ctx.now(), 0);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
return compactObject({
|
|
1881
|
+
...base,
|
|
1882
|
+
loggedIn: false,
|
|
1883
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const withProfile = compactObject({
|
|
1887
|
+
...base,
|
|
1888
|
+
email: data.email ?? profile.email,
|
|
1889
|
+
profile: compactObject(profile),
|
|
1890
|
+
accessTokenExpiresAt: expiry,
|
|
1891
|
+
accessTokenExpired: expired
|
|
1892
|
+
});
|
|
1893
|
+
if (!args.verify)
|
|
1894
|
+
return withProfile;
|
|
1895
|
+
try {
|
|
1896
|
+
const balance = await ctx.api.getBalance();
|
|
1897
|
+
return compactObject({
|
|
1898
|
+
...withProfile,
|
|
1899
|
+
verified: true,
|
|
1900
|
+
refreshes: http.state().refreshes,
|
|
1901
|
+
balanceReachable: balance !== null && typeof balance === "object"
|
|
1902
|
+
});
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
if (error instanceof KotasAuthError) {
|
|
1905
|
+
return compactObject({ ...withProfile, loggedIn: false, verified: false, error: error.message });
|
|
1906
|
+
}
|
|
1907
|
+
throw error;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// src/cache/rows.ts
|
|
1914
|
+
function invoiceOut(row, compact = false) {
|
|
1915
|
+
const base = {
|
|
1916
|
+
id: row.id,
|
|
1917
|
+
description: row.description,
|
|
1918
|
+
product: row.product,
|
|
1919
|
+
groupId: row.group_id,
|
|
1920
|
+
status: row.status,
|
|
1921
|
+
statusId: row.status_id,
|
|
1922
|
+
type: row.type,
|
|
1923
|
+
overdue: yes(row.overdue),
|
|
1924
|
+
total: money(row.total_units),
|
|
1925
|
+
dueDate: row.due_date,
|
|
1926
|
+
paidDate: row.paid_date
|
|
1927
|
+
};
|
|
1928
|
+
if (compact)
|
|
1929
|
+
return base;
|
|
1930
|
+
return {
|
|
1931
|
+
...base,
|
|
1932
|
+
amount: money(row.amount_units),
|
|
1933
|
+
interest: money(row.interest_units),
|
|
1934
|
+
discount: money(row.discount_units),
|
|
1935
|
+
fee: money(row.fee_units),
|
|
1936
|
+
itemCount: row.item_count,
|
|
1937
|
+
...row.detail_error ? { detailError: row.detail_error } : {}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
function invoiceItemOut(row) {
|
|
1941
|
+
return {
|
|
1942
|
+
position: row.position,
|
|
1943
|
+
description: row.description,
|
|
1944
|
+
quantity: row.quantity,
|
|
1945
|
+
amount: money(row.amount_units)
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
function groupOut(row, compact = false) {
|
|
1949
|
+
const base = {
|
|
1950
|
+
id: row.id,
|
|
1951
|
+
name: row.name,
|
|
1952
|
+
service: row.service,
|
|
1953
|
+
plan: row.plan,
|
|
1954
|
+
status: row.status,
|
|
1955
|
+
isAdministrator: yes(row.is_administrator),
|
|
1956
|
+
amount: money(row.amount_units),
|
|
1957
|
+
slots: { total: row.slots_total, taken: row.slots_taken, free: row.slots_free }
|
|
1958
|
+
};
|
|
1959
|
+
if (compact)
|
|
1960
|
+
return base;
|
|
1961
|
+
return {
|
|
1962
|
+
...base,
|
|
1963
|
+
category: row.category,
|
|
1964
|
+
adminName: row.admin_name,
|
|
1965
|
+
serviceTotal: money(row.service_total_units),
|
|
1966
|
+
amountWithoutFee: money(row.amount_without_fee_units),
|
|
1967
|
+
fee: money(row.fee_units),
|
|
1968
|
+
loyaltyMonths: row.loyalty_months,
|
|
1969
|
+
loyaltyEndsAt: row.loyalty_ends_at,
|
|
1970
|
+
autoRenew: row.auto_renew === null ? null : yes(row.auto_renew),
|
|
1971
|
+
accessMethod: row.access_method,
|
|
1972
|
+
joinedAt: row.joined_at,
|
|
1973
|
+
cancelledAt: row.cancelled_at,
|
|
1974
|
+
scheduledCancellation: yes(row.scheduled_cancellation),
|
|
1975
|
+
waitingList: yes(row.waiting_list),
|
|
1976
|
+
private: yes(row.private),
|
|
1977
|
+
...row.detail_error ? { detailError: row.detail_error } : {}
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function participantOut(row) {
|
|
1981
|
+
return {
|
|
1982
|
+
name: row.name,
|
|
1983
|
+
amount: money(row.amount_units),
|
|
1984
|
+
slots: row.slots,
|
|
1985
|
+
joinedAt: row.joined_at,
|
|
1986
|
+
accessSentAt: row.access_sent_at,
|
|
1987
|
+
leftAt: row.left_at,
|
|
1988
|
+
waitingList: yes(row.waiting_list)
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function creditOut(row) {
|
|
1992
|
+
return {
|
|
1993
|
+
id: row.id,
|
|
1994
|
+
description: row.description,
|
|
1995
|
+
status: row.status,
|
|
1996
|
+
type: row.type,
|
|
1997
|
+
meaning: row.type_label,
|
|
1998
|
+
amount: money(row.amount_units),
|
|
1999
|
+
available: money(row.available_units),
|
|
2000
|
+
creditedAt: row.credited_at,
|
|
2001
|
+
invoiceId: row.invoice_id,
|
|
2002
|
+
groupId: row.group_id,
|
|
2003
|
+
refundable: yes(row.refundable)
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
function payoutOut(row) {
|
|
2007
|
+
return {
|
|
2008
|
+
groupId: row.group_id,
|
|
2009
|
+
description: row.description,
|
|
2010
|
+
amount: money(row.amount_units),
|
|
2011
|
+
memberShare: money(row.member_share_units),
|
|
2012
|
+
status: row.status,
|
|
2013
|
+
nextPaymentDate: row.next_payment_date
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
function payoutEntryOut(row) {
|
|
2017
|
+
return {
|
|
2018
|
+
id: row.entry_id,
|
|
2019
|
+
description: row.description,
|
|
2020
|
+
participant: row.participant,
|
|
2021
|
+
amount: money(row.amount_units),
|
|
2022
|
+
status: row.status,
|
|
2023
|
+
date: row.date
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
var yes = (value) => value === 1;
|
|
2027
|
+
var init_rows = () => {};
|
|
2028
|
+
|
|
2029
|
+
// src/domain/dates.ts
|
|
2030
|
+
function toDay(value) {
|
|
2031
|
+
if (typeof value !== "string" || value === "")
|
|
2032
|
+
return null;
|
|
2033
|
+
const isoMatch = ISO_DAY.exec(value);
|
|
2034
|
+
if (isoMatch)
|
|
2035
|
+
return `${isoMatch[1]}-${isoMatch[2]}-${isoMatch[3]}`;
|
|
2036
|
+
const brMatch = BR_DAY.exec(value.trim());
|
|
2037
|
+
if (brMatch)
|
|
2038
|
+
return `${brMatch[3]}-${brMatch[2]}-${brMatch[1]}`;
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
function dayFromEpochMs(ms) {
|
|
2042
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
2043
|
+
}
|
|
2044
|
+
var ISO_DAY, BR_DAY;
|
|
2045
|
+
var init_dates = __esm(() => {
|
|
2046
|
+
ISO_DAY = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
2047
|
+
BR_DAY = /^(\d{2})\/(\d{2})\/(\d{4})$/;
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
// src/domain/normalize.ts
|
|
2051
|
+
function parseSubject(description) {
|
|
2052
|
+
const match = INVOICE_SUBJECT.exec(description.trim());
|
|
2053
|
+
if (!match?.groups)
|
|
2054
|
+
return { product: null, groupId: null };
|
|
2055
|
+
const groupId = Number(match.groups.groupId);
|
|
2056
|
+
return {
|
|
2057
|
+
product: match.groups.product?.trim() || null,
|
|
2058
|
+
groupId: Number.isSafeInteger(groupId) ? groupId : null
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
function groupIdFrom(description) {
|
|
2062
|
+
const match = GROUP_IN_TEXT.exec(description);
|
|
2063
|
+
if (!match?.[1])
|
|
2064
|
+
return null;
|
|
2065
|
+
const id = Number(match[1]);
|
|
2066
|
+
return Number.isSafeInteger(id) ? id : null;
|
|
2067
|
+
}
|
|
2068
|
+
function normalizeInvoice(raw) {
|
|
2069
|
+
const id = num(raw.id);
|
|
2070
|
+
if (id === null)
|
|
2071
|
+
return null;
|
|
2072
|
+
const description = str(raw.descricao) ?? "";
|
|
2073
|
+
const statusId = num(raw.statusId);
|
|
2074
|
+
const typeId = num(raw.tipoId);
|
|
2075
|
+
return {
|
|
2076
|
+
id,
|
|
2077
|
+
description,
|
|
2078
|
+
...parseSubject(description),
|
|
2079
|
+
statusId,
|
|
2080
|
+
status: str(raw.statusDescricao) ?? invoiceStatusOf(statusId),
|
|
2081
|
+
typeId,
|
|
2082
|
+
type: str(raw.tipoDescricao) ?? labelOf(INVOICE_TYPE, typeId),
|
|
2083
|
+
overdue: bool(raw.vencida),
|
|
2084
|
+
amount: pickUnits(raw.valor4Casas, raw.valor),
|
|
2085
|
+
interest: pickUnits(raw.valorJuros4Casas, raw.valorJuros),
|
|
2086
|
+
surcharge: pickUnits(raw.valorReajuste4Casas, raw.valorReajuste),
|
|
2087
|
+
discount: pickUnits(raw.valorDesconto4Casas, raw.valorDesconto),
|
|
2088
|
+
fee: toUnits(raw.valorTaxa),
|
|
2089
|
+
total: pickUnits(raw.valorTotal4Casas, raw.valorTotal) ?? pickUnits(raw.valor4Casas, raw.valor),
|
|
2090
|
+
dueDate: toDay(raw.dataVencimento) ?? toDay(raw.dataVencimentoFormatada),
|
|
2091
|
+
paidDate: toDay(raw.dataPagamento) ?? toDay(raw.dataPagamentoFormatada),
|
|
2092
|
+
isGroupSubscription: bool(raw.ehInscricaoEmGrupo),
|
|
2093
|
+
gatewayId: num(raw.idGatewayPagamento)
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
function normalizeInvoiceItems(invoiceId, raws) {
|
|
2097
|
+
return raws.map((raw, position) => ({
|
|
2098
|
+
invoiceId,
|
|
2099
|
+
position,
|
|
2100
|
+
id: num(raw.id),
|
|
2101
|
+
description: str(raw.descricao) ?? "",
|
|
2102
|
+
quantity: num(raw.quantidade) ?? 1,
|
|
2103
|
+
amount: pickUnits(raw.valor4Casas, raw.valor)
|
|
2104
|
+
}));
|
|
2105
|
+
}
|
|
2106
|
+
function normalizeGroup(raw) {
|
|
2107
|
+
const id = num(raw.id);
|
|
2108
|
+
if (id === null)
|
|
2109
|
+
return null;
|
|
2110
|
+
const detail = raw;
|
|
2111
|
+
const statusId = num(raw.statusId);
|
|
2112
|
+
const slotsTotal = num(raw.kotasTotais);
|
|
2113
|
+
const slotsTaken = num(raw.kotasAssinadas);
|
|
2114
|
+
return {
|
|
2115
|
+
id,
|
|
2116
|
+
name: str(raw.nome) ?? str(raw.servico) ?? `Grupo #${id}`,
|
|
2117
|
+
service: str(raw.servico),
|
|
2118
|
+
serviceId: num(raw.servicoId),
|
|
2119
|
+
plan: str(detail.plano),
|
|
2120
|
+
categoryId: num(raw.idCategoria),
|
|
2121
|
+
category: str(detail.categoriaDescricao),
|
|
2122
|
+
statusId,
|
|
2123
|
+
status: str(raw.status) ?? groupStatusOf(statusId),
|
|
2124
|
+
isAdministrator: bool(raw.inAdministrador),
|
|
2125
|
+
adminName: str(raw.adminNome),
|
|
2126
|
+
amount: pickUnits(raw.valor4Casas, raw.valor),
|
|
2127
|
+
serviceTotal: pickUnits(detail.valorTotalServico4Casas, detail.valorTotalServico),
|
|
2128
|
+
amountWithoutFee: toUnits(detail.valorSemTaxa),
|
|
2129
|
+
fee: toUnits(detail.valorTaxa),
|
|
2130
|
+
slotsTotal,
|
|
2131
|
+
slotsTaken,
|
|
2132
|
+
slotsFree: num(raw.kotasDisponiveis) ?? (slotsTotal !== null && slotsTaken !== null ? slotsTotal - slotsTaken : null),
|
|
2133
|
+
loyaltyMonths: num(raw.fidelidade),
|
|
2134
|
+
loyaltyEndsAt: toDay(detail.dataTerminoFidelidade),
|
|
2135
|
+
autoRenew: detail.renovacaoAutomatica === undefined ? null : bool(detail.renovacaoAutomatica),
|
|
2136
|
+
accessMethod: accessMethodOf(detail.formaAcessoId),
|
|
2137
|
+
joinedAt: toDay(raw.dataCadastro),
|
|
2138
|
+
cancelledAt: toDay(detail.dataCancelamento),
|
|
2139
|
+
scheduledCancellation: bool(raw.inCancelamentoAgendado),
|
|
2140
|
+
waitingList: bool(raw.inFilaEspera),
|
|
2141
|
+
private: bool(raw.privado)
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function normalizeParticipants(groupId, raw) {
|
|
2145
|
+
const list = Array.isArray(raw.participantes) ? raw.participantes : [];
|
|
2146
|
+
return list.map((participant, position) => ({
|
|
2147
|
+
groupId,
|
|
2148
|
+
position,
|
|
2149
|
+
name: str(participant.nome),
|
|
2150
|
+
amount: pickUnits(participant.valor4Casas, participant.valor),
|
|
2151
|
+
slots: num(participant.qtdKotas),
|
|
2152
|
+
joinedAt: toDay(participant.dataInscricao),
|
|
2153
|
+
accessSentAt: toDay(participant.dataEnvioAcesso),
|
|
2154
|
+
leftAt: toDay(participant.dataDescadastro),
|
|
2155
|
+
waitingList: bool(participant.inFilaEspera)
|
|
2156
|
+
}));
|
|
2157
|
+
}
|
|
2158
|
+
function normalizeCredit(raw) {
|
|
2159
|
+
const id = num(raw.id);
|
|
2160
|
+
if (id === null)
|
|
2161
|
+
return null;
|
|
2162
|
+
const description = str(raw.descricao) ?? "";
|
|
2163
|
+
const statusId = num(raw.statusId);
|
|
2164
|
+
const typeId = num(raw.tipoId);
|
|
2165
|
+
const available = pickUnits(raw.valorDisponivel4Casas, raw.valorDisponivel);
|
|
2166
|
+
return {
|
|
2167
|
+
id,
|
|
2168
|
+
description,
|
|
2169
|
+
statusId,
|
|
2170
|
+
status: str(raw.statusDescricao) ?? creditStatusOf(statusId),
|
|
2171
|
+
typeId,
|
|
2172
|
+
type: creditTypeOf(typeId),
|
|
2173
|
+
typeLabel: typeId === null ? null : CREDIT_TYPE_LABEL[typeId] ?? null,
|
|
2174
|
+
amount: pickUnits(raw.valor4Casas, raw.valor),
|
|
2175
|
+
available,
|
|
2176
|
+
fee: toUnits(raw.taxa),
|
|
2177
|
+
creditedAt: toDay(raw.dataCredito) ?? toDay(raw.dataCreditoFormatada),
|
|
2178
|
+
invoiceId: num(raw.faturaId),
|
|
2179
|
+
groupId: num(raw.grupoId) ?? groupIdFrom(description),
|
|
2180
|
+
refundable: bool(raw.disponivelParaEstorno)
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
function normalizeBalance(raw, savings) {
|
|
2184
|
+
return {
|
|
2185
|
+
available: pickUnits(raw.valorDisponivel4Casas, raw.valorDisponivel),
|
|
2186
|
+
blocked: pickUnits(raw.valorBloqueado4Casas, raw.valorBloqueado),
|
|
2187
|
+
pending: pickUnits(raw.valorPendente4Casas, raw.valorPendente),
|
|
2188
|
+
saved: savings ? toUnits(savings.valorEconomizado) : null
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
function normalizePayout(raw) {
|
|
2192
|
+
const groupId = num(raw.id);
|
|
2193
|
+
if (groupId === null)
|
|
2194
|
+
return null;
|
|
2195
|
+
const statusId = num(raw.statusId);
|
|
2196
|
+
return {
|
|
2197
|
+
groupId,
|
|
2198
|
+
description: str(raw.descricao) ?? `Grupo #${groupId}`,
|
|
2199
|
+
amount: pickUnits(raw.valor4Casas, raw.valor),
|
|
2200
|
+
memberShare: toUnits(raw.vl_kota_membro),
|
|
2201
|
+
statusId,
|
|
2202
|
+
status: str(raw.status) ?? payoutStatusOf(statusId),
|
|
2203
|
+
nextPaymentDate: toDay(raw.dataProximoPagamento) ?? toDay(raw.dataProximoPagamentoFormatada)
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
function normalizePayoutEntry(groupId, raw) {
|
|
2207
|
+
const id = num(raw.id);
|
|
2208
|
+
if (id === null)
|
|
2209
|
+
return null;
|
|
2210
|
+
const statusId = num(raw.statusId);
|
|
2211
|
+
return {
|
|
2212
|
+
groupId,
|
|
2213
|
+
id,
|
|
2214
|
+
description: str(raw.descricao) ?? "",
|
|
2215
|
+
participant: str(raw.participante),
|
|
2216
|
+
amount: pickUnits(raw.valor4Casas, raw.valor),
|
|
2217
|
+
statusId,
|
|
2218
|
+
status: str(raw.statusDescricao) ?? payoutStatusOf(statusId),
|
|
2219
|
+
date: toDay(raw.data) ?? toDay(raw.dataFormatada)
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
var INVOICE_SUBJECT, GROUP_IN_TEXT, num = (value) => {
|
|
2223
|
+
if (value === null || value === undefined || value === "")
|
|
2224
|
+
return null;
|
|
2225
|
+
const parsed = Number(value);
|
|
2226
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2227
|
+
}, str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null, bool = (value) => value === true;
|
|
2228
|
+
var init_normalize = __esm(() => {
|
|
2229
|
+
init_dates();
|
|
2230
|
+
init_enums();
|
|
2231
|
+
INVOICE_SUBJECT = /^Fatura grupo\s+(?<product>.+)\s+#(?<groupId>\d+)\s*$/;
|
|
2232
|
+
GROUP_IN_TEXT = /#(\d+)/;
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
// src/tools/credits.ts
|
|
2236
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
2237
|
+
var balance, listCredits, TYPE_BY_NAME;
|
|
2238
|
+
var init_credits = __esm(() => {
|
|
2239
|
+
init_rows();
|
|
2240
|
+
init_enums();
|
|
2241
|
+
init_normalize();
|
|
2242
|
+
init_define();
|
|
2243
|
+
init_fields();
|
|
2244
|
+
balance = defineTool({
|
|
2245
|
+
name: "balance",
|
|
2246
|
+
description: "Saldo da carteira Kotas ao vivo (1\u20132 requisi\xE7\xF5es): dispon\xEDvel, bloqueado (as cau\xE7\xF5es das " + "inscri\xE7\xF5es) e pendente, mais a economia acumulada que a pr\xF3pria Kotas calcula. O saldo bloqueado " + "n\xE3o \xE9 dinheiro perdido: volta quando a assinatura \xE9 encerrada.",
|
|
2247
|
+
readOnly: true,
|
|
2248
|
+
input: Type6.Object({
|
|
2249
|
+
with_savings: Type6.Optional(Type6.Boolean({ description: "Tamb\xE9m busca a economia acumulada (1 requisi\xE7\xE3o extra, default true)" }))
|
|
2250
|
+
}),
|
|
2251
|
+
run: async (args, ctx) => {
|
|
2252
|
+
const raw = await ctx.api.getBalance();
|
|
2253
|
+
const savings = args.with_savings === false ? null : await ctx.api.getSavings().catch(() => null);
|
|
2254
|
+
const parsed = normalizeBalance(raw, savings);
|
|
2255
|
+
return compactObject({
|
|
2256
|
+
available: money(parsed.available),
|
|
2257
|
+
blocked: money(parsed.blocked),
|
|
2258
|
+
pending: money(parsed.pending),
|
|
2259
|
+
saved: money(parsed.saved),
|
|
2260
|
+
note: "`blocked` s\xE3o cau\xE7\xF5es de inscri\xE7\xE3o, devolvidas ao encerrar a assinatura."
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
listCredits = defineTool({
|
|
2265
|
+
name: "list_credits",
|
|
2266
|
+
description: "Lista os lan\xE7amentos de cr\xE9dito do cache com o significado de cada tipo traduzido: adi\xE7\xE3o de " + "saldo, cau\xE7\xE3o da inscri\xE7\xE3o (fica bloqueada), repasse recebido como administrador e estorno de " + "cancelamento. N\xE3o usa a rede. \xC9 aqui que aparece o dinheiro que ENTROU \u2014 as faturas s\xF3 mostram o " + "que saiu. ATEN\xC7\xC3O: use `available`, n\xE3o `amount`. A API zera o valor de face de todo lan\xE7amento " + "j\xE1 consumido, ent\xE3o `amount` vem 0 em quase tudo; `available` \xE9 a cifra que existe de verdade e " + "\xE9 a que soma com o saldo da carteira.",
|
|
2267
|
+
readOnly: true,
|
|
2268
|
+
input: Type6.Object({
|
|
2269
|
+
kind: Type6.Optional(Type6.Union([
|
|
2270
|
+
Type6.Literal("adicaoDeSaldo"),
|
|
2271
|
+
Type6.Literal("caucaoDaInscricao"),
|
|
2272
|
+
Type6.Literal("repasseAdministrador"),
|
|
2273
|
+
Type6.Literal("estornoCancelamento")
|
|
2274
|
+
], { description: "Filtra pelo tipo do lan\xE7amento" })),
|
|
2275
|
+
limit: limitField(500, 100)
|
|
2276
|
+
}),
|
|
2277
|
+
run: (args, ctx) => {
|
|
2278
|
+
const cache = ctx.cache();
|
|
2279
|
+
const typeId = args.kind ? TYPE_BY_NAME[args.kind] : undefined;
|
|
2280
|
+
const rows = cache.listCredits(compactObject({ typeId, limit: args.limit ?? 100 }));
|
|
2281
|
+
return compactObject({
|
|
2282
|
+
total: rows.length,
|
|
2283
|
+
credits: rows.map(creditOut),
|
|
2284
|
+
legend: CREDIT_TYPE_LABEL,
|
|
2285
|
+
note: "Some por `available`: a API zera `amount` depois que o cr\xE9dito \xE9 usado ou sacado.",
|
|
2286
|
+
...rows.length === 0 && cache.stats().credits === 0 ? { note: "O cache est\xE1 vazio. Rode `sync` antes de perguntar." } : {}
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
TYPE_BY_NAME = {
|
|
2291
|
+
adicaoDeSaldo: 94,
|
|
2292
|
+
caucaoDaInscricao: 96,
|
|
2293
|
+
repasseAdministrador: 97,
|
|
2294
|
+
estornoCancelamento: 98
|
|
2295
|
+
};
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
// src/tools/doctor.ts
|
|
2299
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
2300
|
+
function loadSession(ctx, add) {
|
|
2301
|
+
try {
|
|
2302
|
+
const session = ctx.session.load();
|
|
2303
|
+
if (!session) {
|
|
2304
|
+
add("session", false, "Nenhuma sess\xE3o salva. Rode `kotas login`.");
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
add("session", true, `sess\xE3o de ${new Date(session.savedAt).toISOString()}, dispositivo ${session.hashDispositivo ? "vinculado" : "SEM hashDispositivo (risco de HTTP 412)"}`);
|
|
2308
|
+
return session;
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
add("session", false, message(error));
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
async function probe(add, name, run) {
|
|
2315
|
+
try {
|
|
2316
|
+
const result = await run();
|
|
2317
|
+
add(name, result.ok, result.detail);
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
add(name, false, message(error));
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function report(checks, ctx, deep) {
|
|
2323
|
+
try {
|
|
2324
|
+
const stats = ctx.cache().stats();
|
|
2325
|
+
checks.push({
|
|
2326
|
+
name: "cache",
|
|
2327
|
+
ok: stats.invoices > 0,
|
|
2328
|
+
detail: stats.invoices === 0 ? "cache vazio: rode `sync`" : `${stats.invoices} faturas (${stats.invoicesWithItems} com itens, ${stats.pendingDetails} pendentes), ` + `${stats.groups} grupos, ${stats.credits} cr\xE9ditos, ${stats.payouts} recebimentos, ` + `${stats.oldest} \u2192 ${stats.newest}`
|
|
2329
|
+
});
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
checks.push({ name: "cache", ok: false, detail: message(error) });
|
|
2332
|
+
}
|
|
2333
|
+
const seen = new Set(checks.map((check) => check.name));
|
|
2334
|
+
const failed = checks.some((check) => !check.ok);
|
|
2335
|
+
for (const name of LAYERS) {
|
|
2336
|
+
if (seen.has(name))
|
|
2337
|
+
continue;
|
|
2338
|
+
checks.push({
|
|
2339
|
+
name,
|
|
2340
|
+
ok: false,
|
|
2341
|
+
detail: !deep ? "pulado: deep=false" : failed ? "pulado: a camada anterior falhou" : "pulado"
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
const rank = (name) => {
|
|
2345
|
+
const index = LAYERS.indexOf(name);
|
|
2346
|
+
return index === -1 ? LAYERS.length : index;
|
|
2347
|
+
};
|
|
2348
|
+
const ordered = [...checks].sort((left, right) => rank(left.name) - rank(right.name));
|
|
2349
|
+
return { ok: ordered.every((check) => check.ok), checks: ordered };
|
|
2350
|
+
}
|
|
2351
|
+
function message(error) {
|
|
2352
|
+
if (error instanceof KotasAuthError)
|
|
2353
|
+
return error.message;
|
|
2354
|
+
return error instanceof Error ? error.message : String(error);
|
|
2355
|
+
}
|
|
2356
|
+
var LAYERS, doctor;
|
|
2357
|
+
var init_doctor = __esm(() => {
|
|
2358
|
+
init_errors();
|
|
2359
|
+
init_jwt();
|
|
2360
|
+
init_define();
|
|
2361
|
+
LAYERS = [
|
|
2362
|
+
"config",
|
|
2363
|
+
"session",
|
|
2364
|
+
"token",
|
|
2365
|
+
"version",
|
|
2366
|
+
"groups",
|
|
2367
|
+
"invoices",
|
|
2368
|
+
"credits",
|
|
2369
|
+
"payouts",
|
|
2370
|
+
"cache"
|
|
2371
|
+
];
|
|
2372
|
+
doctor = defineTool({
|
|
2373
|
+
name: "doctor",
|
|
2374
|
+
description: "Diagn\xF3stico camada a camada: configura\xE7\xE3o, sess\xE3o salva, renova\xE7\xE3o do token, vers\xE3o do front, " + "grupos, faturas, cr\xE9ditos, recebimentos e cache. Use quando algo falhar de um jeito estranho: ele " + "diz qual camada quebrou. Gasta cerca de 6 requisi\xE7\xF5es (2 com deep=false).",
|
|
2375
|
+
readOnly: true,
|
|
2376
|
+
input: Type7.Object({
|
|
2377
|
+
deep: Type7.Optional(Type7.Boolean({ description: "Tamb\xE9m testa grupos, cr\xE9ditos e recebimentos (default true)" }))
|
|
2378
|
+
}),
|
|
2379
|
+
run: async (args, ctx) => {
|
|
2380
|
+
const checks = [];
|
|
2381
|
+
const add = (name, ok, detail) => checks.push({ name, ok, detail });
|
|
2382
|
+
const deep = args.deep !== false;
|
|
2383
|
+
add("config", true, `dados em ${ctx.config.configDir}, ritmo ${ctx.config.minIntervalMs}+${ctx.config.jitterMs} ms, ` + `versao ${ctx.config.appVersion}, somente leitura ${ctx.config.readOnly ? "ligado" : "desligado"}`);
|
|
2384
|
+
const session = loadSession(ctx, add);
|
|
2385
|
+
if (!session)
|
|
2386
|
+
return report(checks, ctx, deep);
|
|
2387
|
+
let tokenOk = false;
|
|
2388
|
+
try {
|
|
2389
|
+
const at = expiresAt(session.accessToken);
|
|
2390
|
+
const expired = isExpired(session.accessToken, ctx.now(), 0);
|
|
2391
|
+
const claims = decodeJwt(session.accessToken);
|
|
2392
|
+
add("token", true, `JWT de ${claims.TX_EMAIL_USUARIO ?? "conta desconhecida"}, expira ${at ? new Date(at).toISOString() : "sem `exp`"}${expired ? " (expirado \u2014 ser\xE1 renovado na pr\xF3xima chamada)" : ""}`);
|
|
2393
|
+
tokenOk = true;
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
add("token", false, message(error));
|
|
2396
|
+
}
|
|
2397
|
+
if (!tokenOk)
|
|
2398
|
+
return report(checks, ctx, deep);
|
|
2399
|
+
await probe(add, "version", async () => {
|
|
2400
|
+
const current = await ctx.api.version();
|
|
2401
|
+
const published = typeof current === "string" ? current : current === null || current === undefined ? null : JSON.stringify(current);
|
|
2402
|
+
if (published === null) {
|
|
2403
|
+
return {
|
|
2404
|
+
ok: true,
|
|
2405
|
+
detail: `o header \`versao\` ${ctx.config.appVersion} foi aceito (a rota n\xE3o publica a vers\xE3o atual)`
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
ok: true,
|
|
2410
|
+
detail: published.includes(ctx.config.appVersion) ? `a API confirma a vers\xE3o ${ctx.config.appVersion}` : `a API publica ${published}, o header \`versao\` manda ${ctx.config.appVersion}. ` + "Se aparecer HTTP 426, ajuste KOTAS_APP_VERSION."
|
|
2411
|
+
};
|
|
2412
|
+
});
|
|
2413
|
+
await probe(add, "invoices", async () => {
|
|
2414
|
+
const page = await ctx.api.listInvoices(13, 1, 5);
|
|
2415
|
+
const withSubject = page.items.filter((raw) => /^Fatura grupo .+ #\d+$/.test(String(raw.descricao ?? ""))).length;
|
|
2416
|
+
return {
|
|
2417
|
+
ok: page.items.length > 0,
|
|
2418
|
+
detail: `${page.items.length} faturas pagas na p\xE1gina 1; ${withSubject} com descri\xE7\xE3o no formato ` + "`Fatura grupo <produto> #<id>` (\xE9 o que reconstr\xF3i o hist\xF3rico dos grupos encerrados)"
|
|
2419
|
+
};
|
|
2420
|
+
});
|
|
2421
|
+
if (deep) {
|
|
2422
|
+
await probe(add, "groups", async () => {
|
|
2423
|
+
const page = await ctx.api.listGroups(0, 5);
|
|
2424
|
+
const admin = page.items.filter((raw) => raw.inAdministrador === true).length;
|
|
2425
|
+
return {
|
|
2426
|
+
ok: true,
|
|
2427
|
+
detail: `${page.items.length} grupos ativos na p\xE1gina 0, ${admin} como administrador`
|
|
2428
|
+
};
|
|
2429
|
+
});
|
|
2430
|
+
await probe(add, "credits", async () => {
|
|
2431
|
+
const balance2 = await ctx.api.getBalance();
|
|
2432
|
+
return {
|
|
2433
|
+
ok: typeof balance2 === "object" && balance2 !== null,
|
|
2434
|
+
detail: "saldo lido; dispon\xEDvel/bloqueado/pendente presentes: " + ["valorDisponivel", "valorBloqueado", "valorPendente"].filter((key) => (key in balance2)).join(", ")
|
|
2435
|
+
};
|
|
2436
|
+
});
|
|
2437
|
+
await probe(add, "payouts", async () => {
|
|
2438
|
+
const page = await ctx.api.listPayouts(0, 5);
|
|
2439
|
+
return { ok: true, detail: `${page.items.length} recebimentos (total ${page.total ?? "?"})` };
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
return report(checks, ctx, deep);
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
// src/tools/export.ts
|
|
2448
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2449
|
+
import { join as join2, resolve } from "path";
|
|
2450
|
+
import { Type as Type8 } from "@sinclair/typebox";
|
|
2451
|
+
function resolveInside(dir, name) {
|
|
2452
|
+
const root = resolve(dir);
|
|
2453
|
+
const target = resolve(join2(root, safeName(name)));
|
|
2454
|
+
if (target !== join2(root, safeName(name)) || !target.startsWith(`${root}/`)) {
|
|
2455
|
+
throw new Error(`Caminho fora de KOTAS_EXPORT_DIR (${root}). O export s\xF3 grava nesse diret\xF3rio.`);
|
|
2456
|
+
}
|
|
2457
|
+
return target;
|
|
2458
|
+
}
|
|
2459
|
+
function collect(ctx, scope, from, to) {
|
|
2460
|
+
const cache = ctx.cache();
|
|
2461
|
+
const amount = (units) => units === null ? null : toDecimal(units);
|
|
2462
|
+
if (scope === "invoices") {
|
|
2463
|
+
const filters = { limit: 1e5, ...from ? { from } : {}, ...to ? { to } : {} };
|
|
2464
|
+
return cache.listInvoices(filters).map((row) => ({
|
|
2465
|
+
id: row.id,
|
|
2466
|
+
date: row.paid_date ?? row.due_date,
|
|
2467
|
+
product: row.product,
|
|
2468
|
+
group_id: row.group_id,
|
|
2469
|
+
status: row.status,
|
|
2470
|
+
total: amount(row.total_units),
|
|
2471
|
+
discount: amount(row.discount_units),
|
|
2472
|
+
interest: amount(row.interest_units),
|
|
2473
|
+
due_date: row.due_date,
|
|
2474
|
+
paid_date: row.paid_date,
|
|
2475
|
+
description: row.description
|
|
2476
|
+
}));
|
|
2477
|
+
}
|
|
2478
|
+
if (scope === "subscriptions") {
|
|
2479
|
+
return cache.listGroups({ limit: 1e5 }).map((row) => ({
|
|
2480
|
+
id: row.id,
|
|
2481
|
+
name: row.name,
|
|
2482
|
+
service: row.service,
|
|
2483
|
+
plan: row.plan,
|
|
2484
|
+
status: row.status,
|
|
2485
|
+
role: row.is_administrator === 1 ? "administrador" : "membro",
|
|
2486
|
+
amount: amount(row.amount_units),
|
|
2487
|
+
service_total: amount(row.service_total_units),
|
|
2488
|
+
slots_total: row.slots_total,
|
|
2489
|
+
slots_taken: row.slots_taken,
|
|
2490
|
+
joined_at: row.joined_at
|
|
2491
|
+
}));
|
|
2492
|
+
}
|
|
2493
|
+
if (scope === "credits") {
|
|
2494
|
+
return cache.listCredits({ limit: 1e5 }).map((row) => ({
|
|
2495
|
+
id: row.id,
|
|
2496
|
+
date: row.credited_at,
|
|
2497
|
+
type: row.type,
|
|
2498
|
+
meaning: row.type_label,
|
|
2499
|
+
status: row.status,
|
|
2500
|
+
amount: amount(row.amount_units),
|
|
2501
|
+
available: amount(row.available_units),
|
|
2502
|
+
group_id: row.group_id,
|
|
2503
|
+
description: row.description
|
|
2504
|
+
}));
|
|
2505
|
+
}
|
|
2506
|
+
if (scope === "payouts") {
|
|
2507
|
+
return cache.listPayouts().map((row) => ({
|
|
2508
|
+
group_id: row.group_id,
|
|
2509
|
+
description: row.description,
|
|
2510
|
+
amount: amount(row.amount_units),
|
|
2511
|
+
member_share: amount(row.member_share_units),
|
|
2512
|
+
status: row.status,
|
|
2513
|
+
next_payment_date: row.next_payment_date
|
|
2514
|
+
}));
|
|
2515
|
+
}
|
|
2516
|
+
return purchaseHistory(cache, { includeCreditPurchases: true }).map((entry) => ({
|
|
2517
|
+
group_id: entry.groupId,
|
|
2518
|
+
product: entry.product,
|
|
2519
|
+
first_invoice: entry.firstInvoice,
|
|
2520
|
+
last_invoice: entry.lastInvoice,
|
|
2521
|
+
months_paid: entry.monthsPaid,
|
|
2522
|
+
total_paid: entry.totalPaid?.amount ?? null,
|
|
2523
|
+
active_today: entry.activeToday ? "sim" : "nao",
|
|
2524
|
+
status: entry.status
|
|
2525
|
+
}));
|
|
2526
|
+
}
|
|
2527
|
+
function toCsv(rows) {
|
|
2528
|
+
if (rows.length === 0)
|
|
2529
|
+
return "";
|
|
2530
|
+
const headers = Object.keys(rows[0]);
|
|
2531
|
+
const cell = (value) => {
|
|
2532
|
+
if (value === null || value === undefined)
|
|
2533
|
+
return "";
|
|
2534
|
+
const text = String(value);
|
|
2535
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
2536
|
+
};
|
|
2537
|
+
const lines = [headers.join(",")];
|
|
2538
|
+
for (const row of rows)
|
|
2539
|
+
lines.push(headers.map((header) => cell(row[header] ?? null)).join(","));
|
|
2540
|
+
return `${lines.join(`
|
|
2541
|
+
`)}
|
|
2542
|
+
`;
|
|
2543
|
+
}
|
|
2544
|
+
var SCOPES, safeName = (name) => name.replace(/[/\\]/g, "_").replace(/^\.+/, ""), exportData;
|
|
2545
|
+
var init_export = __esm(() => {
|
|
2546
|
+
init_analytics();
|
|
2547
|
+
init_dates();
|
|
2548
|
+
init_define();
|
|
2549
|
+
init_fields();
|
|
2550
|
+
SCOPES = ["invoices", "subscriptions", "credits", "payouts", "history"];
|
|
2551
|
+
exportData = defineTool({
|
|
2552
|
+
name: "export",
|
|
2553
|
+
description: "Exporta o cache para CSV ou JSON dentro de KOTAS_EXPORT_DIR (por padr\xE3o ~/Downloads/kotas-export). " + "N\xE3o usa a rede e n\xE3o altera nada na conta. Valores saem em reais com 4 casas, do jeito que a " + "Kotas calcula o rateio.",
|
|
2554
|
+
readOnly: false,
|
|
2555
|
+
input: Type8.Object({
|
|
2556
|
+
scope: Type8.Union(SCOPES.map((scope) => Type8.Literal(scope)), { description: "O que exportar" }),
|
|
2557
|
+
format: Type8.Optional(Type8.Union([Type8.Literal("csv"), Type8.Literal("json")], { description: "Formato (default csv)" })),
|
|
2558
|
+
from: dayField("S\xF3 para invoices: data inicial (YYYY-MM-DD)"),
|
|
2559
|
+
to: dayField("S\xF3 para invoices: data final (YYYY-MM-DD)"),
|
|
2560
|
+
filename: Type8.Optional(Type8.String({ description: "Nome do arquivo, sem caminho; default kotas-<scope>-<data>.<formato>" }))
|
|
2561
|
+
}),
|
|
2562
|
+
run: (args, ctx) => {
|
|
2563
|
+
const format = args.format ?? "csv";
|
|
2564
|
+
const rows = collect(ctx, args.scope, args.from, args.to);
|
|
2565
|
+
const stamp = dayFromEpochMs(ctx.now());
|
|
2566
|
+
const target = resolveInside(ctx.config.exportDir, args.filename ?? `kotas-${args.scope}-${stamp}.${format}`);
|
|
2567
|
+
mkdirSync3(ctx.config.exportDir, { recursive: true, mode: 448 });
|
|
2568
|
+
const content = format === "json" ? `${JSON.stringify(rows, null, 2)}
|
|
2569
|
+
` : toCsv(rows);
|
|
2570
|
+
writeFileSync2(target, content, { mode: 384 });
|
|
2571
|
+
chmodSync3(target, 384);
|
|
2572
|
+
return { path: target, format, scope: args.scope, rows: rows.length };
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
// src/tools/invoices.ts
|
|
2578
|
+
import { Type as Type9 } from "@sinclair/typebox";
|
|
2579
|
+
var NO_CACHE = "O cache est\xE1 vazio. Rode `sync` antes de perguntar.", listInvoices, getInvoice;
|
|
2580
|
+
var init_invoices = __esm(() => {
|
|
2581
|
+
init_rows();
|
|
2582
|
+
init_normalize();
|
|
2583
|
+
init_define();
|
|
2584
|
+
init_fields();
|
|
2585
|
+
listInvoices = defineTool({
|
|
2586
|
+
name: "list_invoices",
|
|
2587
|
+
description: "Lista as faturas do Kotas a partir do cache local, da mais recente para a mais antiga. N\xE3o usa a " + "rede. ATEN\xC7\xC3O ao somar: s\xF3 fatura com status `pago` \xE9 dinheiro que saiu da conta \u2014 cancelada e " + "estornada n\xE3o s\xE3o gasto. O campo a somar \xE9 `total`, nunca `amount`. Use `product` para saber de " + "qual assinatura a cobran\xE7a era, inclusive de grupos que j\xE1 foram encerrados.",
|
|
2588
|
+
readOnly: true,
|
|
2589
|
+
input: Type9.Object({
|
|
2590
|
+
status: invoiceStatusField,
|
|
2591
|
+
from: dayField("Data inicial (YYYY-MM-DD), sobre a data de pagamento ou vencimento"),
|
|
2592
|
+
to: dayField("Data final (YYYY-MM-DD)"),
|
|
2593
|
+
group_id: Type9.Optional(Type9.Integer({ minimum: 1, description: "S\xF3 as faturas deste grupo" })),
|
|
2594
|
+
product: Type9.Optional(Type9.String({ minLength: 1, description: "Nome exato do produto, como aparece em `product`" })),
|
|
2595
|
+
limit: limitField(500, 50),
|
|
2596
|
+
offset: offsetField,
|
|
2597
|
+
compact: compactField
|
|
2598
|
+
}),
|
|
2599
|
+
run: (args, ctx) => {
|
|
2600
|
+
const cache = ctx.cache();
|
|
2601
|
+
const filters = compactObject({
|
|
2602
|
+
statusId: args.status ? INVOICE_STATUS_BY_NAME[args.status] : undefined,
|
|
2603
|
+
from: args.from,
|
|
2604
|
+
to: args.to,
|
|
2605
|
+
groupId: args.group_id,
|
|
2606
|
+
product: args.product,
|
|
2607
|
+
limit: args.limit ?? 50,
|
|
2608
|
+
offset: args.offset ?? 0
|
|
2609
|
+
});
|
|
2610
|
+
const rows = cache.listInvoices(filters);
|
|
2611
|
+
const total = cache.countInvoices(filters);
|
|
2612
|
+
return compactObject({
|
|
2613
|
+
total,
|
|
2614
|
+
invoices: rows.map((row) => invoiceOut(row, args.compact === true)),
|
|
2615
|
+
...total === 0 && cache.stats().invoices === 0 ? { note: NO_CACHE } : {}
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
getInvoice = defineTool({
|
|
2620
|
+
name: "get_invoice",
|
|
2621
|
+
description: "Detalha uma fatura, com os itens que a comp\xF5em. L\xEA o cache; se os itens ainda n\xE3o estiverem l\xE1, " + "busca 1 vez na API (`fatura/extrato`), porque a listagem devolve `itens: null`.",
|
|
2622
|
+
readOnly: true,
|
|
2623
|
+
input: Type9.Object({ invoice_id: invoiceIdField }),
|
|
2624
|
+
run: async (args, ctx) => {
|
|
2625
|
+
const cache = ctx.cache();
|
|
2626
|
+
const row = cache.getInvoice(args.invoice_id);
|
|
2627
|
+
if (!row) {
|
|
2628
|
+
throw new Error(`Fatura ${args.invoice_id} n\xE3o est\xE1 no cache. Rode \`sync\` \u2014 a API n\xE3o permite buscar uma fatura avulsa pela listagem.`);
|
|
2629
|
+
}
|
|
2630
|
+
let items = cache.getInvoiceItems(args.invoice_id);
|
|
2631
|
+
if (items.length === 0 && row.detail_fetched_at === null) {
|
|
2632
|
+
const raw = await ctx.api.getInvoiceItems(args.invoice_id);
|
|
2633
|
+
cache.setInvoiceItems(args.invoice_id, normalizeInvoiceItems(args.invoice_id, raw), raw);
|
|
2634
|
+
items = cache.getInvoiceItems(args.invoice_id);
|
|
2635
|
+
}
|
|
2636
|
+
return compactObject({
|
|
2637
|
+
invoice: invoiceOut(row),
|
|
2638
|
+
items: items.map(invoiceItemOut),
|
|
2639
|
+
...row.detail_error ? { detailError: row.detail_error } : {}
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
// src/session/browser-import.ts
|
|
2646
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2647
|
+
import { homedir as homedir2 } from "os";
|
|
2648
|
+
import { join as join3 } from "path";
|
|
2649
|
+
function readUvarint(data, at) {
|
|
2650
|
+
let result = 0;
|
|
2651
|
+
let shift = 0;
|
|
2652
|
+
let index = at;
|
|
2653
|
+
while (index < data.length) {
|
|
2654
|
+
const byte = data[index];
|
|
2655
|
+
result |= (byte & 127) << shift;
|
|
2656
|
+
index += 1;
|
|
2657
|
+
if ((byte & 128) === 0)
|
|
2658
|
+
return { value: result >>> 0, next: index };
|
|
2659
|
+
shift += 7;
|
|
2660
|
+
if (shift > 28)
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
function varintsEndingAt(data, end, count) {
|
|
2666
|
+
for (let width = count;width <= count * 5; width += 1) {
|
|
2667
|
+
const start = end - width;
|
|
2668
|
+
if (start < 0)
|
|
2669
|
+
continue;
|
|
2670
|
+
const values = [];
|
|
2671
|
+
let cursor = start;
|
|
2672
|
+
let ok = true;
|
|
2673
|
+
for (let n = 0;n < count; n += 1) {
|
|
2674
|
+
const read = readUvarint(data, cursor);
|
|
2675
|
+
if (!read) {
|
|
2676
|
+
ok = false;
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
values.push(read.value);
|
|
2680
|
+
cursor = read.next;
|
|
2681
|
+
}
|
|
2682
|
+
if (ok && cursor === end)
|
|
2683
|
+
return values;
|
|
2684
|
+
}
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
function decodeValue(raw) {
|
|
2688
|
+
if (raw.length < 2)
|
|
2689
|
+
return null;
|
|
2690
|
+
const body = raw.subarray(1);
|
|
2691
|
+
if (raw[0] === 0)
|
|
2692
|
+
return body.toString("utf16le");
|
|
2693
|
+
if (raw[0] === 1)
|
|
2694
|
+
return body.toString("latin1");
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
function scanFile(data, origin) {
|
|
2698
|
+
const prefix = originPrefix(origin);
|
|
2699
|
+
const found = [];
|
|
2700
|
+
let at = data.indexOf(prefix);
|
|
2701
|
+
while (at !== -1) {
|
|
2702
|
+
for (const candidate of framings(data, at, prefix.length))
|
|
2703
|
+
found.push(candidate);
|
|
2704
|
+
at = data.indexOf(prefix, at + 1);
|
|
2705
|
+
}
|
|
2706
|
+
return found;
|
|
2707
|
+
}
|
|
2708
|
+
function framings(data, at, prefixLength) {
|
|
2709
|
+
const out = [];
|
|
2710
|
+
const push = (nameEnd, valueStart, valueLength) => {
|
|
2711
|
+
if (nameEnd <= at + prefixLength || valueStart + valueLength > data.length)
|
|
2712
|
+
return;
|
|
2713
|
+
const name = data.subarray(at + prefixLength, nameEnd).toString("latin1");
|
|
2714
|
+
if (!PRINTABLE.test(name))
|
|
2715
|
+
return;
|
|
2716
|
+
const value = decodeValue(data.subarray(valueStart, valueStart + valueLength));
|
|
2717
|
+
if (value !== null)
|
|
2718
|
+
out.push({ name, value });
|
|
2719
|
+
};
|
|
2720
|
+
const log = varintsEndingAt(data, at, 1);
|
|
2721
|
+
const keyLength = log?.[0];
|
|
2722
|
+
if (keyLength !== undefined && keyLength >= prefixLength) {
|
|
2723
|
+
const keyEnd = at + keyLength;
|
|
2724
|
+
const valueLength = readUvarint(data, keyEnd);
|
|
2725
|
+
if (valueLength)
|
|
2726
|
+
push(keyEnd, valueLength.next, valueLength.value);
|
|
2727
|
+
}
|
|
2728
|
+
const table = varintsEndingAt(data, at, 3);
|
|
2729
|
+
if (table && table[0] === 0 && table[1] !== undefined && table[2] !== undefined) {
|
|
2730
|
+
for (const trailer of [8, 0]) {
|
|
2731
|
+
push(at + table[1] - trailer, at + table[1], table[2]);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return out;
|
|
2735
|
+
}
|
|
2736
|
+
function leveldbFiles(profileDir) {
|
|
2737
|
+
const dir = join3(profileDir, "Local Storage", "leveldb");
|
|
2738
|
+
if (!existsSync3(dir))
|
|
2739
|
+
return [];
|
|
2740
|
+
return readdirSync(dir).filter((name) => name.endsWith(".log") || name.endsWith(".ldb")).map((name) => join3(dir, name)).sort((left, right) => statSync2(left).mtimeMs - statSync2(right).mtimeMs);
|
|
2741
|
+
}
|
|
2742
|
+
function profileDirs(browser, home = homedir2()) {
|
|
2743
|
+
const base = join3(home, "Library", "Application Support", LOCATIONS[browser].dir);
|
|
2744
|
+
if (!existsSync3(base))
|
|
2745
|
+
return [];
|
|
2746
|
+
return readdirSync(base).filter((name) => name === "Default" || /^Profile \d+$/.test(name)).map((name) => join3(base, name));
|
|
2747
|
+
}
|
|
2748
|
+
function importFromBrowser(browser, now, options = {}) {
|
|
2749
|
+
const origin = options.origin ?? APP_ORIGIN;
|
|
2750
|
+
const label = LOCATIONS[browser].label;
|
|
2751
|
+
const dirs = profileDirs(browser, options.home);
|
|
2752
|
+
if (dirs.length === 0) {
|
|
2753
|
+
throw new LoginError(`N\xE3o achei um perfil do ${label} nesta m\xE1quina. Rode \`kotas login\` (e-mail e senha) ou \`kotas login --paste\`.`);
|
|
2754
|
+
}
|
|
2755
|
+
const best = {};
|
|
2756
|
+
let profile = "";
|
|
2757
|
+
for (const dir of dirs) {
|
|
2758
|
+
for (const file of leveldbFiles(dir)) {
|
|
2759
|
+
let data;
|
|
2760
|
+
try {
|
|
2761
|
+
data = readFileSync2(file);
|
|
2762
|
+
} catch {
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
for (const { name, value } of scanFile(data, origin)) {
|
|
2766
|
+
if (!accept(name, value))
|
|
2767
|
+
continue;
|
|
2768
|
+
best[name] = value;
|
|
2769
|
+
profile = dir;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
const accessToken = best[STORAGE_KEYS.accessToken];
|
|
2774
|
+
const refreshToken = best[STORAGE_KEYS.refreshToken];
|
|
2775
|
+
if (!accessToken || !refreshToken) {
|
|
2776
|
+
throw new LoginError(`N\xE3o encontrei uma sess\xE3o do Kotas no ${label}. Abra ${origin}, entre na conta e rode de novo \u2014 ` + "ou use `kotas login` (e-mail e senha), que n\xE3o depende do navegador.");
|
|
2777
|
+
}
|
|
2778
|
+
return {
|
|
2779
|
+
browser: label,
|
|
2780
|
+
profile,
|
|
2781
|
+
session: {
|
|
2782
|
+
version: 1,
|
|
2783
|
+
accessToken,
|
|
2784
|
+
refreshToken,
|
|
2785
|
+
hashDispositivo: best[STORAGE_KEYS.deviceHash] ?? "",
|
|
2786
|
+
savedAt: now
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function accept(name, value) {
|
|
2791
|
+
if (name === STORAGE_KEYS.accessToken)
|
|
2792
|
+
return JWT.test(value);
|
|
2793
|
+
if (name === STORAGE_KEYS.refreshToken)
|
|
2794
|
+
return OPAQUE.test(value);
|
|
2795
|
+
if (name === STORAGE_KEYS.deviceHash)
|
|
2796
|
+
return HEX32.test(value);
|
|
2797
|
+
return false;
|
|
2798
|
+
}
|
|
2799
|
+
var STORAGE_KEYS, LOCATIONS, originPrefix = (origin) => Buffer.from(`_${origin}\x00\x01`, "latin1"), JWT, OPAQUE, HEX32, PRINTABLE;
|
|
2800
|
+
var init_browser_import = __esm(() => {
|
|
2801
|
+
init_config();
|
|
2802
|
+
init_errors();
|
|
2803
|
+
STORAGE_KEYS = {
|
|
2804
|
+
accessToken: "@chave",
|
|
2805
|
+
refreshToken: "@c08cbbfd6eefc83ac6d23c4c791277e4",
|
|
2806
|
+
deviceHash: "SEFTSF9ESVNQT1NJVElWTw=="
|
|
2807
|
+
};
|
|
2808
|
+
LOCATIONS = {
|
|
2809
|
+
arc: { label: "Arc", dir: "Arc/User Data" },
|
|
2810
|
+
chrome: { label: "Google Chrome", dir: "Google/Chrome" },
|
|
2811
|
+
chromium: { label: "Chromium", dir: "Chromium" },
|
|
2812
|
+
brave: { label: "Brave", dir: "BraveSoftware/Brave-Browser" },
|
|
2813
|
+
edge: { label: "Microsoft Edge", dir: "Microsoft Edge" }
|
|
2814
|
+
};
|
|
2815
|
+
JWT = /^eyJ[\w-]+\.[\w-]+\.[\w-]+$/;
|
|
2816
|
+
OPAQUE = /^[A-Za-z0-9+/=_.-]{16,4096}$/;
|
|
2817
|
+
HEX32 = /^[0-9a-f]{32}$/;
|
|
2818
|
+
PRINTABLE = /^[ -~]+$/;
|
|
2819
|
+
});
|
|
2820
|
+
|
|
2821
|
+
// src/session/login.ts
|
|
2822
|
+
function loginBody(input) {
|
|
2823
|
+
return {
|
|
2824
|
+
email: input.email,
|
|
2825
|
+
senha: input.password,
|
|
2826
|
+
socialAuth: SOCIAL_AUTH.kotas,
|
|
2827
|
+
loginViaSocialAuth: false,
|
|
2828
|
+
term: null,
|
|
2829
|
+
medium: null,
|
|
2830
|
+
source: null,
|
|
2831
|
+
content: null,
|
|
2832
|
+
campaign: null,
|
|
2833
|
+
ref: null
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
async function login(ctx, input) {
|
|
2837
|
+
const previous = safeLoad(ctx);
|
|
2838
|
+
const hashDispositivo = input.hashDispositivo ?? previous?.hashDispositivo ?? "";
|
|
2839
|
+
const response = await ctx.http.send({
|
|
2840
|
+
method: "POST",
|
|
2841
|
+
path: LOGIN_PATH,
|
|
2842
|
+
auth: "none",
|
|
2843
|
+
headers: { hashDispositivo },
|
|
2844
|
+
body: loginBody(input),
|
|
2845
|
+
label: LOGIN_PATH
|
|
2846
|
+
});
|
|
2847
|
+
const payload = parse(response.text);
|
|
2848
|
+
if (response.status === 412) {
|
|
2849
|
+
const hash = typeof payload.hash === "string" ? payload.hash : hashDispositivo;
|
|
2850
|
+
if (hash && previous)
|
|
2851
|
+
ctx.session.save({ ...previous, hashDispositivo: hash });
|
|
2852
|
+
throw new DeviceChallengeError(hash, Number(payload.tipoSolicitacao ?? 0));
|
|
2853
|
+
}
|
|
2854
|
+
if (response.status === 401 || response.status === 403) {
|
|
2855
|
+
throw new LoginError(`O Kotas recusou o login (HTTP ${response.status}): ${payload.message ?? "e-mail ou senha incorretos"}.`);
|
|
2856
|
+
}
|
|
2857
|
+
if (response.status !== 200) {
|
|
2858
|
+
throw new LoginError(`O Kotas respondeu HTTP ${response.status} no login: ${payload.message ?? response.text.slice(0, 200)}`);
|
|
2859
|
+
}
|
|
2860
|
+
if (!payload.token || !payload.refreshToken) {
|
|
2861
|
+
throw new LoginError("O login foi aceito, mas a resposta veio sem `token`/`refreshToken`. A API mudou; rode `kotas doctor`.");
|
|
2862
|
+
}
|
|
2863
|
+
let session = {
|
|
2864
|
+
version: 1,
|
|
2865
|
+
accessToken: payload.token,
|
|
2866
|
+
refreshToken: payload.refreshToken,
|
|
2867
|
+
hashDispositivo: payload.hashDispositivo ?? hashDispositivo,
|
|
2868
|
+
email: input.email,
|
|
2869
|
+
savedAt: ctx.now()
|
|
2870
|
+
};
|
|
2871
|
+
if (needsTwoFactor(session.accessToken)) {
|
|
2872
|
+
if (!input.pin) {
|
|
2873
|
+
throw new TwoFactorError("A conta tem verifica\xE7\xE3o em duas etapas ligada.");
|
|
2874
|
+
}
|
|
2875
|
+
session = await validateTwoFactor(ctx, session, input.pin);
|
|
2876
|
+
}
|
|
2877
|
+
ctx.session.save(session);
|
|
2878
|
+
ctx.http.adopt(session);
|
|
2879
|
+
return session;
|
|
2880
|
+
}
|
|
2881
|
+
function needsTwoFactor(accessToken) {
|
|
2882
|
+
try {
|
|
2883
|
+
return String(decodeJwt(accessToken).TWFAHABILITADO ?? "").toLowerCase() === "true";
|
|
2884
|
+
} catch {
|
|
2885
|
+
return false;
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
async function validateTwoFactor(ctx, session, pin) {
|
|
2889
|
+
if (!/^\d{4,8}$/.test(pin)) {
|
|
2890
|
+
throw new TwoFactorError(`"${pin}" n\xE3o parece um PIN (esperado 6 d\xEDgitos).`);
|
|
2891
|
+
}
|
|
2892
|
+
const response = await ctx.http.send({
|
|
2893
|
+
method: "POST",
|
|
2894
|
+
path: TWO_FACTOR_PATH,
|
|
2895
|
+
auth: "none",
|
|
2896
|
+
headers: {
|
|
2897
|
+
TWFA: pin,
|
|
2898
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
2899
|
+
hashDispositivo: session.hashDispositivo
|
|
2900
|
+
},
|
|
2901
|
+
body: {},
|
|
2902
|
+
label: TWO_FACTOR_PATH
|
|
2903
|
+
});
|
|
2904
|
+
if (response.status !== 200) {
|
|
2905
|
+
const payload2 = parse(response.text);
|
|
2906
|
+
throw new TwoFactorError(`O Kotas recusou o PIN (HTTP ${response.status}): ${payload2.message ?? "c\xF3digo inv\xE1lido ou expirado"}.`);
|
|
2907
|
+
}
|
|
2908
|
+
const payload = parse(response.text);
|
|
2909
|
+
return {
|
|
2910
|
+
...session,
|
|
2911
|
+
...payload.token ? { accessToken: payload.token } : {},
|
|
2912
|
+
...payload.refreshToken ? { refreshToken: payload.refreshToken } : {},
|
|
2913
|
+
savedAt: ctx.now()
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
function safeLoad(ctx) {
|
|
2917
|
+
try {
|
|
2918
|
+
return ctx.session.load();
|
|
2919
|
+
} catch {
|
|
2920
|
+
return null;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
function parse(text) {
|
|
2924
|
+
try {
|
|
2925
|
+
const parsed = JSON.parse(text);
|
|
2926
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2927
|
+
} catch {
|
|
2928
|
+
return {};
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
var LOGIN_PATH = "autenticacao/token", TWO_FACTOR_PATH = "autenticacao/2fa/pin/validar";
|
|
2932
|
+
var init_login = __esm(() => {
|
|
2933
|
+
init_errors();
|
|
2934
|
+
init_enums();
|
|
2935
|
+
init_jwt();
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
// src/tools/login.ts
|
|
2939
|
+
import { Type as Type10 } from "@sinclair/typebox";
|
|
2940
|
+
function safeProfile(accessToken) {
|
|
2941
|
+
try {
|
|
2942
|
+
return compactObject(profileFrom(decodeJwt(accessToken)));
|
|
2943
|
+
} catch {
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
var login2;
|
|
2948
|
+
var init_login2 = __esm(() => {
|
|
2949
|
+
init_config();
|
|
2950
|
+
init_errors();
|
|
2951
|
+
init_browser_import();
|
|
2952
|
+
init_login();
|
|
2953
|
+
init_jwt();
|
|
2954
|
+
init_define();
|
|
2955
|
+
login2 = defineTool({
|
|
2956
|
+
name: "login",
|
|
2957
|
+
description: "Salva uma sess\xE3o do Kotas em disco, cifrada. Tr\xEAs caminhos: e-mail e senha (`email`+`password`, " + "ou as vari\xE1veis KOTAS_EMAIL/KOTAS_SENHA); importando de um navegador j\xE1 logado " + "(`from_browser`); ou colando os tr\xEAs valores do localStorage (`tokens`). A senha \xE9 usada uma vez " + "e nunca \xE9 gravada. Se a conta tiver verifica\xE7\xE3o em duas etapas, passe `pin`.",
|
|
2958
|
+
readOnly: false,
|
|
2959
|
+
input: Type10.Object({
|
|
2960
|
+
email: Type10.Optional(Type10.String({ minLength: 3, description: "E-mail da conta Kotas" })),
|
|
2961
|
+
password: Type10.Optional(Type10.String({ minLength: 1, description: "Senha; usada uma vez, nunca gravada" })),
|
|
2962
|
+
pin: Type10.Optional(Type10.String({ pattern: "^\\d{4,8}$", description: "C\xF3digo de 6 d\xEDgitos da verifica\xE7\xE3o em duas etapas" })),
|
|
2963
|
+
from_browser: Type10.Optional(Type10.Union(IMPORT_BROWSERS.map((browser) => Type10.Literal(browser)), { description: "Importa a sess\xE3o do localStorage deste navegador (macOS)" })),
|
|
2964
|
+
tokens: Type10.Optional(Type10.Object({
|
|
2965
|
+
access_token: Type10.String({ minLength: 20, description: "localStorage `@chave`" }),
|
|
2966
|
+
refresh_token: Type10.String({
|
|
2967
|
+
minLength: 8,
|
|
2968
|
+
description: "localStorage `@c08cbbfd6eefc83ac6d23c4c791277e4`"
|
|
2969
|
+
}),
|
|
2970
|
+
device_hash: Type10.Optional(Type10.String({ description: "localStorage `SEFTSF9ESVNQT1NJVElWTw==`" }))
|
|
2971
|
+
}, { description: "Sess\xE3o colada \xE0 m\xE3o, do DevTools do navegador" }))
|
|
2972
|
+
}),
|
|
2973
|
+
run: async (args, ctx) => {
|
|
2974
|
+
let session;
|
|
2975
|
+
let via;
|
|
2976
|
+
if (args.tokens) {
|
|
2977
|
+
session = {
|
|
2978
|
+
version: 1,
|
|
2979
|
+
accessToken: args.tokens.access_token.trim(),
|
|
2980
|
+
refreshToken: args.tokens.refresh_token.trim(),
|
|
2981
|
+
hashDispositivo: args.tokens.device_hash?.trim() ?? "",
|
|
2982
|
+
savedAt: ctx.now()
|
|
2983
|
+
};
|
|
2984
|
+
ctx.session.save(session);
|
|
2985
|
+
ctx.http.adopt(session);
|
|
2986
|
+
via = "tokens colados";
|
|
2987
|
+
} else if (args.from_browser ?? ctx.config.importBrowser) {
|
|
2988
|
+
const browser = args.from_browser ?? ctx.config.importBrowser;
|
|
2989
|
+
const result = importFromBrowser(browser, ctx.now());
|
|
2990
|
+
session = result.session;
|
|
2991
|
+
ctx.session.save(session);
|
|
2992
|
+
ctx.http.adopt(session);
|
|
2993
|
+
via = `import do ${result.browser}`;
|
|
2994
|
+
} else {
|
|
2995
|
+
const email = args.email ?? ctx.config.email;
|
|
2996
|
+
const password = args.password ?? ctx.config.password;
|
|
2997
|
+
if (!email || !password) {
|
|
2998
|
+
throw new LoginError("Faltou e-mail ou senha. Passe `email` e `password`, defina KOTAS_EMAIL/KOTAS_SENHA, " + "ou use `from_browser` para importar uma sess\xE3o j\xE1 aberta no navegador.");
|
|
2999
|
+
}
|
|
3000
|
+
session = await login(ctx, compactObject({ email, password, pin: args.pin }));
|
|
3001
|
+
via = "e-mail e senha";
|
|
3002
|
+
}
|
|
3003
|
+
const profile = safeProfile(session.accessToken);
|
|
3004
|
+
return compactObject({
|
|
3005
|
+
loggedIn: true,
|
|
3006
|
+
via,
|
|
3007
|
+
sessionFile: ctx.session.paths.sessionPath,
|
|
3008
|
+
deviceBound: Boolean(session.hashDispositivo),
|
|
3009
|
+
profile,
|
|
3010
|
+
...session.hashDispositivo ? {} : {
|
|
3011
|
+
warning: "Sess\xE3o sem hashDispositivo. O Kotas pode pedir libera\xE7\xE3o de dispositivo (HTTP 412) na pr\xF3xima renova\xE7\xE3o."
|
|
3012
|
+
},
|
|
3013
|
+
next: "Rode `sync` para baixar o hist\xF3rico para o cache local."
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
// src/tools/payouts.ts
|
|
3020
|
+
import { Type as Type11 } from "@sinclair/typebox";
|
|
3021
|
+
var listPayouts;
|
|
3022
|
+
var init_payouts = __esm(() => {
|
|
3023
|
+
init_rows();
|
|
3024
|
+
init_define();
|
|
3025
|
+
listPayouts = defineTool({
|
|
3026
|
+
name: "list_payouts",
|
|
3027
|
+
description: "Lista os recebimentos como administrador: quanto cada grupo repassa por ciclo, quanto cada membro " + "paga e a data do pr\xF3ximo pagamento. L\xEA o cache, n\xE3o usa a rede. Com group_id devolve tamb\xE9m o " + "extrato daquele grupo, participante por participante. Status `Cancelado` significa que o repasse " + "n\xE3o vai mais acontecer \u2014 n\xE3o some no previsto.",
|
|
3028
|
+
readOnly: true,
|
|
3029
|
+
input: Type11.Object({
|
|
3030
|
+
group_id: Type11.Optional(Type11.Integer({ minimum: 1, description: "Tamb\xE9m traz o extrato deste grupo" })),
|
|
3031
|
+
only_scheduled: Type11.Optional(Type11.Boolean({ description: "S\xF3 os repasses agendados (default false)" }))
|
|
3032
|
+
}),
|
|
3033
|
+
run: (args, ctx) => {
|
|
3034
|
+
const cache = ctx.cache();
|
|
3035
|
+
const all = cache.listPayouts();
|
|
3036
|
+
const rows = args.only_scheduled ? all.filter((row) => row.status_id === 4) : all;
|
|
3037
|
+
const scheduled = all.filter((row) => row.status_id === 4);
|
|
3038
|
+
return compactObject({
|
|
3039
|
+
total: rows.length,
|
|
3040
|
+
scheduledTotal: money(sumUnits(scheduled.map((row) => row.amount_units))),
|
|
3041
|
+
payouts: rows.map(payoutOut),
|
|
3042
|
+
...args.group_id ? { statement: cache.getPayoutEntries(args.group_id).map(payoutEntryOut) } : {},
|
|
3043
|
+
...all.length === 0 ? { note: "Nenhum recebimento no cache. Rode `sync` \u2014 ou voc\xEA n\xE3o administra nenhum grupo." } : {}
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
});
|
|
3048
|
+
|
|
3049
|
+
// src/tools/raw.ts
|
|
3050
|
+
import { Type as Type12 } from "@sinclair/typebox";
|
|
3051
|
+
function assertAllowed(path) {
|
|
3052
|
+
if (path.includes(".."))
|
|
3053
|
+
throw new Error("path inv\xE1lido: n\xE3o pode conter '..'");
|
|
3054
|
+
if (CREDENTIALS_PATH.test(path)) {
|
|
3055
|
+
throw new Error("Rota bloqueada: `grupo/obter-dados-acesso` devolve o login e a senha do servi\xE7o compartilhado em texto claro. " + "O kotas-mcp nunca exp\xF5e essas credenciais. Pegue-as no app do Kotas.");
|
|
3056
|
+
}
|
|
3057
|
+
if (FORBIDDEN_PATH.test(path)) {
|
|
3058
|
+
throw new Error(`Rota recusada: "${path}" parece uma rota de escrita, e o kotas-mcp \xE9 somente leitura \u2014 nunca altera a conta.`);
|
|
3059
|
+
}
|
|
3060
|
+
const bare = path.split("?")[0] ?? path;
|
|
3061
|
+
const allowed = ALLOWED_EXACT.includes(bare) || ALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
3062
|
+
if (!allowed) {
|
|
3063
|
+
throw new Error(`Rota fora do escopo: "${path}". Aceitas: ${[...ALLOWED_EXACT, ...ALLOWED_PREFIXES].join(", ")}.`);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
var ALLOWED_EXACT, ALLOWED_PREFIXES, FORBIDDEN_PATH, CREDENTIALS_PATH, MAX_RAW_BYTES, rawGet;
|
|
3067
|
+
var init_raw = __esm(() => {
|
|
3068
|
+
init_define();
|
|
3069
|
+
ALLOWED_EXACT = ["credito", "cartao", "identificador", "cupom_indicacao"];
|
|
3070
|
+
ALLOWED_PREFIXES = [
|
|
3071
|
+
"grupo/",
|
|
3072
|
+
"assinatura/restricoes/",
|
|
3073
|
+
"fatura/",
|
|
3074
|
+
"credito/",
|
|
3075
|
+
"recebimento/",
|
|
3076
|
+
"servico/",
|
|
3077
|
+
"notificacoes",
|
|
3078
|
+
"notificacao/",
|
|
3079
|
+
"usuario/cadastro/dados-fiscais",
|
|
3080
|
+
"usuario/obter-endereco-fiscal",
|
|
3081
|
+
"cartao",
|
|
3082
|
+
"meio-pagamento/",
|
|
3083
|
+
"contabanco/",
|
|
3084
|
+
"retirada/mes",
|
|
3085
|
+
"retirada/disponibilidade-boleto",
|
|
3086
|
+
"reclamacao/",
|
|
3087
|
+
"rede/obter",
|
|
3088
|
+
"indicacao/",
|
|
3089
|
+
"loteria/",
|
|
3090
|
+
"api/versao"
|
|
3091
|
+
];
|
|
3092
|
+
FORBIDDEN_PATH = /(cancelar|cancelamento|pagar|pagamento|retirar|retirada\/retirar|alterar|alterarnome|remover|excluir|deletar|criar|enviar|assinar|comprar|estornar|liberar|reservar|ativar|desativar|bloquear|validar|aceitar|recusar|resetar|checkout|convite)/i;
|
|
3093
|
+
CREDENTIALS_PATH = /obter-dados-acesso|dados-acesso/i;
|
|
3094
|
+
MAX_RAW_BYTES = 64 * 1024;
|
|
3095
|
+
rawGet = defineTool({
|
|
3096
|
+
name: "raw_get",
|
|
3097
|
+
description: "Chama uma rota GET da API do Kotas diretamente, com a mesma sess\xE3o, o mesmo ritmo e os mesmos " + "headers das outras tools. Serve para redescobrir um endpoint quando a API muda. Use com " + "parcim\xF4nia e nunca em rajada. Somente leitura: qualquer rota com verbo de escrita (cancelar, " + "pagar, retirar, alterar\u2026) \xE9 recusada, e as credenciais do servi\xE7o compartilhado " + "(`grupo/obter-dados-acesso`) s\xE3o bloqueadas em qualquer forma.",
|
|
3098
|
+
readOnly: true,
|
|
3099
|
+
input: Type12.Object({
|
|
3100
|
+
path: Type12.String({
|
|
3101
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_/.-]*$",
|
|
3102
|
+
description: "Rota relativa, sem barra inicial (ex.: grupo/categorias)"
|
|
3103
|
+
}),
|
|
3104
|
+
query: Type12.Optional(Type12.Record(Type12.String(), Type12.Union([Type12.String(), Type12.Number(), Type12.Boolean()]), {
|
|
3105
|
+
description: 'Par\xE2metros de query (ex.: { "page": 1, "pageSize": 50 })'
|
|
3106
|
+
})),
|
|
3107
|
+
max_bytes: Type12.Optional(Type12.Integer({
|
|
3108
|
+
minimum: 1024,
|
|
3109
|
+
maximum: MAX_RAW_BYTES,
|
|
3110
|
+
description: `Corta a resposta neste tamanho (default ${MAX_RAW_BYTES})`
|
|
3111
|
+
}))
|
|
3112
|
+
}),
|
|
3113
|
+
run: async (args, ctx) => {
|
|
3114
|
+
const path = args.path.trim().replace(/^\/+/, "");
|
|
3115
|
+
assertAllowed(path);
|
|
3116
|
+
const payload = await ctx.api.getRaw(path, args.query ?? {});
|
|
3117
|
+
const limit = args.max_bytes ?? MAX_RAW_BYTES;
|
|
3118
|
+
const serialized = JSON.stringify(payload ?? null);
|
|
3119
|
+
const truncated = serialized.length > limit;
|
|
3120
|
+
return {
|
|
3121
|
+
path,
|
|
3122
|
+
bytes: serialized.length,
|
|
3123
|
+
truncated,
|
|
3124
|
+
data: truncated ? `${serialized.slice(0, limit)}\u2026` : payload ?? null
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
// src/tools/services.ts
|
|
3131
|
+
import { Type as Type13 } from "@sinclair/typebox";
|
|
3132
|
+
var searchServices;
|
|
3133
|
+
var init_services = __esm(() => {
|
|
3134
|
+
init_define();
|
|
3135
|
+
searchServices = defineTool({
|
|
3136
|
+
name: "search_services",
|
|
3137
|
+
description: "Busca servi\xE7os no cat\xE1logo do Kotas pelo nome (Netflix, Spotify, Google One\u2026) e devolve os planos " + "dispon\xEDveis com o valor cheio e quantas kotas cada um tem. Gasta 1 requisi\xE7\xE3o. Isto \xE9 o CAT\xC1LOGO: " + "para o que voc\xEA j\xE1 assinou ou pagou, use `purchase_history` ou `list_invoices`.",
|
|
3138
|
+
readOnly: true,
|
|
3139
|
+
input: Type13.Object({
|
|
3140
|
+
query: Type13.String({ minLength: 2, description: "Nome ou parte do nome do servi\xE7o" }),
|
|
3141
|
+
limit: Type13.Optional(Type13.Integer({ minimum: 1, maximum: 50, description: "M\xE1ximo de servi\xE7os (default 10)" }))
|
|
3142
|
+
}),
|
|
3143
|
+
run: async (args, ctx) => {
|
|
3144
|
+
const services = await ctx.api.searchServices(args.query.trim());
|
|
3145
|
+
const limited = services.slice(0, args.limit ?? 10);
|
|
3146
|
+
return compactObject({
|
|
3147
|
+
total: services.length,
|
|
3148
|
+
services: limited.map((service) => compactObject({
|
|
3149
|
+
id: service.id,
|
|
3150
|
+
name: service.nome,
|
|
3151
|
+
category: service.nomeCategoria,
|
|
3152
|
+
site: service.site,
|
|
3153
|
+
price: money(toUnits(service.valor)),
|
|
3154
|
+
slots: service.qtdKotas,
|
|
3155
|
+
plans: (service.planos ?? []).map((plan) => compactObject({ id: plan.id, name: plan.nome, price: money(toUnits(plan.valor)) }))
|
|
3156
|
+
})),
|
|
3157
|
+
...services.length === 0 ? { note: `Nada encontrado para "${args.query}".` } : {}
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
});
|
|
3162
|
+
|
|
3163
|
+
// src/tools/subscriptions.ts
|
|
3164
|
+
import { Type as Type14 } from "@sinclair/typebox";
|
|
3165
|
+
var NO_CACHE2 = "O cache est\xE1 vazio. Rode `sync` (ou `kotas sync` no terminal) antes de perguntar.", listSubscriptions, getSubscription;
|
|
3166
|
+
var init_subscriptions = __esm(() => {
|
|
3167
|
+
init_normalize();
|
|
3168
|
+
init_rows();
|
|
3169
|
+
init_define();
|
|
3170
|
+
init_fields();
|
|
3171
|
+
listSubscriptions = defineTool({
|
|
3172
|
+
name: "list_subscriptions",
|
|
3173
|
+
description: "Lista as assinaturas compartilhadas (grupos) do Kotas a partir do cache local, com o que voc\xEA paga " + "em cada uma, quantas vagas tem e se voc\xEA \xE9 membro ou administrador. N\xE3o usa a rede. S\xF3 aparecem " + "grupos que ainda existem na API: um grupo encerrado some do Kotas e s\xF3 sobrevive nas faturas \u2014 " + "para o hist\xF3rico completo use `purchase_history`.",
|
|
3174
|
+
readOnly: true,
|
|
3175
|
+
input: Type14.Object({
|
|
3176
|
+
role: Type14.Optional(Type14.Union([Type14.Literal("todos"), Type14.Literal("membro"), Type14.Literal("administrador")], {
|
|
3177
|
+
description: "Filtra pelo seu papel no grupo (default todos)"
|
|
3178
|
+
})),
|
|
3179
|
+
limit: limitField(200, 100),
|
|
3180
|
+
compact: compactField
|
|
3181
|
+
}),
|
|
3182
|
+
run: (args, ctx) => {
|
|
3183
|
+
const cache = ctx.cache();
|
|
3184
|
+
const options = { limit: args.limit ?? 100 };
|
|
3185
|
+
if (args.role === "administrador")
|
|
3186
|
+
options.administrator = true;
|
|
3187
|
+
if (args.role === "membro")
|
|
3188
|
+
options.administrator = false;
|
|
3189
|
+
const rows = cache.listGroups(options);
|
|
3190
|
+
return compactObject({
|
|
3191
|
+
total: rows.length,
|
|
3192
|
+
subscriptions: rows.map((row) => groupOut(row, args.compact === true)),
|
|
3193
|
+
...rows.length === 0 && cache.stats().groups === 0 ? { note: NO_CACHE2 } : {}
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
getSubscription = defineTool({
|
|
3198
|
+
name: "get_subscription",
|
|
3199
|
+
description: "Detalha uma assinatura: plano, o que voc\xEA paga, o valor cheio do servi\xE7o, a taxa da Kotas, vagas, " + "fidelidade e os participantes com o que cada um paga \u2014 a API devolve os participantes tanto para " + "membro quanto para administrador. L\xEA o cache; se o grupo n\xE3o estiver l\xE1, busca 1 vez na API. N\xC3O " + "devolve login nem senha do servi\xE7o compartilhado: o kotas-mcp nunca exp\xF5e essas credenciais.",
|
|
3200
|
+
readOnly: true,
|
|
3201
|
+
input: Type14.Object({ group_id: groupIdField, compact: compactField }),
|
|
3202
|
+
run: async (args, ctx) => {
|
|
3203
|
+
const cache = ctx.cache();
|
|
3204
|
+
let row = cache.getGroup(args.group_id);
|
|
3205
|
+
if (!row) {
|
|
3206
|
+
const raw = await ctx.api.getGroup(args.group_id, "member").catch(async (error) => {
|
|
3207
|
+
const admin = await ctx.api.getGroup(args.group_id, "admin").catch(() => null);
|
|
3208
|
+
if (admin)
|
|
3209
|
+
return admin;
|
|
3210
|
+
throw error;
|
|
3211
|
+
});
|
|
3212
|
+
const group = normalizeGroup(raw);
|
|
3213
|
+
if (!group)
|
|
3214
|
+
throw new Error(`O Kotas devolveu um grupo sem \`id\` para ${args.group_id}.`);
|
|
3215
|
+
cache.setGroupDetail(group, normalizeParticipants(group.id, raw), raw);
|
|
3216
|
+
row = cache.getGroup(args.group_id);
|
|
3217
|
+
}
|
|
3218
|
+
if (!row)
|
|
3219
|
+
throw new Error(`Grupo ${args.group_id} n\xE3o encontrado.`);
|
|
3220
|
+
const participants = cache.getParticipants(row.id);
|
|
3221
|
+
return compactObject({
|
|
3222
|
+
subscription: groupOut(row, args.compact === true),
|
|
3223
|
+
...participants.length > 0 ? { participants: participants.map(participantOut) } : { participants: [], note: "Sem participantes no cache; rode `sync`." }
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
});
|
|
3228
|
+
|
|
3229
|
+
// src/cache/sync.ts
|
|
3230
|
+
function resolveMode(cache, requested, cursor) {
|
|
3231
|
+
if (requested === "reparse" || requested === "full")
|
|
3232
|
+
return requested;
|
|
3233
|
+
if (cursor && cursor.mode === "full")
|
|
3234
|
+
return "full";
|
|
3235
|
+
return cache.getMeta(META.lastFull) === null ? "full" : "incremental";
|
|
3236
|
+
}
|
|
3237
|
+
function readCursor(cache) {
|
|
3238
|
+
const raw = cache.getMeta(META.cursor);
|
|
3239
|
+
if (!raw)
|
|
3240
|
+
return null;
|
|
3241
|
+
try {
|
|
3242
|
+
return JSON.parse(raw);
|
|
3243
|
+
} catch {
|
|
3244
|
+
return null;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
function reparseAll(cache) {
|
|
3248
|
+
let count = 0;
|
|
3249
|
+
const pending = cache.invoicesToReparse(PARSER_VERSION);
|
|
3250
|
+
const invoices = [];
|
|
3251
|
+
const raws = [];
|
|
3252
|
+
for (const row of pending) {
|
|
3253
|
+
let raw;
|
|
3254
|
+
try {
|
|
3255
|
+
raw = JSON.parse(row.raw_list);
|
|
3256
|
+
} catch {
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
const invoice = normalizeInvoice(raw);
|
|
3260
|
+
if (!invoice)
|
|
3261
|
+
continue;
|
|
3262
|
+
invoices.push(invoice);
|
|
3263
|
+
raws.push(raw);
|
|
3264
|
+
count += 1;
|
|
3265
|
+
}
|
|
3266
|
+
if (invoices.length > 0)
|
|
3267
|
+
cache.upsertInvoices(invoices, raws, PARSER_VERSION);
|
|
3268
|
+
return count;
|
|
3269
|
+
}
|
|
3270
|
+
function fatal(error) {
|
|
3271
|
+
return error instanceof KotasAuthError;
|
|
3272
|
+
}
|
|
3273
|
+
async function runSyncChunk(ctx, options = {}) {
|
|
3274
|
+
const cache = ctx.cache();
|
|
3275
|
+
const budget = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
|
|
3276
|
+
const cursor = readCursor(cache);
|
|
3277
|
+
const mode = resolveMode(cache, options.mode, cursor);
|
|
3278
|
+
const report2 = {
|
|
3279
|
+
mode,
|
|
3280
|
+
done: false,
|
|
3281
|
+
requestsUsed: 0,
|
|
3282
|
+
invoices: 0,
|
|
3283
|
+
invoiceItems: 0,
|
|
3284
|
+
groups: 0,
|
|
3285
|
+
groupErrors: 0,
|
|
3286
|
+
credits: 0,
|
|
3287
|
+
payouts: 0,
|
|
3288
|
+
reparsed: 0,
|
|
3289
|
+
pendingDetails: 0,
|
|
3290
|
+
errors: []
|
|
3291
|
+
};
|
|
3292
|
+
if (mode === "reparse") {
|
|
3293
|
+
report2.reparsed = reparseAll(cache);
|
|
3294
|
+
cache.rebuildFts();
|
|
3295
|
+
report2.done = true;
|
|
3296
|
+
report2.pendingDetails = cache.pendingInvoiceDetails(1e4).length;
|
|
3297
|
+
return report2;
|
|
3298
|
+
}
|
|
3299
|
+
const before = ctx.http.state().requests;
|
|
3300
|
+
const spent = () => ctx.http.state().requests - before;
|
|
3301
|
+
const canSpend = () => spent() < budget;
|
|
3302
|
+
const resumed = cursor?.mode === mode ? cursor : null;
|
|
3303
|
+
let statusIndex = resumed?.statusIndex ?? 0;
|
|
3304
|
+
let page = resumed?.page ?? 1;
|
|
3305
|
+
let invoicesDone = resumed?.invoicesDone ?? false;
|
|
3306
|
+
let groupsListed = resumed?.groupsListed ?? false;
|
|
3307
|
+
let creditsActiveDone = resumed?.creditsActiveDone ?? false;
|
|
3308
|
+
let creditsHistoryDone = resumed?.creditsHistoryDone ?? false;
|
|
3309
|
+
let payoutsListed = resumed?.payoutsListed ?? false;
|
|
3310
|
+
const startedAt = resumed?.startedAt ?? ctx.now();
|
|
3311
|
+
while (!invoicesDone && canSpend()) {
|
|
3312
|
+
const statusId = INVOICE_STATUS_IDS[statusIndex];
|
|
3313
|
+
if (statusId === undefined) {
|
|
3314
|
+
invoicesDone = true;
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
3317
|
+
let result;
|
|
3318
|
+
try {
|
|
3319
|
+
result = await ctx.api.listInvoices(statusId, page, PAGE_SIZE);
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
if (fatal(error))
|
|
3322
|
+
throw error;
|
|
3323
|
+
report2.errors.push(`faturas status ${statusId} p\xE1gina ${page}: ${messageOf2(error)}`);
|
|
3324
|
+
statusIndex += 1;
|
|
3325
|
+
page = 1;
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const invoices = [];
|
|
3329
|
+
const raws = [];
|
|
3330
|
+
let fresh = 0;
|
|
3331
|
+
for (const raw of result.items) {
|
|
3332
|
+
const invoice = normalizeInvoice(raw);
|
|
3333
|
+
if (!invoice)
|
|
3334
|
+
continue;
|
|
3335
|
+
const known = cache.getInvoice(invoice.id);
|
|
3336
|
+
if (!known || known.status_id !== invoice.statusId)
|
|
3337
|
+
fresh += 1;
|
|
3338
|
+
invoices.push(invoice);
|
|
3339
|
+
raws.push(raw);
|
|
3340
|
+
}
|
|
3341
|
+
report2.invoices += cache.upsertInvoices(invoices, raws, PARSER_VERSION);
|
|
3342
|
+
const exhausted = !result.hasMore || page >= MAX_PAGES_PER_STATUS || mode === "incremental" && fresh === 0 && result.items.length > 0;
|
|
3343
|
+
if (exhausted) {
|
|
3344
|
+
statusIndex += 1;
|
|
3345
|
+
page = 1;
|
|
3346
|
+
if (statusIndex >= INVOICE_STATUS_IDS.length)
|
|
3347
|
+
invoicesDone = true;
|
|
3348
|
+
} else {
|
|
3349
|
+
page += 1;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
const wantGroups = options.withGroups !== false;
|
|
3353
|
+
if (invoicesDone && wantGroups && !groupsListed && canSpend()) {
|
|
3354
|
+
try {
|
|
3355
|
+
const groups = [];
|
|
3356
|
+
const raws = [];
|
|
3357
|
+
let groupPage = 0;
|
|
3358
|
+
for (;; ) {
|
|
3359
|
+
const result = await ctx.api.listGroups(groupPage, PAGE_SIZE);
|
|
3360
|
+
for (const raw of result.items) {
|
|
3361
|
+
const group = normalizeGroup(raw);
|
|
3362
|
+
if (group) {
|
|
3363
|
+
groups.push(group);
|
|
3364
|
+
raws.push(raw);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
if (!result.hasMore) {
|
|
3368
|
+
groupsListed = true;
|
|
3369
|
+
break;
|
|
3370
|
+
}
|
|
3371
|
+
if (!canSpend())
|
|
3372
|
+
break;
|
|
3373
|
+
groupPage += 1;
|
|
3374
|
+
}
|
|
3375
|
+
report2.groups = cache.upsertGroups(groups, raws);
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
if (fatal(error))
|
|
3378
|
+
throw error;
|
|
3379
|
+
report2.errors.push(`grupos: ${messageOf2(error)}`);
|
|
3380
|
+
groupsListed = true;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
if (invoicesDone && wantGroups) {
|
|
3384
|
+
while (canSpend()) {
|
|
3385
|
+
const [groupId] = cache.pendingGroupDetails(1);
|
|
3386
|
+
if (groupId === undefined)
|
|
3387
|
+
break;
|
|
3388
|
+
const known = cache.getGroup(groupId);
|
|
3389
|
+
try {
|
|
3390
|
+
const raw = await ctx.api.getGroup(groupId, known?.is_administrator === 1 ? "admin" : "member");
|
|
3391
|
+
const detailed = normalizeGroup({ ...raw, inAdministrador: known?.is_administrator === 1 });
|
|
3392
|
+
if (!detailed)
|
|
3393
|
+
throw new ParseError(`grupo ${groupId} veio sem \`id\`.`);
|
|
3394
|
+
cache.setGroupDetail(detailed, normalizeParticipants(groupId, raw), raw);
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
if (fatal(error))
|
|
3397
|
+
throw error;
|
|
3398
|
+
report2.groupErrors += 1;
|
|
3399
|
+
cache.markGroupDetailError(groupId, messageOf2(error));
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const wantCredits = options.withCredits !== false;
|
|
3404
|
+
for (const active of [true, false]) {
|
|
3405
|
+
const alreadyDone = active ? creditsActiveDone : creditsHistoryDone;
|
|
3406
|
+
if (!invoicesDone || !wantCredits || alreadyDone || !canSpend())
|
|
3407
|
+
continue;
|
|
3408
|
+
try {
|
|
3409
|
+
const credits = [];
|
|
3410
|
+
const raws = [];
|
|
3411
|
+
let creditPage = 1;
|
|
3412
|
+
for (;; ) {
|
|
3413
|
+
const result = await ctx.api.listCredits(creditPage, PAGE_SIZE, active);
|
|
3414
|
+
for (const raw of result.items) {
|
|
3415
|
+
const credit = normalizeCredit(raw);
|
|
3416
|
+
if (credit) {
|
|
3417
|
+
credits.push(credit);
|
|
3418
|
+
raws.push(raw);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (!result.hasMore || creditPage >= MAX_PAGES_PER_STATUS) {
|
|
3422
|
+
if (active)
|
|
3423
|
+
creditsActiveDone = true;
|
|
3424
|
+
else
|
|
3425
|
+
creditsHistoryDone = true;
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3428
|
+
if (!canSpend())
|
|
3429
|
+
break;
|
|
3430
|
+
creditPage += 1;
|
|
3431
|
+
}
|
|
3432
|
+
report2.credits += cache.upsertCredits(credits, raws);
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
if (fatal(error))
|
|
3435
|
+
throw error;
|
|
3436
|
+
report2.errors.push(`cr\xE9ditos (ativos=${active}): ${messageOf2(error)}`);
|
|
3437
|
+
if (active)
|
|
3438
|
+
creditsActiveDone = true;
|
|
3439
|
+
else
|
|
3440
|
+
creditsHistoryDone = true;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
const wantPayouts = options.withPayouts !== false;
|
|
3444
|
+
if (invoicesDone && wantPayouts && !payoutsListed && canSpend()) {
|
|
3445
|
+
try {
|
|
3446
|
+
const payouts = [];
|
|
3447
|
+
const raws = [];
|
|
3448
|
+
let payoutPage = 0;
|
|
3449
|
+
for (;; ) {
|
|
3450
|
+
const result = await ctx.api.listPayouts(payoutPage, PAGE_SIZE);
|
|
3451
|
+
for (const raw of result.items) {
|
|
3452
|
+
const payout = normalizePayout(raw);
|
|
3453
|
+
if (payout) {
|
|
3454
|
+
payouts.push(payout);
|
|
3455
|
+
raws.push(raw);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
if (!result.hasMore) {
|
|
3459
|
+
payoutsListed = true;
|
|
3460
|
+
break;
|
|
3461
|
+
}
|
|
3462
|
+
if (!canSpend())
|
|
3463
|
+
break;
|
|
3464
|
+
payoutPage += 1;
|
|
3465
|
+
}
|
|
3466
|
+
report2.payouts = cache.upsertPayouts(payouts, raws);
|
|
3467
|
+
} catch (error) {
|
|
3468
|
+
if (fatal(error))
|
|
3469
|
+
throw error;
|
|
3470
|
+
report2.errors.push(`recebimentos: ${messageOf2(error)}`);
|
|
3471
|
+
payoutsListed = true;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
if (invoicesDone && wantPayouts) {
|
|
3475
|
+
while (canSpend()) {
|
|
3476
|
+
const [groupId] = cache.pendingPayoutStatements(1);
|
|
3477
|
+
if (groupId === undefined)
|
|
3478
|
+
break;
|
|
3479
|
+
try {
|
|
3480
|
+
const entries = await ctx.api.getPayoutStatement(groupId);
|
|
3481
|
+
cache.setPayoutEntries(groupId, entries.map((entry) => normalizePayoutEntry(groupId, entry)).filter((entry) => entry !== null));
|
|
3482
|
+
} catch (error) {
|
|
3483
|
+
if (fatal(error))
|
|
3484
|
+
throw error;
|
|
3485
|
+
report2.errors.push(`extrato do grupo ${groupId}: ${messageOf2(error)}`);
|
|
3486
|
+
cache.setPayoutEntries(groupId, []);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
if (invoicesDone && options.withInvoiceItems !== false) {
|
|
3491
|
+
while (canSpend()) {
|
|
3492
|
+
const [invoiceId] = cache.pendingInvoiceDetails(1);
|
|
3493
|
+
if (invoiceId === undefined)
|
|
3494
|
+
break;
|
|
3495
|
+
try {
|
|
3496
|
+
const items = await ctx.api.getInvoiceItems(invoiceId);
|
|
3497
|
+
cache.setInvoiceItems(invoiceId, items.map((item, position) => ({
|
|
3498
|
+
invoiceId,
|
|
3499
|
+
position,
|
|
3500
|
+
id: typeof item.id === "number" ? item.id : null,
|
|
3501
|
+
description: typeof item.descricao === "string" ? item.descricao : "",
|
|
3502
|
+
quantity: typeof item.quantidade === "number" ? item.quantidade : 1,
|
|
3503
|
+
amount: typeof item.valor4Casas === "number" ? Math.round(item.valor4Casas * 1e4) : typeof item.valor === "number" ? Math.round(item.valor * 1e4) : null
|
|
3504
|
+
})), items);
|
|
3505
|
+
report2.invoiceItems += items.length;
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
if (fatal(error))
|
|
3508
|
+
throw error;
|
|
3509
|
+
if (error instanceof KotasApiError || error instanceof ParseError || error instanceof RateLimitError) {
|
|
3510
|
+
cache.markInvoiceDetailError(invoiceId, messageOf2(error));
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
throw error;
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
report2.requestsUsed = spent();
|
|
3518
|
+
report2.pendingDetails = cache.pendingInvoiceDetails(1e4).length;
|
|
3519
|
+
const enrichmentDone = (!wantGroups || groupsListed && cache.pendingGroupDetails(1).length === 0) && (!wantCredits || creditsActiveDone && creditsHistoryDone) && (!wantPayouts || payoutsListed && cache.pendingPayoutStatements(1).length === 0);
|
|
3520
|
+
report2.done = invoicesDone && enrichmentDone && (options.withInvoiceItems === false || report2.pendingDetails === 0);
|
|
3521
|
+
if (report2.done) {
|
|
3522
|
+
cache.rebuildFts();
|
|
3523
|
+
cache.setMeta(META.cursor, null);
|
|
3524
|
+
const finishedAt = new Date(ctx.now()).toISOString();
|
|
3525
|
+
cache.setMeta(META.lastCompleted, finishedAt);
|
|
3526
|
+
if (mode === "full")
|
|
3527
|
+
cache.setMeta(META.lastFull, finishedAt);
|
|
3528
|
+
} else {
|
|
3529
|
+
cache.setMeta(META.cursor, JSON.stringify({
|
|
3530
|
+
mode,
|
|
3531
|
+
statusIndex,
|
|
3532
|
+
page,
|
|
3533
|
+
invoicesDone,
|
|
3534
|
+
groupsListed,
|
|
3535
|
+
creditsActiveDone,
|
|
3536
|
+
creditsHistoryDone,
|
|
3537
|
+
payoutsListed,
|
|
3538
|
+
startedAt
|
|
3539
|
+
}));
|
|
3540
|
+
report2.hint = invoicesDone ? `Faltam ${report2.pendingDetails} detalhes de fatura. Chame \`sync\` de novo com os mesmos par\xE2metros at\xE9 \`done: true\`.` : "A varredura de faturas n\xE3o terminou. Chame `sync` de novo com os mesmos par\xE2metros at\xE9 `done: true`.";
|
|
3541
|
+
}
|
|
3542
|
+
return report2;
|
|
3543
|
+
}
|
|
3544
|
+
var PARSER_VERSION = 1, DEFAULT_MAX_REQUESTS = 40, PAGE_SIZE = 50, MAX_PAGES_PER_STATUS = 60, messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
3545
|
+
var init_sync = __esm(() => {
|
|
3546
|
+
init_errors();
|
|
3547
|
+
init_enums();
|
|
3548
|
+
init_normalize();
|
|
3549
|
+
init_db();
|
|
3550
|
+
});
|
|
3551
|
+
|
|
3552
|
+
// src/tools/sync.ts
|
|
3553
|
+
import { Type as Type15 } from "@sinclair/typebox";
|
|
3554
|
+
var sync;
|
|
3555
|
+
var init_sync2 = __esm(() => {
|
|
3556
|
+
init_sync();
|
|
3557
|
+
init_define();
|
|
3558
|
+
sync = defineTool({
|
|
3559
|
+
name: "sync",
|
|
3560
|
+
description: "Baixa o hist\xF3rico do Kotas para o cache local. Trabalha em blocos: faz at\xE9 max_requests chamadas e " + "devolve `done: false` com um `hint`. CHAME DE NOVO com os mesmos par\xE2metros at\xE9 `done: true`. " + "Nunca chame em paralelo nem dispare outras tools de rede junto. Varre as faturas dos cinco status " + "(\xE9 a \xFAnica fonte do hist\xF3rico), depois grupos, cr\xE9ditos e recebimentos. " + "`mode: reparse` reprocessa o que j\xE1 est\xE1 no cache sem usar a rede.",
|
|
3561
|
+
readOnly: false,
|
|
3562
|
+
input: Type15.Object({
|
|
3563
|
+
mode: Type15.Optional(Type15.Union([Type15.Literal("incremental"), Type15.Literal("full"), Type15.Literal("reparse")], {
|
|
3564
|
+
description: "incremental (default depois do primeiro full) para no primeiro bloco sem novidade; full varre tudo; reparse n\xE3o usa a rede"
|
|
3565
|
+
})),
|
|
3566
|
+
max_requests: Type15.Optional(Type15.Integer({
|
|
3567
|
+
minimum: 1,
|
|
3568
|
+
maximum: 200,
|
|
3569
|
+
description: `Or\xE7amento de requisi\xE7\xF5es do bloco (default ${DEFAULT_MAX_REQUESTS})`
|
|
3570
|
+
})),
|
|
3571
|
+
with_groups: Type15.Optional(Type15.Boolean({ description: "Tamb\xE9m sincroniza os grupos (default true)" })),
|
|
3572
|
+
with_credits: Type15.Optional(Type15.Boolean({ description: "Tamb\xE9m sincroniza os cr\xE9ditos (default true)" })),
|
|
3573
|
+
with_payouts: Type15.Optional(Type15.Boolean({ description: "Tamb\xE9m sincroniza os recebimentos de administrador (default true)" })),
|
|
3574
|
+
with_invoice_items: Type15.Optional(Type15.Boolean({ description: "Tamb\xE9m busca os itens de cada fatura, 1 requisi\xE7\xE3o por fatura (default true)" }))
|
|
3575
|
+
}),
|
|
3576
|
+
run: (args, ctx) => runSyncChunk(ctx, {
|
|
3577
|
+
...args.mode ? { mode: args.mode } : {},
|
|
3578
|
+
...args.max_requests === undefined ? {} : { maxRequests: args.max_requests },
|
|
3579
|
+
...args.with_groups === undefined ? {} : { withGroups: args.with_groups },
|
|
3580
|
+
...args.with_credits === undefined ? {} : { withCredits: args.with_credits },
|
|
3581
|
+
...args.with_payouts === undefined ? {} : { withPayouts: args.with_payouts },
|
|
3582
|
+
...args.with_invoice_items === undefined ? {} : { withInvoiceItems: args.with_invoice_items }
|
|
3583
|
+
})
|
|
3584
|
+
});
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
// src/tools/registry.ts
|
|
3588
|
+
function activeTools(config) {
|
|
3589
|
+
return config.readOnly ? allTools.filter((tool) => tool.readOnly) : allTools;
|
|
3590
|
+
}
|
|
3591
|
+
function toolByName(name) {
|
|
3592
|
+
return allTools.find((tool) => tool.name === name);
|
|
3593
|
+
}
|
|
3594
|
+
var allTools;
|
|
3595
|
+
var init_registry = __esm(() => {
|
|
3596
|
+
init_analytics2();
|
|
3597
|
+
init_auth();
|
|
3598
|
+
init_credits();
|
|
3599
|
+
init_doctor();
|
|
3600
|
+
init_export();
|
|
3601
|
+
init_invoices();
|
|
3602
|
+
init_login2();
|
|
3603
|
+
init_payouts();
|
|
3604
|
+
init_raw();
|
|
3605
|
+
init_services();
|
|
3606
|
+
init_subscriptions();
|
|
3607
|
+
init_sync2();
|
|
3608
|
+
allTools = [
|
|
3609
|
+
authStatus,
|
|
3610
|
+
login2,
|
|
3611
|
+
doctor,
|
|
3612
|
+
sync,
|
|
3613
|
+
listSubscriptions,
|
|
3614
|
+
getSubscription,
|
|
3615
|
+
listInvoices,
|
|
3616
|
+
getInvoice,
|
|
3617
|
+
listCredits,
|
|
3618
|
+
balance,
|
|
3619
|
+
listPayouts,
|
|
3620
|
+
purchaseHistory2,
|
|
3621
|
+
spendingSummary2,
|
|
3622
|
+
searchServices,
|
|
3623
|
+
exportData,
|
|
3624
|
+
rawGet
|
|
3625
|
+
];
|
|
3626
|
+
});
|
|
3627
|
+
|
|
3628
|
+
// src/mcp/server.ts
|
|
3629
|
+
var exports_server = {};
|
|
3630
|
+
__export(exports_server, {
|
|
3631
|
+
startMcpServer: () => startMcpServer,
|
|
3632
|
+
SERVER_NAME: () => SERVER_NAME
|
|
3633
|
+
});
|
|
3634
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3635
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3636
|
+
import {
|
|
3637
|
+
CallToolRequestSchema,
|
|
3638
|
+
ListToolsRequestSchema
|
|
3639
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
3640
|
+
async function startMcpServer(ctx, version) {
|
|
3641
|
+
const tools = activeTools(ctx.config);
|
|
3642
|
+
const server = new Server({ name: SERVER_NAME, version }, { capabilities: { tools: {} } });
|
|
3643
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3644
|
+
tools: tools.map((tool) => ({
|
|
3645
|
+
name: tool.name,
|
|
3646
|
+
description: tool.description,
|
|
3647
|
+
inputSchema: tool.input,
|
|
3648
|
+
annotations: {
|
|
3649
|
+
readOnlyHint: tool.readOnly,
|
|
3650
|
+
destructiveHint: false,
|
|
3651
|
+
idempotentHint: tool.readOnly,
|
|
3652
|
+
openWorldHint: true
|
|
3653
|
+
}
|
|
3654
|
+
}))
|
|
3655
|
+
}));
|
|
3656
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3657
|
+
const tool = tools.find((candidate) => candidate.name === request.params.name);
|
|
3658
|
+
if (!tool)
|
|
3659
|
+
return toolError(`Tool desconhecida: ${request.params.name}`);
|
|
3660
|
+
try {
|
|
3661
|
+
const result = await runTool(tool, request.params.arguments ?? {}, ctx);
|
|
3662
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
return toolError(error instanceof Error ? error.message : String(error));
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
await server.connect(new StdioServerTransport);
|
|
3668
|
+
ctx.log.info(`${SERVER_NAME} pronto (${tools.length} tools)`);
|
|
3669
|
+
}
|
|
3670
|
+
function toolError(message2) {
|
|
3671
|
+
return { isError: true, content: [{ type: "text", text: message2 }] };
|
|
3672
|
+
}
|
|
3673
|
+
var SERVER_NAME = "kotas-mcp";
|
|
3674
|
+
var init_server = __esm(() => {
|
|
3675
|
+
init_define();
|
|
3676
|
+
init_registry();
|
|
3677
|
+
});
|
|
3678
|
+
|
|
3679
|
+
// src/cli/render.ts
|
|
3680
|
+
function format(value) {
|
|
3681
|
+
if (value === null || value === undefined)
|
|
3682
|
+
return "-";
|
|
3683
|
+
if (isMoney(value)) {
|
|
3684
|
+
return value.amount.toLocaleString("pt-BR", {
|
|
3685
|
+
style: "currency",
|
|
3686
|
+
currency: value.currency,
|
|
3687
|
+
minimumFractionDigits: 2
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
if (typeof value === "boolean")
|
|
3691
|
+
return value ? "sim" : "n\xE3o";
|
|
3692
|
+
if (Array.isArray(value))
|
|
3693
|
+
return `${value.length} item(ns)`;
|
|
3694
|
+
if (typeof value === "object")
|
|
3695
|
+
return JSON.stringify(value);
|
|
3696
|
+
return String(value);
|
|
3697
|
+
}
|
|
3698
|
+
function table(rows, columns) {
|
|
3699
|
+
if (rows.length === 0)
|
|
3700
|
+
return "(vazio)";
|
|
3701
|
+
const headers = columns ?? Object.keys(rows[0]);
|
|
3702
|
+
const cells = rows.map((row) => headers.map((header) => format(row[header])));
|
|
3703
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...cells.map((line2) => (line2[index] ?? "").length)));
|
|
3704
|
+
const line = (values) => values.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
|
|
3705
|
+
return [line(headers), line(widths.map((width) => "-".repeat(width))), ...cells.map(line)].join(`
|
|
3706
|
+
`);
|
|
3707
|
+
}
|
|
3708
|
+
function kv(value) {
|
|
3709
|
+
const width = Math.max(...Object.keys(value).map((key) => key.length));
|
|
3710
|
+
return Object.entries(value).map(([key, entry]) => `${key.padEnd(width)} ${format(entry)}`).join(`
|
|
3711
|
+
`);
|
|
3712
|
+
}
|
|
3713
|
+
function mainList(result) {
|
|
3714
|
+
for (const [key, value] of Object.entries(result)) {
|
|
3715
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object") {
|
|
3716
|
+
return { key, rows: value };
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
return null;
|
|
3720
|
+
}
|
|
3721
|
+
var isMoney = (value) => typeof value === "object" && value !== null && typeof value.amount === "number" && typeof value.currency === "string";
|
|
3722
|
+
|
|
3723
|
+
// src/cli/index.ts
|
|
3724
|
+
var exports_cli = {};
|
|
3725
|
+
__export(exports_cli, {
|
|
3726
|
+
runCli: () => runCli
|
|
3727
|
+
});
|
|
3728
|
+
import { Command } from "commander";
|
|
3729
|
+
async function runCli(argv, version) {
|
|
3730
|
+
const program = new Command;
|
|
3731
|
+
program.name("kotas").description("Assinaturas compartilhadas do Kotas: grupos, faturas, cr\xE9ditos e recebimentos").version(version).option("--json", "Imprime o resultado cru em JSON");
|
|
3732
|
+
const withCtx = async (run) => {
|
|
3733
|
+
let ctx;
|
|
3734
|
+
try {
|
|
3735
|
+
ctx = createContext(loadConfig());
|
|
3736
|
+
await run(ctx, program.opts());
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
if (error instanceof ConfigError)
|
|
3739
|
+
console.error(error.message);
|
|
3740
|
+
else
|
|
3741
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3742
|
+
process.exitCode = 1;
|
|
3743
|
+
} finally {
|
|
3744
|
+
ctx?.dispose();
|
|
3745
|
+
}
|
|
3746
|
+
};
|
|
3747
|
+
const call = async (ctx, name, args, options) => {
|
|
3748
|
+
const tool = toolByName(name);
|
|
3749
|
+
if (!tool)
|
|
3750
|
+
throw new Error(`Tool desconhecida: ${name}`);
|
|
3751
|
+
if (ctx.config.readOnly && !tool.readOnly) {
|
|
3752
|
+
throw new Error(`A tool ${name} n\xE3o est\xE1 dispon\xEDvel com KOTAS_READ_ONLY=1.`);
|
|
3753
|
+
}
|
|
3754
|
+
const result = await runTool(tool, prune(args), ctx);
|
|
3755
|
+
print(result, options);
|
|
3756
|
+
};
|
|
3757
|
+
program.command("status").description("Mostra a sess\xE3o salva e o que j\xE1 est\xE1 no cache").option("--verify", "Confirma com o Kotas que a sess\xE3o \xE9 aceita (1 requisi\xE7\xE3o)").action((flags) => withCtx((ctx, options) => call(ctx, "auth_status", { verify: flags.verify }, options)));
|
|
3758
|
+
program.command("login").description("Salva uma sess\xE3o do Kotas, cifrada em disco").option("-e, --email <email>", "E-mail da conta").option("-p, --password <senha>", "Senha (ou defina KOTAS_SENHA)").option("--pin <codigo>", "C\xF3digo de 6 d\xEDgitos da verifica\xE7\xE3o em duas etapas").option("--from-browser <navegador>", "arc | chrome | chromium | brave | edge").option("--paste", "Cola os tr\xEAs valores do localStorage pela entrada padr\xE3o").action((flags) => withCtx(async (ctx, options) => {
|
|
3759
|
+
const args = {
|
|
3760
|
+
email: flags.email,
|
|
3761
|
+
password: flags.password,
|
|
3762
|
+
pin: flags.pin,
|
|
3763
|
+
from_browser: flags.fromBrowser
|
|
3764
|
+
};
|
|
3765
|
+
if (flags.paste)
|
|
3766
|
+
args.tokens = await readPastedTokens();
|
|
3767
|
+
await call(ctx, "login", args, options);
|
|
3768
|
+
}));
|
|
3769
|
+
program.command("doctor").description("Diagn\xF3stico camada a camada").option("--shallow", "N\xE3o testa grupos, cr\xE9ditos e recebimentos").action((flags) => withCtx((ctx, options) => call(ctx, "doctor", { deep: !flags.shallow }, options)));
|
|
3770
|
+
program.command("sync").description("Baixa o hist\xF3rico para o cache local (repete os blocos sozinho)").option("--full", "Varre tudo, n\xE3o s\xF3 o que mudou").option("--reparse", "Reprocessa o cache sem usar a rede").option("--max-requests <n>", "Or\xE7amento por bloco", (value) => Number(value)).action((flags) => withCtx(async (ctx, options) => {
|
|
3771
|
+
const tool = toolByName("sync");
|
|
3772
|
+
if (!tool)
|
|
3773
|
+
throw new Error("Tool sync ausente do registry.");
|
|
3774
|
+
const args = prune({
|
|
3775
|
+
mode: flags.reparse ? "reparse" : flags.full ? "full" : undefined,
|
|
3776
|
+
max_requests: flags.maxRequests
|
|
3777
|
+
});
|
|
3778
|
+
let last = {};
|
|
3779
|
+
const errors = [];
|
|
3780
|
+
for (let chunk = 1;chunk <= 100; chunk += 1) {
|
|
3781
|
+
last = await runTool(tool, args, ctx);
|
|
3782
|
+
errors.push(...last.errors ?? []);
|
|
3783
|
+
console.error(`bloco ${chunk}: ${last.requestsUsed} req, ${last.invoices} faturas, ${last.pendingDetails} detalhes pendentes${last.done ? ", pronto" : ""}`);
|
|
3784
|
+
if (last.done === true)
|
|
3785
|
+
break;
|
|
3786
|
+
}
|
|
3787
|
+
print({ ...last, errors }, options);
|
|
3788
|
+
}));
|
|
3789
|
+
program.command("subscriptions").description("Lista as assinaturas do cache").option("--role <papel>", "todos | membro | administrador").action((flags) => withCtx((ctx, options) => call(ctx, "list_subscriptions", { role: flags.role }, options)));
|
|
3790
|
+
program.command("subscription <groupId>").description("Detalha uma assinatura").action((groupId) => withCtx((ctx, options) => call(ctx, "get_subscription", { group_id: Number(groupId) }, options)));
|
|
3791
|
+
program.command("invoices").description("Lista as faturas do cache").option("--status <situacao>", "pago | pendente | cancelado | atraso | estornado").option("--from <data>", "YYYY-MM-DD").option("--to <data>", "YYYY-MM-DD").option("--product <nome>", "Nome exato do produto").option("--limit <n>", "M\xE1ximo de faturas", (value) => Number(value)).action((flags) => withCtx((ctx, options) => call(ctx, "list_invoices", {
|
|
3792
|
+
status: flags.status,
|
|
3793
|
+
from: flags.from,
|
|
3794
|
+
to: flags.to,
|
|
3795
|
+
product: flags.product,
|
|
3796
|
+
limit: flags.limit
|
|
3797
|
+
}, options)));
|
|
3798
|
+
program.command("invoice <invoiceId>").description("Detalha uma fatura, com os itens").action((invoiceId) => withCtx((ctx, options) => call(ctx, "get_invoice", { invoice_id: Number(invoiceId) }, options)));
|
|
3799
|
+
program.command("credits").description("Lista os lan\xE7amentos de cr\xE9dito").option("--kind <tipo>", "adicaoDeSaldo | caucaoDaInscricao | repasseAdministrador | estornoCancelamento").action((flags) => withCtx((ctx, options) => call(ctx, "list_credits", { kind: flags.kind }, options)));
|
|
3800
|
+
program.command("balance").description("Saldo da carteira e economia acumulada (ao vivo)").action(() => withCtx((ctx, options) => call(ctx, "balance", {}, options)));
|
|
3801
|
+
program.command("payouts").description("Recebimentos como administrador").option("--group <id>", "Tamb\xE9m mostra o extrato deste grupo", (value) => Number(value)).action((flags) => withCtx((ctx, options) => call(ctx, "list_payouts", { group_id: flags.group }, options)));
|
|
3802
|
+
program.command("history").description("Linha do tempo de todas as assinaturas j\xE1 pagas, inclusive as encerradas").option("--active", "S\xF3 as que ainda existem hoje").option("--with-credits", "Inclui as compras de cr\xE9dito avulso").action((flags) => withCtx((ctx, options) => call(ctx, "purchase_history", { active_only: flags.active, include_credit_purchases: flags.withCredits }, options)));
|
|
3803
|
+
program.command("spending").description("Soma os gastos por m\xEAs, ano, produto ou situa\xE7\xE3o").option("--by <criterio>", "month | year | product | status", "month").option("--from <data>", "YYYY-MM-DD").option("--to <data>", "YYYY-MM-DD").action((flags) => withCtx((ctx, options) => call(ctx, "spending_summary", { by: flags.by, from: flags.from, to: flags.to }, options)));
|
|
3804
|
+
program.command("search <termo>").description("Busca servi\xE7os no cat\xE1logo do Kotas").action((termo) => withCtx((ctx, options) => call(ctx, "search_services", { query: termo }, options)));
|
|
3805
|
+
program.command("export <escopo>").description("invoices | subscriptions | credits | payouts | history").option("--format <formato>", "csv | json", "csv").option("--filename <nome>", "Nome do arquivo, sem caminho").action((escopo, flags) => withCtx((ctx, options) => call(ctx, "export", { scope: escopo, format: flags.format, filename: flags.filename }, options)));
|
|
3806
|
+
program.command("raw <rota>").description("GET autenticado numa rota de leitura da API").option("-q, --query <k=v...>", "Par\xE2metros de query, repet\xEDveis (ex.: -q page=1 -q pageSize=50)").action((rota, flags) => withCtx((ctx, options) => call(ctx, "raw_get", { path: rota, query: parseQuery(flags.query) }, options)));
|
|
3807
|
+
program.command("tools").description("Lista as tools registradas").action(() => withCtx(async (ctx, options) => {
|
|
3808
|
+
const rows = activeTools(ctx.config).map((tool) => ({
|
|
3809
|
+
tool: tool.name,
|
|
3810
|
+
escreve: !tool.readOnly
|
|
3811
|
+
}));
|
|
3812
|
+
print({ total: rows.length, tools: rows }, options);
|
|
3813
|
+
}));
|
|
3814
|
+
program.command("mcp").description("Inicia o servidor MCP (stdio)").action(() => {
|
|
3815
|
+
throw new Error("Use `kotas mcp` diretamente: o modo MCP \xE9 tratado antes do CLI.");
|
|
3816
|
+
});
|
|
3817
|
+
await program.parseAsync(argv);
|
|
3818
|
+
}
|
|
3819
|
+
async function readPastedTokens() {
|
|
3820
|
+
process.stderr.write(`Cole os valores do localStorage de app.kotas.com.br, um por linha:
|
|
3821
|
+
` + ` 1) @chave
|
|
3822
|
+
2) @c08cbbfd6eefc83ac6d23c4c791277e4
|
|
3823
|
+
3) SEFTSF9ESVNQT1NJVElWTw== (opcional)
|
|
3824
|
+
`);
|
|
3825
|
+
const text = await new Response(Bun.stdin.stream()).text();
|
|
3826
|
+
const [accessToken, refreshToken, deviceHash] = text.split(`
|
|
3827
|
+
`).map((line) => line.trim()).filter((line) => line !== "");
|
|
3828
|
+
if (!accessToken || !refreshToken) {
|
|
3829
|
+
throw new Error("Faltou o access token ou o refresh token (uma linha para cada).");
|
|
3830
|
+
}
|
|
3831
|
+
return {
|
|
3832
|
+
access_token: accessToken,
|
|
3833
|
+
refresh_token: refreshToken,
|
|
3834
|
+
...deviceHash ? { device_hash: deviceHash } : {}
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
function parseQuery(pairs) {
|
|
3838
|
+
if (!pairs || pairs.length === 0)
|
|
3839
|
+
return;
|
|
3840
|
+
const query = {};
|
|
3841
|
+
for (const pair of pairs) {
|
|
3842
|
+
const at = pair.indexOf("=");
|
|
3843
|
+
if (at === -1)
|
|
3844
|
+
throw new Error(`Par\xE2metro inv\xE1lido: "${pair}". Use chave=valor.`);
|
|
3845
|
+
query[pair.slice(0, at)] = pair.slice(at + 1);
|
|
3846
|
+
}
|
|
3847
|
+
return query;
|
|
3848
|
+
}
|
|
3849
|
+
function print(result, options) {
|
|
3850
|
+
if (options.json) {
|
|
3851
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
const list = mainList(result);
|
|
3855
|
+
if (list) {
|
|
3856
|
+
const rest = Object.fromEntries(Object.entries(result).filter(([key]) => key !== list.key));
|
|
3857
|
+
if (Object.keys(rest).length > 0)
|
|
3858
|
+
console.log(`${kv(rest)}
|
|
3859
|
+
`);
|
|
3860
|
+
console.log(table(list.rows));
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
console.log(kv(result));
|
|
3864
|
+
}
|
|
3865
|
+
var prune = (args) => Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
|
|
3866
|
+
var init_cli = __esm(() => {
|
|
3867
|
+
init_config();
|
|
3868
|
+
init_context();
|
|
3869
|
+
init_config();
|
|
3870
|
+
init_define();
|
|
3871
|
+
init_registry();
|
|
3872
|
+
});
|
|
3873
|
+
// package.json
|
|
3874
|
+
var package_default = {
|
|
3875
|
+
name: "@maxwellmezadre/kotas-mcp",
|
|
3876
|
+
version: "0.1.0",
|
|
3877
|
+
description: "CLI + servidor MCP para as assinaturas compartilhadas do Kotas (grupos, faturas, cr\xE9ditos, recebimentos e hist\xF3rico) sobre um n\xFAcleo compartilhado",
|
|
3878
|
+
type: "module",
|
|
3879
|
+
license: "MIT",
|
|
3880
|
+
repository: { type: "git", url: "git+https://github.com/maxwellmezadre/kotas-mcp.git" },
|
|
3881
|
+
homepage: "https://github.com/maxwellmezadre/kotas-mcp#readme",
|
|
3882
|
+
keywords: ["kotas", "cli", "mcp", "subscriptions", "invoices", "assinaturas", "claude"],
|
|
3883
|
+
bin: { kotas: "dist/bin.js", "kotas-mcp": "dist/mcp-bin.js" },
|
|
3884
|
+
files: ["dist", "SKILL.md", "docs/TOOLS.md", "README.md", "LICENSE"],
|
|
3885
|
+
exports: "./dist/bin.js",
|
|
3886
|
+
publishConfig: { access: "public" },
|
|
3887
|
+
engines: { bun: ">=1.3" },
|
|
3888
|
+
scripts: {
|
|
3889
|
+
start: "bun run src/bin.ts",
|
|
3890
|
+
login: "bun run src/bin.ts login",
|
|
3891
|
+
typecheck: "tsc --noEmit",
|
|
3892
|
+
test: "bun test",
|
|
3893
|
+
verify: "bun run scripts/verify.ts",
|
|
3894
|
+
"docs:tools": "bun run scripts/gen-tools-doc.ts",
|
|
3895
|
+
"build:dist": "bun build src/bin.ts src/mcp-bin.ts --target=bun --packages external --outdir dist",
|
|
3896
|
+
"build:binary": "bun build --compile src/bin.ts --outfile kotas",
|
|
3897
|
+
prepublishOnly: "bun run build:dist",
|
|
3898
|
+
setup: "bun run scripts/install.ts"
|
|
3899
|
+
},
|
|
3900
|
+
dependencies: {
|
|
3901
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
3902
|
+
"@sinclair/typebox": "^0.34.52",
|
|
3903
|
+
commander: "^15.0.0"
|
|
3904
|
+
},
|
|
3905
|
+
devDependencies: { "@types/bun": "^1.4.1", typescript: "^5" }
|
|
3906
|
+
};
|
|
3907
|
+
|
|
3908
|
+
// src/bin.ts
|
|
3909
|
+
var arg = process.argv[2];
|
|
3910
|
+
if (arg === "mcp") {
|
|
3911
|
+
const [{ loadConfig: loadConfig2 }, { createContext: createContext2 }, { startMcpServer: startMcpServer2 }] = await Promise.all([
|
|
3912
|
+
Promise.resolve().then(() => (init_config(), exports_config)),
|
|
3913
|
+
Promise.resolve().then(() => (init_context(), exports_context)),
|
|
3914
|
+
Promise.resolve().then(() => (init_server(), exports_server))
|
|
3915
|
+
]);
|
|
3916
|
+
await startMcpServer2(createContext2(loadConfig2()), package_default.version);
|
|
3917
|
+
} else {
|
|
3918
|
+
const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), exports_cli));
|
|
3919
|
+
await runCli2(process.argv, package_default.version);
|
|
3920
|
+
}
|