mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-05-19 09:28:47 +02:00
Fix pension paste parser for split-line format from bank website
All checks were successful
Deploy to VPS / deploy (push) Successful in 14s
All checks were successful
Deploy to VPS / deploy (push) Successful in 14s
The bank website puts field labels and amounts on separate lines. Parser now handles both inline and split formats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,14 +14,12 @@ export interface PensionParsedEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseAmount(raw: string): number {
|
function parseAmount(raw: string): number {
|
||||||
// "¢ 18,684,764.98" or "¢ -552,213.24" or just "18,684,764.98"
|
|
||||||
const cleaned = raw.replace(/[¢\s]/g, '').replace(/,/g, '');
|
const cleaned = raw.replace(/[¢\s]/g, '').replace(/,/g, '');
|
||||||
const num = parseFloat(cleaned);
|
const num = parseFloat(cleaned);
|
||||||
return isNaN(num) ? 0 : num;
|
return isNaN(num) ? 0 : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDateDMY(raw: string): string {
|
function parseDateDMY(raw: string): string {
|
||||||
// "01/03/2026" → "2026-03-01"
|
|
||||||
const m = raw.match(/(\d{2})\/(\d{2})\/(\d{4})/);
|
const m = raw.match(/(\d{2})\/(\d{2})\/(\d{4})/);
|
||||||
if (!m) return '';
|
if (!m) return '';
|
||||||
return `${m[3]}-${m[2]}-${m[1]}`;
|
return `${m[3]}-${m[2]}-${m[1]}`;
|
||||||
@@ -33,6 +31,17 @@ function extractAmounts(line: string): number[] {
|
|||||||
return matches.map(parseAmount);
|
return matches.map(parseAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Field labels in the order they appear in the bank statement
|
||||||
|
const FIELD_LABELS: [RegExp, string][] = [
|
||||||
|
[/saldo\s*anterior/i, 'saldo_anterior'],
|
||||||
|
[/aportes/i, 'aportes'],
|
||||||
|
[/rendimientos/i, 'rendimientos'],
|
||||||
|
[/retiros/i, 'retiros'],
|
||||||
|
[/traslados/i, 'traslados'],
|
||||||
|
[/comisi[oó]n/i, 'comision'],
|
||||||
|
[/bonificaci[oó]n/i, 'bonificacion'],
|
||||||
|
];
|
||||||
|
|
||||||
interface BlockResult {
|
interface BlockResult {
|
||||||
funds: string[];
|
funds: string[];
|
||||||
fields: Record<string, number[]>;
|
fields: Record<string, number[]>;
|
||||||
@@ -60,32 +69,69 @@ function parseBlock(lines: string[]): BlockResult | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldMap: [RegExp, string][] = [
|
// Strategy 1: Try same-line parsing (label + amounts on same line)
|
||||||
[/saldo\s*anterior/i, 'saldo_anterior'],
|
// Strategy 2: Collect standalone amount lines for split-format parsing
|
||||||
[/aportes/i, 'aportes'],
|
const detectedFieldOrder: string[] = [];
|
||||||
[/rendimientos/i, 'rendimientos'],
|
const standaloneAmounts: number[] = [];
|
||||||
[/retiros/i, 'retiros'],
|
|
||||||
[/traslados/i, 'traslados'],
|
|
||||||
[/comisi[oó]n/i, 'comision'],
|
|
||||||
[/bonificaci[oó]n/i, 'bonificacion'],
|
|
||||||
[/saldo\s*actual/i, 'saldo_final'],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
for (const [regex, key] of fieldMap) {
|
// Check for period
|
||||||
if (regex.test(line)) {
|
|
||||||
const amounts = extractAmounts(line);
|
|
||||||
if (amounts.length > 0) {
|
|
||||||
result.fields[key] = amounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Period
|
|
||||||
const periodMatch = line.match(/del\s+(\d{2}\/\d{2}\/\d{4})\s+al\s+(\d{2}\/\d{2}\/\d{4})/i);
|
const periodMatch = line.match(/del\s+(\d{2}\/\d{2}\/\d{4})\s+al\s+(\d{2}\/\d{2}\/\d{4})/i);
|
||||||
if (periodMatch) {
|
if (periodMatch) {
|
||||||
result.period_start = parseDateDMY(periodMatch[1]);
|
result.period_start = parseDateDMY(periodMatch[1]);
|
||||||
result.period_end = parseDateDMY(periodMatch[2]);
|
result.period_end = parseDateDMY(periodMatch[2]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "Saldo Actual" line (always has amounts inline)
|
||||||
|
if (/saldo\s*actual/i.test(line)) {
|
||||||
|
const amounts = extractAmounts(line);
|
||||||
|
if (amounts.length > 0) {
|
||||||
|
result.fields['saldo_final'] = amounts;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this line matches a field label
|
||||||
|
let matchedLabel = false;
|
||||||
|
for (const [regex, key] of FIELD_LABELS) {
|
||||||
|
if (regex.test(line)) {
|
||||||
|
matchedLabel = true;
|
||||||
|
const amounts = extractAmounts(line);
|
||||||
|
if (amounts.length > 0) {
|
||||||
|
// Strategy 1: amounts on same line as label
|
||||||
|
result.fields[key] = amounts;
|
||||||
|
} else {
|
||||||
|
// Strategy 2: label-only line, record the order
|
||||||
|
detectedFieldOrder.push(key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a label line, check if it's a standalone amount line
|
||||||
|
if (!matchedLabel) {
|
||||||
|
const amounts = extractAmounts(line);
|
||||||
|
if (amounts.length === 1) {
|
||||||
|
standaloneAmounts.push(amounts[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have standalone amounts and field labels, map them
|
||||||
|
// Format: N labels, then N amounts for fund1, then N amounts for fund2, ...
|
||||||
|
if (detectedFieldOrder.length > 0 && standaloneAmounts.length > 0) {
|
||||||
|
const numFields = detectedFieldOrder.length;
|
||||||
|
const numFunds = result.funds.length;
|
||||||
|
|
||||||
|
if (standaloneAmounts.length >= numFields * numFunds) {
|
||||||
|
for (let f = 0; f < numFunds; f++) {
|
||||||
|
for (let i = 0; i < numFields; i++) {
|
||||||
|
const key = detectedFieldOrder[i];
|
||||||
|
if (!result.fields[key]) result.fields[key] = [];
|
||||||
|
result.fields[key].push(standaloneAmounts[f * numFields + i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user