financeops-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/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- package/.github/assets/banner.svg +104 -0
- package/.github/pull_request_template.md +25 -0
- package/CODE_OF_CONDUCT.md +40 -0
- package/CONTRIBUTING.md +71 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +30 -0
- package/dist/adapters/stripe.d.ts +15 -0
- package/dist/adapters/stripe.js +175 -0
- package/dist/adapters/types.d.ts +30 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/xero.d.ts +19 -0
- package/dist/adapters/xero.js +261 -0
- package/dist/analysis/anomaly.d.ts +12 -0
- package/dist/analysis/anomaly.js +103 -0
- package/dist/analysis/cashflow.d.ts +10 -0
- package/dist/analysis/cashflow.js +134 -0
- package/dist/analysis/pnl.d.ts +8 -0
- package/dist/analysis/pnl.js +56 -0
- package/dist/analysis/reconciliation.d.ts +14 -0
- package/dist/analysis/reconciliation.js +98 -0
- package/dist/analysis/tax.d.ts +11 -0
- package/dist/analysis/tax.js +81 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +565 -0
- package/dist/lib/audit.d.ts +17 -0
- package/dist/lib/audit.js +70 -0
- package/dist/lib/providers.d.ts +6 -0
- package/dist/lib/providers.js +25 -0
- package/dist/premium/gate.d.ts +12 -0
- package/dist/premium/gate.js +22 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.js +7 -0
- package/mcpize.yaml +10 -0
- package/package.json +35 -0
- package/src/adapters/stripe.ts +190 -0
- package/src/adapters/types.ts +34 -0
- package/src/adapters/xero.ts +317 -0
- package/src/analysis/anomaly.ts +119 -0
- package/src/analysis/cashflow.ts +158 -0
- package/src/analysis/pnl.ts +80 -0
- package/src/analysis/reconciliation.ts +117 -0
- package/src/analysis/tax.ts +98 -0
- package/src/index.ts +649 -0
- package/src/lib/audit.ts +92 -0
- package/src/lib/providers.ts +29 -0
- package/src/premium/gate.ts +24 -0
- package/src/types.ts +153 -0
- package/tests/adapters/stripe.test.ts +150 -0
- package/tests/adapters/xero.test.ts +188 -0
- package/tests/analysis/anomaly.test.ts +137 -0
- package/tests/analysis/cashflow.test.ts +112 -0
- package/tests/analysis/pnl.test.ts +95 -0
- package/tests/analysis/reconciliation.test.ts +121 -0
- package/tests/analysis/tax.test.ts +163 -0
- package/tests/helpers/mock-data.ts +135 -0
- package/tests/lib/audit.test.ts +89 -0
- package/tests/lib/providers.test.ts +129 -0
- package/tests/premium/cash_flow_forecast.test.ts +157 -0
- package/tests/premium/detect_anomalies.test.ts +189 -0
- package/tests/premium/gate.test.ts +59 -0
- package/tests/premium/generate_pnl.test.ts +155 -0
- package/tests/premium/multi_currency_report.test.ts +141 -0
- package/tests/premium/reconcile.test.ts +174 -0
- package/tests/premium/tax_summary.test.ts +166 -0
- package/tests/tools/expense_tracker.test.ts +181 -0
- package/tests/tools/financial_health.test.ts +196 -0
- package/tests/tools/get_balances.test.ts +160 -0
- package/tests/tools/list_invoices.test.ts +191 -0
- package/tests/tools/list_transactions.test.ts +210 -0
- package/tests/tools/revenue_summary.test.ts +188 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Transaction, ReconciliationResult, MatchedPair } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const DATE_TOLERANCE_DAYS = 2;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Levenshtein distance between two strings.
|
|
7
|
+
*/
|
|
8
|
+
function levenshtein(a: string, b: string): number {
|
|
9
|
+
const m = a.length;
|
|
10
|
+
const n = b.length;
|
|
11
|
+
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
|
12
|
+
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
for (let i = 1; i <= m; i++) {
|
|
16
|
+
for (let j = 1; j <= n; j++) {
|
|
17
|
+
if (a[i - 1] === b[j - 1]) {
|
|
18
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
19
|
+
} else {
|
|
20
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return dp[m][n];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalized Levenshtein distance: 0 = identical, 1 = completely different.
|
|
30
|
+
*/
|
|
31
|
+
function normalizedLevenshtein(a: string, b: string): number {
|
|
32
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
33
|
+
const maxLen = Math.max(a.length, b.length);
|
|
34
|
+
if (maxLen === 0) return 0;
|
|
35
|
+
return levenshtein(a, b) / maxLen;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reconcile Stripe transactions against Xero transactions.
|
|
40
|
+
*
|
|
41
|
+
* Matching criteria:
|
|
42
|
+
* 1. Exact amount match (in cents)
|
|
43
|
+
* 2. Date within ±2 days
|
|
44
|
+
* 3. HARD GATE: description similarity >= 20% (Critical Fix 4)
|
|
45
|
+
* Prevents false positives like "AWS hosting $50" matching "Employee lunch $50"
|
|
46
|
+
*
|
|
47
|
+
* Confidence: weighted score (40% date proximity + 60% description similarity).
|
|
48
|
+
*/
|
|
49
|
+
export function reconcile(
|
|
50
|
+
stripeTransactions: Transaction[],
|
|
51
|
+
xeroTransactions: Transaction[]
|
|
52
|
+
): ReconciliationResult {
|
|
53
|
+
const matchedStripeIds = new Set<string>();
|
|
54
|
+
const matchedXeroIds = new Set<string>();
|
|
55
|
+
const matched: MatchedPair[] = [];
|
|
56
|
+
|
|
57
|
+
for (const st of stripeTransactions) {
|
|
58
|
+
const stDate = new Date(st.date).getTime();
|
|
59
|
+
|
|
60
|
+
let bestMatch: { xero: Transaction; confidence: number } | null = null;
|
|
61
|
+
|
|
62
|
+
for (const xt of xeroTransactions) {
|
|
63
|
+
if (matchedXeroIds.has(xt.id)) continue;
|
|
64
|
+
|
|
65
|
+
// 1. Exact amount match required
|
|
66
|
+
if (st.amount !== xt.amount) continue;
|
|
67
|
+
|
|
68
|
+
// 2. Date tolerance check
|
|
69
|
+
const xtDate = new Date(xt.date).getTime();
|
|
70
|
+
const daysDiff = Math.abs(stDate - xtDate) / (1000 * 60 * 60 * 24);
|
|
71
|
+
if (daysDiff > DATE_TOLERANCE_DAYS) continue;
|
|
72
|
+
|
|
73
|
+
// 3. HARD GATE: description similarity (Critical Fix 4)
|
|
74
|
+
const descSimilarity =
|
|
75
|
+
1 -
|
|
76
|
+
normalizedLevenshtein(
|
|
77
|
+
st.description.toLowerCase(),
|
|
78
|
+
xt.description.toLowerCase()
|
|
79
|
+
);
|
|
80
|
+
if (descSimilarity < 0.2) continue;
|
|
81
|
+
|
|
82
|
+
// Confidence score
|
|
83
|
+
const dateScore = 1 - daysDiff / DATE_TOLERANCE_DAYS;
|
|
84
|
+
const confidence = dateScore * 0.4 + descSimilarity * 0.6;
|
|
85
|
+
|
|
86
|
+
if (!bestMatch || confidence > bestMatch.confidence) {
|
|
87
|
+
bestMatch = { xero: xt, confidence };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (bestMatch) {
|
|
92
|
+
matchedStripeIds.add(st.id);
|
|
93
|
+
matchedXeroIds.add(bestMatch.xero.id);
|
|
94
|
+
matched.push({
|
|
95
|
+
stripe: st,
|
|
96
|
+
xero: bestMatch.xero,
|
|
97
|
+
confidence: Math.round(bestMatch.confidence * 1000) / 1000,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const unmatchedStripe = stripeTransactions.filter((t) => !matchedStripeIds.has(t.id));
|
|
103
|
+
const unmatchedXero = xeroTransactions.filter((t) => !matchedXeroIds.has(t.id));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
matched,
|
|
107
|
+
unmatched_stripe: unmatchedStripe,
|
|
108
|
+
unmatched_xero: unmatchedXero,
|
|
109
|
+
summary: {
|
|
110
|
+
total_stripe: stripeTransactions.length,
|
|
111
|
+
total_xero: xeroTransactions.length,
|
|
112
|
+
matched_count: matched.length,
|
|
113
|
+
unmatched_stripe_count: unmatchedStripe.length,
|
|
114
|
+
unmatched_xero_count: unmatchedXero.length,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Transaction, TaxSummary, TaxByRate } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default VAT/tax rates by jurisdiction.
|
|
5
|
+
* Key = jurisdiction code, value = array of applicable rates.
|
|
6
|
+
*/
|
|
7
|
+
const TAX_RATES_BY_JURISDICTION: Record<string, number[]> = {
|
|
8
|
+
EU: [0.0, 0.05, 0.12, 0.2, 0.21, 0.25],
|
|
9
|
+
NL: [0.0, 0.09, 0.21],
|
|
10
|
+
DE: [0.0, 0.07, 0.19],
|
|
11
|
+
FR: [0.0, 0.055, 0.1, 0.2],
|
|
12
|
+
GB: [0.0, 0.05, 0.2],
|
|
13
|
+
US: [0.0, 0.05, 0.07, 0.08, 0.1],
|
|
14
|
+
DEFAULT: [0.0, 0.1, 0.2],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const STANDARD_RATE_BY_JURISDICTION: Record<string, number> = {
|
|
18
|
+
NL: 0.21,
|
|
19
|
+
DE: 0.19,
|
|
20
|
+
FR: 0.2,
|
|
21
|
+
GB: 0.2,
|
|
22
|
+
US: 0.08,
|
|
23
|
+
EU: 0.21,
|
|
24
|
+
DEFAULT: 0.2,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate VAT/sales tax summary from transactions.
|
|
29
|
+
* Input amounts are in cents; output is in major currency units.
|
|
30
|
+
*
|
|
31
|
+
* This is a simplified v1 calculation. For production:
|
|
32
|
+
* - Use actual tax metadata from the payment processor
|
|
33
|
+
* - Apply jurisdiction-specific rules
|
|
34
|
+
*/
|
|
35
|
+
export function calculateTax(
|
|
36
|
+
transactions: Transaction[],
|
|
37
|
+
period: 'quarter' | 'year',
|
|
38
|
+
jurisdiction?: string
|
|
39
|
+
): TaxSummary {
|
|
40
|
+
const provider = transactions.length > 0 ? transactions[0].provider : 'stripe';
|
|
41
|
+
const jur = (jurisdiction ?? 'DEFAULT').toUpperCase();
|
|
42
|
+
|
|
43
|
+
// Standard rate for the jurisdiction
|
|
44
|
+
const standardRate = STANDARD_RATE_BY_JURISDICTION[jur] ?? STANDARD_RATE_BY_JURISDICTION['DEFAULT'];
|
|
45
|
+
|
|
46
|
+
// Income transactions are taxable
|
|
47
|
+
const taxableTransactions = transactions.filter((t) => t.type === 'income');
|
|
48
|
+
|
|
49
|
+
// Group by assumed tax rate (in v1, we assume all income at standard rate unless metadata says otherwise)
|
|
50
|
+
const byRate: Record<number, { taxable: number; tax: number }> = {};
|
|
51
|
+
|
|
52
|
+
for (const t of taxableTransactions) {
|
|
53
|
+
// Try to get tax rate from metadata
|
|
54
|
+
const rateStr = t.metadata?.tax_rate;
|
|
55
|
+
const rate = rateStr ? parseFloat(rateStr) : standardRate;
|
|
56
|
+
|
|
57
|
+
// Tax-exclusive: taxable amount = amount / (1 + rate), tax = amount - taxable
|
|
58
|
+
const taxableAmountCents = t.amount / (1 + rate);
|
|
59
|
+
const taxAmountCents = t.amount - taxableAmountCents;
|
|
60
|
+
|
|
61
|
+
if (!byRate[rate]) byRate[rate] = { taxable: 0, tax: 0 };
|
|
62
|
+
byRate[rate].taxable += taxableAmountCents;
|
|
63
|
+
byRate[rate].tax += taxAmountCents;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const byRateResult: TaxByRate[] = Object.entries(byRate).map(([rateStr, totals]) => {
|
|
67
|
+
const rate = parseFloat(rateStr);
|
|
68
|
+
return {
|
|
69
|
+
rate,
|
|
70
|
+
rate_label: rate === 0 ? 'Exempt' : `${(rate * 100).toFixed(0)}% VAT`,
|
|
71
|
+
taxable_amount: Math.round(totals.taxable) / 100,
|
|
72
|
+
tax_amount: Math.round(totals.tax) / 100,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const totalTaxableCents = Object.values(byRate).reduce((sum, r) => sum + r.taxable, 0);
|
|
77
|
+
const totalTaxCents = Object.values(byRate).reduce((sum, r) => sum + r.tax, 0);
|
|
78
|
+
|
|
79
|
+
// Filing period label
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const year = now.getFullYear();
|
|
82
|
+
const quarter = Math.ceil((now.getMonth() + 1) / 3);
|
|
83
|
+
const filingPeriod =
|
|
84
|
+
period === 'year'
|
|
85
|
+
? `${year}`
|
|
86
|
+
: `Q${quarter} ${year}`;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
provider,
|
|
90
|
+
period,
|
|
91
|
+
jurisdiction: jur !== 'DEFAULT' ? jur : undefined,
|
|
92
|
+
total_taxable_amount: Math.round(totalTaxableCents) / 100,
|
|
93
|
+
total_tax_collected: Math.round(totalTaxCents) / 100,
|
|
94
|
+
total_tax_owed: Math.round(totalTaxCents) / 100, // In v1, owed = collected
|
|
95
|
+
by_rate: byRateResult,
|
|
96
|
+
filing_period: filingPeriod,
|
|
97
|
+
};
|
|
98
|
+
}
|