Weekly Financial Report
Pipeline 3: Weekly Financial Analysis
Retrospective visualization of spending habits over the previous 7 days is generated using a columnar dashboard format.
Trigger: Weekly (Monday at 10:00 AM)
1. Technical Analysis: API Constraint Management
The current methodology is implemented to bypass critical limitations encountered during development:
| Error | Description |
|---|---|
| Error 422 (Unprocessable Entity) | NocoDB API rejections occurred with ISO 8601 strings and timezone-aware timestamps during created_at filtering. |
| Type Mismatch | date column configured as Text type caused range operator (ge, le) failures due to alphabetical sorting. |
| SQL Injection Constraints | Raw SQL function wrappers (e.g., DATE()) are not supported by the API layer, preventing server-side type casting. |
2. Methodology & Logic
2.1 Smart Filter Generation
- Role: Calculation of specific dates for the preceding 7-day period.
- Mechanism: Dates are formatted as
DD-MM-YYYYto ensure compatibility with text columns. A precise "OR" filter string is generated (e.g.,(date,eq,05-02-2026)~or(date,eq,04-02-2026)...) to force exact string matching.
2.2 Data Retrieval
- Operation: "Get Many" via NocoDB node.
- Pagination: Enabled to ensure 100% data integrity and scalability beyond standard row limits.
2.3 Aggregation & Payload Construction
- Processing: Summations by category and percentage weights are calculated.
- Efficiency: Processing is restricted to rows returned by the smart filter to minimize CPU overhead.
- Layout: JSON structure for Discord is manually constructed to enforce a 3-column "Inline" layout.
2.4 Delivery
- Method: POST to Discord Webhook via HTTP Request node.
- Policy: The standard Discord node UI is bypassed to ensure "Inline" toggles are correctly applied for table rendering.
3. Configuration Scripts
Script A: Filter Generator
const dates = [];
for (let i = 0; i < 7; i++) {
const d = new Date();
d.setDate(d.getDate() - i);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
dates.push(`${day}-${month}-${year}`);
}
return {
filter_formula: dates.map(dateStr => `(date,eq,${dateStr})`).join('~or')
};
Script B: Aggregator & Discord Formatter
const transactions = $input.all();
let categoryMap = {};
let totalSpend = 0;
transactions.forEach(item => {
const t = item.json;
const amount = parseFloat(t.amount || 0);
const cat = t.category || "Uncategorized";
if (!categoryMap[cat]) categoryMap[cat] = 0;
categoryMap[cat] += amount;
totalSpend += amount;
});
let colCategory = [], colAmount = [], colPercent = [];
const sorted = Object.entries(categoryMap).sort((a, b) => b[1] - a[1]);
for (const [cat, amount] of sorted) {
const percent = totalSpend > 0 ? Math.round((amount / totalSpend) * 100) : 0;
colCategory.push(cat);
colAmount.push(`Rp ${amount.toLocaleString('id-ID')}`);
colPercent.push(`${percent}%`);
}
return {
"username": "Finance Bot",
"embeds": [{
"title": "Weekly Expense Report",
"description": `**Total Spent:** Rp ${totalSpend.toLocaleString('id-ID')}\n**Transactions:** ${transactions.length}`,
"color": 3447003,
"timestamp": new Date().toISOString(),
"fields": [
{ "name": "Category", "value": colCategory.join("\n") || "-", "inline": true },
{ "name": "Amount", "value": colAmount.join("\n") || "-", "inline": true },
{ "name": "%", "value": colPercent.join("\n") || "-", "inline": true }
]
}]
};