@kansei-link/bantou 0.2.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 +110 -0
- package/data/exclusion-rules/README.md +104 -0
- package/data/exclusion-rules/jp-tax-baseline-v1.json +185 -0
- package/data/exclusion-rules-schema.json +109 -0
- package/data/keyword-dict/README.md +91 -0
- package/data/keyword-dict/jp-tax-baseline-v1.json +398 -0
- package/data/keyword-dict-schema.json +117 -0
- package/data/tax-rules/jp-tax-rules-v1.json +170 -0
- package/dist/adapters/csv-parser.d.ts +11 -0
- package/dist/adapters/csv-parser.js +133 -0
- package/dist/adapters/freee-csv-adapter.d.ts +14 -0
- package/dist/adapters/freee-csv-adapter.js +67 -0
- package/dist/adapters/generic-adapter.d.ts +20 -0
- package/dist/adapters/generic-adapter.js +73 -0
- package/dist/adapters/index.d.ts +23 -0
- package/dist/adapters/index.js +386 -0
- package/dist/adapters/types.d.ts +111 -0
- package/dist/adapters/types.js +9 -0
- package/dist/adapters/yayoi-adapter.d.ts +46 -0
- package/dist/adapters/yayoi-adapter.js +181 -0
- package/dist/bin/freee-doctor.d.ts +3 -0
- package/dist/bin/freee-doctor.js +15 -0
- package/dist/classifier/claude-classifier.d.ts +24 -0
- package/dist/classifier/claude-classifier.js +154 -0
- package/dist/classifier/keyword-classifier.d.ts +22 -0
- package/dist/classifier/keyword-classifier.js +124 -0
- package/dist/classifier/keyword-match.d.ts +21 -0
- package/dist/classifier/keyword-match.js +57 -0
- package/dist/classifier/normalize.d.ts +3 -0
- package/dist/classifier/normalize.js +27 -0
- package/dist/classifier/two-stage-classifier.d.ts +21 -0
- package/dist/classifier/two-stage-classifier.js +51 -0
- package/dist/classifier/types.d.ts +31 -0
- package/dist/classifier/types.js +3 -0
- package/dist/connectors/freee.d.ts +115 -0
- package/dist/connectors/freee.js +177 -0
- package/dist/exclusion/exclusion-checker.d.ts +10 -0
- package/dist/exclusion/exclusion-checker.js +162 -0
- package/dist/freee-doctor.d.ts +26 -0
- package/dist/freee-doctor.js +82 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +656 -0
- package/dist/memory/cockpit-memory.d.ts +73 -0
- package/dist/memory/cockpit-memory.js +473 -0
- package/dist/memory/types.d.ts +114 -0
- package/dist/memory/types.js +11 -0
- package/dist/pipeline/confidence-router.d.ts +38 -0
- package/dist/pipeline/confidence-router.js +129 -0
- package/dist/pipeline/nightly-pipeline.d.ts +44 -0
- package/dist/pipeline/nightly-pipeline.js +497 -0
- package/dist/pipeline/types.d.ts +84 -0
- package/dist/pipeline/types.js +12 -0
- package/dist/reports/monthly-report.d.ts +64 -0
- package/dist/reports/monthly-report.js +230 -0
- package/dist/secrets.d.ts +14 -0
- package/dist/secrets.js +86 -0
- package/dist/tax-rules/tax-rule-engine.d.ts +103 -0
- package/dist/tax-rules/tax-rule-engine.js +449 -0
- package/dist/tax-rules/types.d.ts +103 -0
- package/dist/tax-rules/types.js +7 -0
- package/package.json +74 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// CSV import orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// Auto-detects CSV format (弥生/freee/MF/generic), parses rows,
|
|
4
|
+
// runs each through the full classification pipeline, and returns
|
|
5
|
+
// structured results grouped by routing action.
|
|
6
|
+
import { parseCsv } from './csv-parser.js';
|
|
7
|
+
import { YayoiAdapter } from './yayoi-adapter.js';
|
|
8
|
+
import { FreeeCsvAdapter } from './freee-csv-adapter.js';
|
|
9
|
+
import { GenericCsvAdapter } from './generic-adapter.js';
|
|
10
|
+
/**
|
|
11
|
+
* Import CSV and run through the full classification pipeline.
|
|
12
|
+
*
|
|
13
|
+
* @param csvText - Raw CSV text (UTF-8).
|
|
14
|
+
* @param classifier - TwoStageClassifier instance.
|
|
15
|
+
* @param exclusion - ExclusionChecker instance.
|
|
16
|
+
* @param router - ConfidenceRouter instance.
|
|
17
|
+
* @param opts.source - Force a specific source format (skip auto-detection).
|
|
18
|
+
* @param opts.mapping - Column mapping for generic CSV.
|
|
19
|
+
*/
|
|
20
|
+
export async function importCsv(csvText, classifier, exclusion, router, opts = {}) {
|
|
21
|
+
// 1. Parse CSV
|
|
22
|
+
const { headers, rows } = parseCsv(csvText);
|
|
23
|
+
if (headers.length === 0 || rows.length === 0) {
|
|
24
|
+
return emptyResult('CSV is empty or has no data rows');
|
|
25
|
+
}
|
|
26
|
+
// 2. Detect or use specified adapter
|
|
27
|
+
const adapter = resolveAdapter(headers, opts.source, opts.mapping);
|
|
28
|
+
if (!adapter) {
|
|
29
|
+
return emptyResult(`CSV format not recognized. Headers: [${headers.slice(0, 5).join(', ')}...]. ` +
|
|
30
|
+
'Specify source="generic" with column mapping, or export from 弥生/freee in UTF-8.');
|
|
31
|
+
}
|
|
32
|
+
// 3. Parse all rows
|
|
33
|
+
const warnings = [];
|
|
34
|
+
const parsed = [];
|
|
35
|
+
let skipped = 0;
|
|
36
|
+
for (let i = 0; i < rows.length; i++) {
|
|
37
|
+
const rowNumber = i + 2; // 1-based, +1 for header row
|
|
38
|
+
const { transaction, skip_reason } = adapter.parseRow(rows[i], rowNumber);
|
|
39
|
+
if (transaction) {
|
|
40
|
+
parsed.push({ rowNumber, transaction });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
skipped++;
|
|
44
|
+
if (skip_reason && skipped <= 10) {
|
|
45
|
+
warnings.push(`Row ${rowNumber}: ${skip_reason}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (skipped > 10) {
|
|
50
|
+
warnings.push(`... and ${skipped - 10} more skipped rows`);
|
|
51
|
+
}
|
|
52
|
+
if (parsed.length === 0) {
|
|
53
|
+
return emptyResult('No valid transactions found in CSV', warnings);
|
|
54
|
+
}
|
|
55
|
+
// 4. Run each transaction through the pipeline
|
|
56
|
+
const autoRegister = [];
|
|
57
|
+
const autoRegisterWithLog = [];
|
|
58
|
+
const humanReview = [];
|
|
59
|
+
const excludedTxns = [];
|
|
60
|
+
for (const { rowNumber, transaction } of parsed) {
|
|
61
|
+
const ct = await classifyTransaction(rowNumber, transaction, classifier, exclusion, router, opts.memory, opts.taxRuleEngine);
|
|
62
|
+
switch (ct.action) {
|
|
63
|
+
case 'auto_register':
|
|
64
|
+
autoRegister.push(ct);
|
|
65
|
+
break;
|
|
66
|
+
case 'auto_register_with_log':
|
|
67
|
+
autoRegisterWithLog.push(ct);
|
|
68
|
+
break;
|
|
69
|
+
case 'human_review':
|
|
70
|
+
if (ct.excluded) {
|
|
71
|
+
excludedTxns.push(ct);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
humanReview.push(ct);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 5. Build result
|
|
80
|
+
const totalClassified = autoRegister.length + autoRegisterWithLog.length;
|
|
81
|
+
const totalProcessed = parsed.length;
|
|
82
|
+
const classificationRate = totalProcessed > 0
|
|
83
|
+
? ((totalClassified / totalProcessed) * 100).toFixed(1) + '%'
|
|
84
|
+
: '0%';
|
|
85
|
+
// Save memory after processing all rows
|
|
86
|
+
if (opts.memory) {
|
|
87
|
+
opts.memory.save();
|
|
88
|
+
}
|
|
89
|
+
const result = {
|
|
90
|
+
ok: true,
|
|
91
|
+
source: adapter.source,
|
|
92
|
+
source_label: adapter.label,
|
|
93
|
+
total_rows: rows.length,
|
|
94
|
+
parsed_count: parsed.length,
|
|
95
|
+
skipped_count: skipped,
|
|
96
|
+
warnings,
|
|
97
|
+
auto_register: autoRegister,
|
|
98
|
+
auto_register_with_log: autoRegisterWithLog,
|
|
99
|
+
human_review: humanReview,
|
|
100
|
+
excluded: excludedTxns,
|
|
101
|
+
summary: {
|
|
102
|
+
auto_register_count: autoRegister.length,
|
|
103
|
+
auto_register_with_log_count: autoRegisterWithLog.length,
|
|
104
|
+
human_review_count: humanReview.length,
|
|
105
|
+
excluded_count: excludedTxns.length,
|
|
106
|
+
classification_rate: classificationRate,
|
|
107
|
+
},
|
|
108
|
+
csv_output: buildCsvOutput([...autoRegister, ...autoRegisterWithLog, ...humanReview, ...excludedTxns]),
|
|
109
|
+
markdown_report: buildMarkdownReport(adapter, parsed.length, skipped, autoRegister, autoRegisterWithLog, humanReview, excludedTxns),
|
|
110
|
+
};
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
// ============================================================
|
|
114
|
+
// Internal helpers
|
|
115
|
+
// ============================================================
|
|
116
|
+
async function classifyTransaction(rowNumber, transaction, classifier, exclusion, router, memory, taxRuleEngine) {
|
|
117
|
+
// Stage 0: Exclusion
|
|
118
|
+
const exc = exclusion.check(transaction);
|
|
119
|
+
// Stage 1+2: Classification (only if not excluded)
|
|
120
|
+
let cls = null;
|
|
121
|
+
let memorySource;
|
|
122
|
+
if (!exc.excluded) {
|
|
123
|
+
// Memory recall before classification
|
|
124
|
+
if (memory) {
|
|
125
|
+
const recall = memory.recallPattern(transaction);
|
|
126
|
+
if (recall.found && recall.source === 'correction' && recall.correction) {
|
|
127
|
+
cls = {
|
|
128
|
+
classified: true,
|
|
129
|
+
category_id: recall.correction.to_category_id,
|
|
130
|
+
category_name_ja: recall.correction.to_category_name_ja,
|
|
131
|
+
freee_account_code: recall.correction.to_freee_account_code,
|
|
132
|
+
tax_code: recall.correction.to_tax_code,
|
|
133
|
+
confidence: 'high',
|
|
134
|
+
match_reason: recall.reason,
|
|
135
|
+
classifier_version: 'memory-correction',
|
|
136
|
+
};
|
|
137
|
+
memorySource = 'correction';
|
|
138
|
+
}
|
|
139
|
+
else if (recall.found && recall.source === 'pattern' && recall.pattern) {
|
|
140
|
+
cls = {
|
|
141
|
+
classified: true,
|
|
142
|
+
category_id: recall.pattern.category_id,
|
|
143
|
+
category_name_ja: recall.pattern.category_name_ja,
|
|
144
|
+
freee_account_code: recall.pattern.freee_account_code,
|
|
145
|
+
tax_code: recall.pattern.tax_code,
|
|
146
|
+
confidence: recall.confidence,
|
|
147
|
+
match_reason: recall.reason,
|
|
148
|
+
classifier_version: 'memory-pattern',
|
|
149
|
+
};
|
|
150
|
+
memorySource = 'pattern';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Fall through to normal classification if memory miss
|
|
154
|
+
if (!cls) {
|
|
155
|
+
cls = await classifier.classify(transaction);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Routing
|
|
159
|
+
const routing = router.route(exc, cls, {
|
|
160
|
+
amount: transaction.amount,
|
|
161
|
+
partner_name: transaction.partner_name,
|
|
162
|
+
is_new_partner: false, // CSV import = no partner DB to check
|
|
163
|
+
date: transaction.date,
|
|
164
|
+
});
|
|
165
|
+
// Remember classification for future recall (only successful auto-classifications)
|
|
166
|
+
if (memory && !memorySource && cls?.classified && (routing.action === 'auto_register' || routing.action === 'auto_register_with_log')) {
|
|
167
|
+
memory.rememberClassification(transaction, cls.category_id, cls.category_name_ja, cls.confidence, 'keyword', // CSV import is always Stage 1 in practice
|
|
168
|
+
cls.freee_account_code, typeof cls.tax_code === 'number' ? cls.tax_code : undefined);
|
|
169
|
+
}
|
|
170
|
+
// Tax Rule Engine: post-classification refinements
|
|
171
|
+
let taxCodeOverride;
|
|
172
|
+
let taxCodeReason;
|
|
173
|
+
let assetTier;
|
|
174
|
+
let assetWarning;
|
|
175
|
+
let withholdingAmount;
|
|
176
|
+
let withholdingRate;
|
|
177
|
+
let consumptionTaxRate;
|
|
178
|
+
let consumptionTaxReason;
|
|
179
|
+
let taxRuleWarnings;
|
|
180
|
+
if (taxRuleEngine && cls?.classified) {
|
|
181
|
+
const taxResult = taxRuleEngine.applyRules(transaction, cls);
|
|
182
|
+
if (taxResult.tax_code_override !== undefined) {
|
|
183
|
+
taxCodeOverride = taxResult.tax_code_override;
|
|
184
|
+
taxCodeReason = taxResult.tax_code_reason;
|
|
185
|
+
}
|
|
186
|
+
if (taxResult.asset_tier && taxResult.asset_tier !== 'expense') {
|
|
187
|
+
assetTier = taxResult.asset_tier;
|
|
188
|
+
assetWarning = taxResult.asset_warning;
|
|
189
|
+
}
|
|
190
|
+
if (taxResult.withholding) {
|
|
191
|
+
withholdingAmount = taxResult.withholding.withholding_amount;
|
|
192
|
+
withholdingRate = taxResult.withholding.rate_description;
|
|
193
|
+
}
|
|
194
|
+
if (taxResult.consumption_tax_rate !== undefined) {
|
|
195
|
+
consumptionTaxRate = taxResult.consumption_tax_rate;
|
|
196
|
+
consumptionTaxReason = taxResult.consumption_tax_reason;
|
|
197
|
+
}
|
|
198
|
+
if (taxResult.warnings.length > 0) {
|
|
199
|
+
taxRuleWarnings = taxResult.warnings;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
row_number: rowNumber,
|
|
204
|
+
transaction,
|
|
205
|
+
excluded: exc.excluded,
|
|
206
|
+
exclusion_rule: exc.rule_id,
|
|
207
|
+
exclusion_reason: exc.reason,
|
|
208
|
+
classified: cls?.classified ?? false,
|
|
209
|
+
category_id: cls?.category_id,
|
|
210
|
+
category_name_ja: cls?.category_name_ja,
|
|
211
|
+
confidence: cls?.confidence,
|
|
212
|
+
matched_keyword: cls?.matched_keyword,
|
|
213
|
+
stage: cls?.stage,
|
|
214
|
+
freee_account_code: cls?.freee_account_code,
|
|
215
|
+
tax_code: taxCodeOverride ?? cls?.tax_code,
|
|
216
|
+
tax_code_override: taxCodeOverride,
|
|
217
|
+
tax_code_reason: taxCodeReason,
|
|
218
|
+
asset_tier: assetTier,
|
|
219
|
+
asset_warning: assetWarning,
|
|
220
|
+
withholding_amount: withholdingAmount,
|
|
221
|
+
withholding_rate: withholdingRate,
|
|
222
|
+
consumption_tax_rate: consumptionTaxRate,
|
|
223
|
+
consumption_tax_reason: consumptionTaxReason,
|
|
224
|
+
tax_rule_warnings: taxRuleWarnings,
|
|
225
|
+
action: routing.action,
|
|
226
|
+
routing_flags: routing.flags,
|
|
227
|
+
routing_reasons: routing.reasons,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function resolveAdapter(headers, source, mapping) {
|
|
231
|
+
// If source is explicitly specified
|
|
232
|
+
if (source === 'yayoi') {
|
|
233
|
+
const a = new YayoiAdapter();
|
|
234
|
+
a.detectFormat(headers);
|
|
235
|
+
return a;
|
|
236
|
+
}
|
|
237
|
+
if (source === 'freee_export') {
|
|
238
|
+
const a = new FreeeCsvAdapter();
|
|
239
|
+
a.detectFormat(headers);
|
|
240
|
+
return a;
|
|
241
|
+
}
|
|
242
|
+
if (source === 'generic' && mapping) {
|
|
243
|
+
return new GenericCsvAdapter(mapping);
|
|
244
|
+
}
|
|
245
|
+
// Auto-detect: try each adapter in priority order
|
|
246
|
+
const adapters = [
|
|
247
|
+
new YayoiAdapter(),
|
|
248
|
+
new FreeeCsvAdapter(),
|
|
249
|
+
];
|
|
250
|
+
for (const adapter of adapters) {
|
|
251
|
+
if (adapter.detectFormat(headers)) {
|
|
252
|
+
return adapter;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Fallback: if mapping provided, use generic
|
|
256
|
+
if (mapping) {
|
|
257
|
+
return new GenericCsvAdapter(mapping);
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
function emptyResult(error, warnings = []) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
source: 'generic',
|
|
265
|
+
source_label: 'Unknown',
|
|
266
|
+
total_rows: 0,
|
|
267
|
+
parsed_count: 0,
|
|
268
|
+
skipped_count: 0,
|
|
269
|
+
warnings: [error, ...warnings],
|
|
270
|
+
auto_register: [],
|
|
271
|
+
auto_register_with_log: [],
|
|
272
|
+
human_review: [],
|
|
273
|
+
excluded: [],
|
|
274
|
+
summary: {
|
|
275
|
+
auto_register_count: 0,
|
|
276
|
+
auto_register_with_log_count: 0,
|
|
277
|
+
human_review_count: 0,
|
|
278
|
+
excluded_count: 0,
|
|
279
|
+
classification_rate: '0%',
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Build a CSV output with classification results appended as new columns.
|
|
285
|
+
* Can be re-imported into 弥生 or used for review.
|
|
286
|
+
*/
|
|
287
|
+
function buildCsvOutput(transactions) {
|
|
288
|
+
const headers = [
|
|
289
|
+
'行番号', '日付', '金額', '摘要', '取引先',
|
|
290
|
+
'分類結果', '勘定科目', '信頼度', 'アクション', 'フラグ',
|
|
291
|
+
];
|
|
292
|
+
const lines = [headers.join(',')];
|
|
293
|
+
for (const ct of transactions) {
|
|
294
|
+
const cols = [
|
|
295
|
+
String(ct.row_number),
|
|
296
|
+
ct.transaction.date,
|
|
297
|
+
String(ct.transaction.amount),
|
|
298
|
+
csvEscape(ct.transaction.memo),
|
|
299
|
+
csvEscape(ct.transaction.partner_name || ''),
|
|
300
|
+
csvEscape(ct.category_name_ja || (ct.excluded ? `除外: ${ct.exclusion_rule}` : '未分類')),
|
|
301
|
+
ct.freee_account_code ? String(ct.freee_account_code) : '',
|
|
302
|
+
ct.confidence || '',
|
|
303
|
+
ct.action,
|
|
304
|
+
ct.routing_flags.join(';'),
|
|
305
|
+
];
|
|
306
|
+
lines.push(cols.join(','));
|
|
307
|
+
}
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|
|
310
|
+
function csvEscape(value) {
|
|
311
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
312
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Build a Markdown report summarizing the import results.
|
|
318
|
+
*/
|
|
319
|
+
function buildMarkdownReport(adapter, parsedCount, skippedCount, autoRegister, autoRegisterWithLog, humanReview, excluded) {
|
|
320
|
+
const total = parsedCount;
|
|
321
|
+
const autoCount = autoRegister.length;
|
|
322
|
+
const autoLogCount = autoRegisterWithLog.length;
|
|
323
|
+
const reviewCount = humanReview.length;
|
|
324
|
+
const excludedCount = excluded.length;
|
|
325
|
+
const classRate = total > 0
|
|
326
|
+
? (((autoCount + autoLogCount) / total) * 100).toFixed(1)
|
|
327
|
+
: '0';
|
|
328
|
+
let md = `# CSV Import Report\n\n`;
|
|
329
|
+
md += `**Source**: ${adapter.label}\n`;
|
|
330
|
+
md += `**Date**: ${new Date().toISOString().slice(0, 10)}\n\n`;
|
|
331
|
+
md += `## Summary\n\n`;
|
|
332
|
+
md += `| Metric | Value |\n|---|---|\n`;
|
|
333
|
+
md += `| Total rows | ${total + skippedCount} |\n`;
|
|
334
|
+
md += `| Parsed | ${total} |\n`;
|
|
335
|
+
md += `| Skipped | ${skippedCount} |\n`;
|
|
336
|
+
md += `| Auto-register (high confidence) | ${autoCount} |\n`;
|
|
337
|
+
md += `| Auto-register with log (medium) | ${autoLogCount} |\n`;
|
|
338
|
+
md += `| Human review required | ${reviewCount} |\n`;
|
|
339
|
+
md += `| Excluded (Stage 0) | ${excludedCount} |\n`;
|
|
340
|
+
md += `| **Classification rate** | **${classRate}%** |\n\n`;
|
|
341
|
+
// Category breakdown
|
|
342
|
+
const categoryMap = new Map();
|
|
343
|
+
for (const ct of [...autoRegister, ...autoRegisterWithLog]) {
|
|
344
|
+
const cat = ct.category_name_ja || '不明';
|
|
345
|
+
const existing = categoryMap.get(cat) || { count: 0, total: 0 };
|
|
346
|
+
existing.count++;
|
|
347
|
+
existing.total += ct.transaction.amount;
|
|
348
|
+
categoryMap.set(cat, existing);
|
|
349
|
+
}
|
|
350
|
+
if (categoryMap.size > 0) {
|
|
351
|
+
md += `## Category Breakdown\n\n`;
|
|
352
|
+
md += `| Category | Count | Total Amount |\n|---|---|---|\n`;
|
|
353
|
+
const sorted = [...categoryMap.entries()].sort((a, b) => b[1].total - a[1].total);
|
|
354
|
+
for (const [cat, { count, total }] of sorted) {
|
|
355
|
+
md += `| ${cat} | ${count} | ¥${total.toLocaleString()} |\n`;
|
|
356
|
+
}
|
|
357
|
+
md += '\n';
|
|
358
|
+
}
|
|
359
|
+
// Human review items
|
|
360
|
+
if (humanReview.length > 0) {
|
|
361
|
+
md += `## Human Review Required (${reviewCount} items)\n\n`;
|
|
362
|
+
md += `| Row | Date | Amount | Memo | Reason |\n|---|---|---|---|---|\n`;
|
|
363
|
+
for (const ct of humanReview.slice(0, 20)) {
|
|
364
|
+
const reasons = ct.routing_flags.join(', ') || ct.routing_reasons[0] || '';
|
|
365
|
+
md += `| ${ct.row_number} | ${ct.transaction.date} | ¥${ct.transaction.amount.toLocaleString()} | ${ct.transaction.memo.slice(0, 30)} | ${reasons} |\n`;
|
|
366
|
+
}
|
|
367
|
+
if (humanReview.length > 20) {
|
|
368
|
+
md += `| ... | ... | ... | ... | +${humanReview.length - 20} more |\n`;
|
|
369
|
+
}
|
|
370
|
+
md += '\n';
|
|
371
|
+
}
|
|
372
|
+
// Excluded items
|
|
373
|
+
if (excluded.length > 0) {
|
|
374
|
+
md += `## Excluded Transactions (${excludedCount} items)\n\n`;
|
|
375
|
+
md += `| Row | Date | Amount | Memo | Rule |\n|---|---|---|---|---|\n`;
|
|
376
|
+
for (const ct of excluded.slice(0, 10)) {
|
|
377
|
+
md += `| ${ct.row_number} | ${ct.transaction.date} | ¥${ct.transaction.amount.toLocaleString()} | ${ct.transaction.memo.slice(0, 30)} | ${ct.exclusion_rule || ''} |\n`;
|
|
378
|
+
}
|
|
379
|
+
if (excluded.length > 10) {
|
|
380
|
+
md += `| ... | ... | ... | ... | +${excluded.length - 10} more |\n`;
|
|
381
|
+
}
|
|
382
|
+
md += '\n';
|
|
383
|
+
}
|
|
384
|
+
return md;
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Transaction } from '../classifier/types.js';
|
|
2
|
+
/** Supported CSV source platforms. */
|
|
3
|
+
export type CsvSource = 'yayoi' | 'freee_export' | 'mf_export' | 'generic';
|
|
4
|
+
/**
|
|
5
|
+
* Column mapping for generic CSV import.
|
|
6
|
+
* User specifies which columns map to Transaction fields.
|
|
7
|
+
*/
|
|
8
|
+
export interface ColumnMapping {
|
|
9
|
+
date: string;
|
|
10
|
+
amount: string;
|
|
11
|
+
memo: string;
|
|
12
|
+
partner_name?: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A single parsed CSV row before conversion to Transaction.
|
|
17
|
+
*/
|
|
18
|
+
export interface ParsedCsvRow {
|
|
19
|
+
/** Original row index (1-based, excluding header). */
|
|
20
|
+
row_number: number;
|
|
21
|
+
/** Raw column values keyed by header name. */
|
|
22
|
+
raw: Record<string, string>;
|
|
23
|
+
/** Parsed Transaction (null if row was skipped/invalid). */
|
|
24
|
+
transaction: Transaction | null;
|
|
25
|
+
/** Skip reason (null if successfully parsed). */
|
|
26
|
+
skip_reason: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Result of CSV import + classification pipeline.
|
|
30
|
+
*/
|
|
31
|
+
export interface ImportResult {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
source: CsvSource;
|
|
34
|
+
/** Source format detected or specified. */
|
|
35
|
+
source_label: string;
|
|
36
|
+
total_rows: number;
|
|
37
|
+
parsed_count: number;
|
|
38
|
+
skipped_count: number;
|
|
39
|
+
warnings: string[];
|
|
40
|
+
auto_register: ClassifiedTransaction[];
|
|
41
|
+
auto_register_with_log: ClassifiedTransaction[];
|
|
42
|
+
human_review: ClassifiedTransaction[];
|
|
43
|
+
excluded: ClassifiedTransaction[];
|
|
44
|
+
summary: {
|
|
45
|
+
auto_register_count: number;
|
|
46
|
+
auto_register_with_log_count: number;
|
|
47
|
+
human_review_count: number;
|
|
48
|
+
excluded_count: number;
|
|
49
|
+
classification_rate: string;
|
|
50
|
+
};
|
|
51
|
+
csv_output?: string;
|
|
52
|
+
markdown_report?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A transaction that has been through the full pipeline
|
|
56
|
+
* (exclusion + classification + routing).
|
|
57
|
+
*/
|
|
58
|
+
export interface ClassifiedTransaction {
|
|
59
|
+
row_number: number;
|
|
60
|
+
transaction: Transaction;
|
|
61
|
+
excluded: boolean;
|
|
62
|
+
exclusion_rule?: string;
|
|
63
|
+
exclusion_reason?: string;
|
|
64
|
+
classified: boolean;
|
|
65
|
+
category_id?: string;
|
|
66
|
+
category_name_ja?: string;
|
|
67
|
+
confidence?: 'high' | 'medium' | 'low' | 'none';
|
|
68
|
+
matched_keyword?: string;
|
|
69
|
+
stage?: number;
|
|
70
|
+
freee_account_code?: number;
|
|
71
|
+
tax_code?: number;
|
|
72
|
+
tax_code_override?: number;
|
|
73
|
+
tax_code_reason?: string;
|
|
74
|
+
asset_tier?: string;
|
|
75
|
+
asset_warning?: string;
|
|
76
|
+
withholding_amount?: number;
|
|
77
|
+
withholding_rate?: string;
|
|
78
|
+
consumption_tax_rate?: number;
|
|
79
|
+
consumption_tax_reason?: string;
|
|
80
|
+
invoice_valid?: boolean;
|
|
81
|
+
invoice_deduction_rate?: number;
|
|
82
|
+
invoice_warnings?: string[];
|
|
83
|
+
tax_rule_warnings?: string[];
|
|
84
|
+
action: 'auto_register' | 'auto_register_with_log' | 'human_review';
|
|
85
|
+
routing_flags: string[];
|
|
86
|
+
routing_reasons: string[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Interface for platform-specific CSV adapters.
|
|
90
|
+
*/
|
|
91
|
+
export interface CsvAdapter {
|
|
92
|
+
readonly source: CsvSource;
|
|
93
|
+
readonly label: string;
|
|
94
|
+
/**
|
|
95
|
+
* Detect whether the given CSV headers match this adapter's format.
|
|
96
|
+
* @param headers - Array of column header strings from the first row.
|
|
97
|
+
* @returns true if this adapter can parse the CSV.
|
|
98
|
+
*/
|
|
99
|
+
detectFormat(headers: string[]): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Parse a single CSV row into a Transaction.
|
|
102
|
+
* @param row - Key-value record (header → value).
|
|
103
|
+
* @param rowNumber - 1-based row index.
|
|
104
|
+
* @returns Parsed Transaction, or null with reason if row should be skipped.
|
|
105
|
+
*/
|
|
106
|
+
parseRow(row: Record<string, string>, rowNumber: number): {
|
|
107
|
+
transaction: Transaction | null;
|
|
108
|
+
skip_reason: string | null;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// CSV adapter types for multi-platform import support.
|
|
2
|
+
//
|
|
3
|
+
// Supported sources:
|
|
4
|
+
// - yayoi: 弥生会計 仕訳日記帳 CSV export
|
|
5
|
+
// - freee_export: freee 取引CSV export
|
|
6
|
+
// - mf_export: MoneyForward CSV export (= Phase 1.B stub)
|
|
7
|
+
// - generic: 汎用 CSV (= user specifies column mapping)
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Transaction } from '../classifier/types.js';
|
|
2
|
+
import { CsvAdapter, CsvSource } from './types.js';
|
|
3
|
+
export declare class YayoiAdapter implements CsvAdapter {
|
|
4
|
+
readonly source: CsvSource;
|
|
5
|
+
readonly label = "\u5F25\u751F\u4F1A\u8A08 CSV";
|
|
6
|
+
private format;
|
|
7
|
+
detectFormat(headers: string[]): boolean;
|
|
8
|
+
parseRow(row: Record<string, string>, rowNumber: number): {
|
|
9
|
+
transaction: Transaction | null;
|
|
10
|
+
skip_reason: string | null;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Parse Format A: 仕訳日記帳 (double-entry).
|
|
14
|
+
*
|
|
15
|
+
* Logic:
|
|
16
|
+
* - Uses 借方金額 as amount (expense side). If 0, uses 貸方金額 (income side).
|
|
17
|
+
* - Combines 摘要 + 仕訳メモ for the memo field.
|
|
18
|
+
* - Skips rows where 識別フラグ indicates non-transaction lines (headers, totals).
|
|
19
|
+
*/
|
|
20
|
+
private parseFullRow;
|
|
21
|
+
/**
|
|
22
|
+
* Parse Format B: 簡易帳簿 (single-entry).
|
|
23
|
+
*/
|
|
24
|
+
private parseSimpleRow;
|
|
25
|
+
/**
|
|
26
|
+
* Parse date from various 弥生 formats:
|
|
27
|
+
* - "2026/05/01" (Western calendar)
|
|
28
|
+
* - "2026-05-01" (ISO)
|
|
29
|
+
* - "R08/05/01" (和暦 令和)
|
|
30
|
+
* - "H28/05/01" (和暦 平成)
|
|
31
|
+
* Returns ISO format "YYYY-MM-DD" or null.
|
|
32
|
+
*/
|
|
33
|
+
private parseDate;
|
|
34
|
+
/**
|
|
35
|
+
* Parse amount string, removing commas and handling negative values.
|
|
36
|
+
* "12,000" → 12000, "-3,000" → 3000 (absolute value for classification).
|
|
37
|
+
*/
|
|
38
|
+
private parseAmount;
|
|
39
|
+
/**
|
|
40
|
+
* Try to extract partner name from 弥生 Format A.
|
|
41
|
+
* Format A doesn't have a dedicated partner column, but some users
|
|
42
|
+
* put the partner name in 借方補助科目 or 貸方補助科目.
|
|
43
|
+
*/
|
|
44
|
+
private extractPartner;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=yayoi-adapter.d.ts.map
|