@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,449 @@
|
|
|
1
|
+
// Post-classification tax rule engine.
|
|
2
|
+
//
|
|
3
|
+
// Runs AFTER keyword classifier (Stage 1) or Claude classifier (Stage 2)
|
|
4
|
+
// has assigned a category. Applies Japanese tax law rules to refine the
|
|
5
|
+
// classification — tax codes, amount-based reclassification, withholding
|
|
6
|
+
// tax calculations, consumption-tax rate selection, and invoice system checks.
|
|
7
|
+
//
|
|
8
|
+
// The engine AUGMENTS the ClassificationResult; it never replaces it.
|
|
9
|
+
// Downstream code merges TaxRuleResult fields back into the pipeline output.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { normalizeMemo } from '../classifier/normalize.js';
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
17
|
+
function defaultDataDir() {
|
|
18
|
+
const envDir = process.env.COCKPIT_DATA_DIR;
|
|
19
|
+
if (envDir)
|
|
20
|
+
return envDir;
|
|
21
|
+
return path.resolve(__dirname, '../../../../data');
|
|
22
|
+
}
|
|
23
|
+
function containsAny(haystack, needles) {
|
|
24
|
+
return needles.some((n) => haystack.includes(n));
|
|
25
|
+
}
|
|
26
|
+
// ── Engine ─────────────────────────────────────────────────────────
|
|
27
|
+
export class TaxRuleEngine {
|
|
28
|
+
config;
|
|
29
|
+
normalizedOverseas;
|
|
30
|
+
normalizedDomestic;
|
|
31
|
+
normalizedJctIndicators;
|
|
32
|
+
normalizedNewspaper;
|
|
33
|
+
normalizedFoodBev;
|
|
34
|
+
normalizedResidential;
|
|
35
|
+
normalizedOverseasAds;
|
|
36
|
+
normalizedDomesticAds;
|
|
37
|
+
normalizedTakeout;
|
|
38
|
+
normalizedFoodPurchase;
|
|
39
|
+
normalizedCateringService;
|
|
40
|
+
constructor(configFile, dataDir) {
|
|
41
|
+
const dir = dataDir || defaultDataDir();
|
|
42
|
+
const file = configFile || path.join(dir, 'tax-rules', 'jp-tax-rules-v1.json');
|
|
43
|
+
if (!fs.existsSync(file)) {
|
|
44
|
+
throw new Error(`Tax rule config not found at ${file}. ` +
|
|
45
|
+
`Set COCKPIT_DATA_DIR env var or place data files at the expected path.`);
|
|
46
|
+
}
|
|
47
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
48
|
+
this.config = JSON.parse(raw);
|
|
49
|
+
// Pre-normalize all lookup lists once at construction time
|
|
50
|
+
this.normalizedOverseas = this.config.overseas_saas_providers.map(normalizeMemo);
|
|
51
|
+
this.normalizedDomestic = this.config.domestic_telecom_providers.map(normalizeMemo);
|
|
52
|
+
this.normalizedJctIndicators = this.config.jct_indicator_keywords.map(normalizeMemo);
|
|
53
|
+
this.normalizedNewspaper = this.config.consumption_tax.newspaper_keywords.map(normalizeMemo);
|
|
54
|
+
this.normalizedFoodBev = this.config.consumption_tax.food_beverage_keywords.map(normalizeMemo);
|
|
55
|
+
this.normalizedResidential = this.config.consumption_tax.residential_rent_keywords.map(normalizeMemo);
|
|
56
|
+
// Tier 2: New normalized lists
|
|
57
|
+
this.normalizedOverseasAds = (this.config.overseas_ad_platforms || []).map(normalizeMemo);
|
|
58
|
+
this.normalizedDomesticAds = (this.config.domestic_ad_platforms || []).map(normalizeMemo);
|
|
59
|
+
this.normalizedTakeout = (this.config.consumption_tax.takeout_delivery_keywords || []).map(normalizeMemo);
|
|
60
|
+
this.normalizedFoodPurchase = (this.config.consumption_tax.food_purchase_keywords || []).map(normalizeMemo);
|
|
61
|
+
this.normalizedCateringService = (this.config.consumption_tax.catering_with_service_keywords || []).map(normalizeMemo);
|
|
62
|
+
}
|
|
63
|
+
// ── Main entry point ──────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Apply all post-classification rules to a transaction.
|
|
66
|
+
* Returns a TaxRuleResult describing any adjustments to make.
|
|
67
|
+
*/
|
|
68
|
+
applyRules(tx, classification) {
|
|
69
|
+
const result = {
|
|
70
|
+
warnings: [],
|
|
71
|
+
rule_config_version: this.config.version,
|
|
72
|
+
};
|
|
73
|
+
if (!classification.classified) {
|
|
74
|
+
return result; // nothing to refine
|
|
75
|
+
}
|
|
76
|
+
// 1. Non-taxable category check (Tier 2: bulk handling for simple categories)
|
|
77
|
+
if (this.resolveNonTaxableCategory(classification, result)) {
|
|
78
|
+
return result; // early return — no further rules needed
|
|
79
|
+
}
|
|
80
|
+
// 2. Overseas SaaS tax-code correction
|
|
81
|
+
this.resolveOverseasSaasTaxCode(tx, classification, result);
|
|
82
|
+
// 3. Overseas advertising tax-code correction (Tier 2)
|
|
83
|
+
this.resolveOverseasAdTaxCode(tx, classification, result);
|
|
84
|
+
// 4. Asset capitalisation tier routing
|
|
85
|
+
this.resolveAssetCapitalization(tx, classification, result);
|
|
86
|
+
// 5. Withholding tax calculation
|
|
87
|
+
this.calculateWithholding(tx, classification, result);
|
|
88
|
+
// 6. Consumption tax rate refinement (only if not already overridden)
|
|
89
|
+
if (result.tax_code_override === undefined) {
|
|
90
|
+
this.resolveConsumptionTaxRate(tx, classification, result);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
// ── (a) Non-taxable category bulk handler (Tier 2) ────────────
|
|
95
|
+
/**
|
|
96
|
+
* If the category is in the non_taxable_categories list, immediately set
|
|
97
|
+
* tax_code to 0 with the configured reason. Returns true if handled.
|
|
98
|
+
*/
|
|
99
|
+
resolveNonTaxableCategory(classification, out) {
|
|
100
|
+
const catId = classification.category_id ?? '';
|
|
101
|
+
const nonTaxable = this.config.consumption_tax.non_taxable_categories || [];
|
|
102
|
+
if (!nonTaxable.includes(catId))
|
|
103
|
+
return false;
|
|
104
|
+
const reasons = this.config.consumption_tax.non_taxable_reasons || {};
|
|
105
|
+
const reason = reasons[catId] || `${catId} — 消費税対象外`;
|
|
106
|
+
out.tax_code_override = 0;
|
|
107
|
+
out.tax_code_reason = `${reason}のため税コード0に変更`;
|
|
108
|
+
out.consumption_tax_rate = 0;
|
|
109
|
+
out.consumption_tax_reason = reason;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// ── (b) Overseas SaaS tax-code override ───────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* If category is "communications" and the keyword/memo matches a known
|
|
115
|
+
* overseas SaaS provider, override tax_code to 0 (対象外).
|
|
116
|
+
*
|
|
117
|
+
* Exception: if the memo contains a JCT indicator (= the overseas provider
|
|
118
|
+
* is charging Japanese consumption tax via an invoice), keep tax_code 2.
|
|
119
|
+
*/
|
|
120
|
+
resolveOverseasSaasTaxCode(tx, classification, out) {
|
|
121
|
+
if (classification.category_id !== 'communications')
|
|
122
|
+
return;
|
|
123
|
+
const normalizedMemo = normalizeMemo(tx.memo);
|
|
124
|
+
const normalizedKeyword = classification.matched_keyword
|
|
125
|
+
? normalizeMemo(classification.matched_keyword)
|
|
126
|
+
: '';
|
|
127
|
+
// Check domestic providers first — if domestic, no override needed
|
|
128
|
+
if (this.matchesProvider(normalizedMemo, normalizedKeyword, this.normalizedDomestic)) {
|
|
129
|
+
return; // domestic telecom -> keep default tax_code (2 = 10%)
|
|
130
|
+
}
|
|
131
|
+
// Check overseas providers
|
|
132
|
+
if (!this.matchesProvider(normalizedMemo, normalizedKeyword, this.normalizedOverseas)) {
|
|
133
|
+
return; // not a recognised overseas provider
|
|
134
|
+
}
|
|
135
|
+
// JCT exception: if memo indicates the provider IS charging JP consumption tax
|
|
136
|
+
if (containsAny(normalizedMemo, this.normalizedJctIndicators)) {
|
|
137
|
+
out.warnings.push('海外SaaSだがJCT/消費税/インボイス表記あり — 税コード2(10%)を維持します。適格請求書の確認を推奨。');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Override to tax_code 0 (対象外)
|
|
141
|
+
out.tax_code_override = 0;
|
|
142
|
+
out.tax_code_reason =
|
|
143
|
+
'海外SaaSプロバイダー — 国外取引のため消費税対象外(税コード0)に変更。';
|
|
144
|
+
out.consumption_tax_rate = 0;
|
|
145
|
+
out.consumption_tax_reason = '国外取引 — 消費税対象外';
|
|
146
|
+
}
|
|
147
|
+
// ── (b2) Overseas advertising tax-code override (Tier 2) ──────
|
|
148
|
+
/**
|
|
149
|
+
* If category is "advertising" and the keyword/memo matches a known
|
|
150
|
+
* overseas ad platform, override tax_code to 0 (対象外).
|
|
151
|
+
*
|
|
152
|
+
* Google Ads, Meta Ads etc. are invoiced from overseas entities.
|
|
153
|
+
* Same JCT exception as overseas SaaS.
|
|
154
|
+
*/
|
|
155
|
+
resolveOverseasAdTaxCode(tx, classification, out) {
|
|
156
|
+
if (classification.category_id !== 'advertising')
|
|
157
|
+
return;
|
|
158
|
+
if (out.tax_code_override !== undefined)
|
|
159
|
+
return; // already handled
|
|
160
|
+
const normalizedMemo = normalizeMemo(tx.memo);
|
|
161
|
+
const normalizedKeyword = classification.matched_keyword
|
|
162
|
+
? normalizeMemo(classification.matched_keyword)
|
|
163
|
+
: '';
|
|
164
|
+
// Check domestic ad platforms first
|
|
165
|
+
if (this.matchesProvider(normalizedMemo, normalizedKeyword, this.normalizedDomesticAds)) {
|
|
166
|
+
return; // domestic ad platform -> keep default tax_code (2 = 10%)
|
|
167
|
+
}
|
|
168
|
+
// Check overseas ad platforms
|
|
169
|
+
if (!this.matchesProvider(normalizedMemo, normalizedKeyword, this.normalizedOverseasAds)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// JCT exception
|
|
173
|
+
if (containsAny(normalizedMemo, this.normalizedJctIndicators)) {
|
|
174
|
+
out.warnings.push('海外広告プラットフォームだがJCT/消費税/インボイス表記あり — 税コード2(10%)を維持。');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
out.tax_code_override = 0;
|
|
178
|
+
out.tax_code_reason =
|
|
179
|
+
'海外広告プラットフォーム — 国外取引のため消費税対象外(税コード0)に変更。';
|
|
180
|
+
out.consumption_tax_rate = 0;
|
|
181
|
+
out.consumption_tax_reason = '国外取引(海外広告) — 消費税対象外';
|
|
182
|
+
}
|
|
183
|
+
// ── (c) Asset capitalisation tier routing ─────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* If category is "consumables" or "supplies" and the amount is significant,
|
|
186
|
+
* determine the correct asset capitalisation tier and add warnings.
|
|
187
|
+
*/
|
|
188
|
+
resolveAssetCapitalization(tx, classification, out) {
|
|
189
|
+
const catId = classification.category_id ?? '';
|
|
190
|
+
if (catId !== 'consumables' && catId !== 'supplies')
|
|
191
|
+
return;
|
|
192
|
+
const amount = Math.abs(tx.amount);
|
|
193
|
+
const thresholds = this.config.asset_capitalization;
|
|
194
|
+
if (amount <= thresholds.expense_max) {
|
|
195
|
+
// Tier: expense — OK as-is
|
|
196
|
+
out.asset_tier = 'expense';
|
|
197
|
+
out.asset_tier_label = '少額経費 (損金算入OK)';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (amount <= thresholds.lump_sum_3yr_max) {
|
|
201
|
+
// Tier: 一括償却資産 (3-year straight-line)
|
|
202
|
+
out.asset_tier = 'lump_sum_3yr';
|
|
203
|
+
out.asset_tier_label = '一括償却資産 (3年均等償却)';
|
|
204
|
+
out.asset_warning =
|
|
205
|
+
`金額${amount.toLocaleString()}円: 一括償却資産(3年均等)の可能性あり。税理士確認を推奨。`;
|
|
206
|
+
out.warnings.push(out.asset_warning);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (amount <= thresholds.sme_immediate_max) {
|
|
210
|
+
// Tier: 少額減価償却資産 (SME immediate expense)
|
|
211
|
+
out.asset_tier = 'sme_immediate';
|
|
212
|
+
out.asset_tier_label = '少額減価償却資産 (中小企業少額特例・即時損金)';
|
|
213
|
+
out.asset_warning =
|
|
214
|
+
`金額${amount.toLocaleString()}円: 中小企業少額特例(即時損金算入)の適用可能性あり。` +
|
|
215
|
+
`年間合計300万円上限に注意。税理士確認を推奨。`;
|
|
216
|
+
out.warnings.push(out.asset_warning);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Tier: 固定資産 — must be reclassified
|
|
220
|
+
out.asset_tier = 'fixed_asset';
|
|
221
|
+
out.asset_tier_label = '固定資産 (減価償却必要)';
|
|
222
|
+
out.asset_category_override = 'tools_equipment'; // 工具器具備品
|
|
223
|
+
out.asset_warning =
|
|
224
|
+
`金額${amount.toLocaleString()}円: 固定資産として計上が必要です(工具器具備品等)。` +
|
|
225
|
+
`減価償却が必要 — 必ず税理士レビューを実施してください。`;
|
|
226
|
+
out.warnings.push(out.asset_warning);
|
|
227
|
+
}
|
|
228
|
+
// ── (d) Withholding tax calculation ───────────────────────────
|
|
229
|
+
/**
|
|
230
|
+
* For professional_fee category, calculate informational withholding tax
|
|
231
|
+
* (源泉徴収税額) to help tax accountants verify freee entries.
|
|
232
|
+
*/
|
|
233
|
+
calculateWithholding(tx, classification, out) {
|
|
234
|
+
if (classification.category_id !== 'professional_fee')
|
|
235
|
+
return;
|
|
236
|
+
const gross = Math.abs(tx.amount);
|
|
237
|
+
const { bracket_1_ceiling, rate_bracket_1, rate_bracket_2 } = this.config.withholding_tax;
|
|
238
|
+
let withholdingAmount;
|
|
239
|
+
let rateDesc;
|
|
240
|
+
if (gross <= bracket_1_ceiling) {
|
|
241
|
+
withholdingAmount = Math.floor(gross * rate_bracket_1);
|
|
242
|
+
rateDesc = `${(rate_bracket_1 * 100).toFixed(2)}% (${bracket_1_ceiling.toLocaleString()}円以下)`;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const part1 = Math.floor(bracket_1_ceiling * rate_bracket_1);
|
|
246
|
+
const part2 = Math.floor((gross - bracket_1_ceiling) * rate_bracket_2);
|
|
247
|
+
withholdingAmount = part1 + part2;
|
|
248
|
+
rateDesc =
|
|
249
|
+
`${bracket_1_ceiling.toLocaleString()}円まで${(rate_bracket_1 * 100).toFixed(2)}% + ` +
|
|
250
|
+
`超過分${(rate_bracket_2 * 100).toFixed(2)}%`;
|
|
251
|
+
}
|
|
252
|
+
const withholding = {
|
|
253
|
+
gross_amount: gross,
|
|
254
|
+
withholding_amount: withholdingAmount,
|
|
255
|
+
net_amount: gross - withholdingAmount,
|
|
256
|
+
rate_description: rateDesc,
|
|
257
|
+
};
|
|
258
|
+
out.withholding = withholding;
|
|
259
|
+
out.warnings.push(`源泉徴収税額(税理士参考情報): ${withholdingAmount.toLocaleString()}円 — freee登録値と照合を推奨。`);
|
|
260
|
+
}
|
|
261
|
+
// ── (e) Consumption tax rate refinement ───────────────────────
|
|
262
|
+
/**
|
|
263
|
+
* Determine the correct consumption tax rate based on category and keywords.
|
|
264
|
+
*
|
|
265
|
+
* Decision tree (Tier 2 expanded):
|
|
266
|
+
* - overseas SaaS/ads -> 0% (handled earlier)
|
|
267
|
+
* - non-taxable categories -> 0% (handled by resolveNonTaxableCategory)
|
|
268
|
+
* - books_magazines + newspaper keyword -> 8% (軽減税率)
|
|
269
|
+
* - meeting_meal + takeout/delivery -> 8%
|
|
270
|
+
* - meeting_meal + food purchase (convenience store) -> 8%
|
|
271
|
+
* - meeting_meal + catering with service -> 10%
|
|
272
|
+
* - meeting_meal (default dine-in) -> 10%
|
|
273
|
+
* - rent + residential keyword -> 0% (非課税)
|
|
274
|
+
*/
|
|
275
|
+
resolveConsumptionTaxRate(tx, classification, out) {
|
|
276
|
+
const catId = classification.category_id ?? '';
|
|
277
|
+
const normalizedMemo = normalizeMemo(tx.memo);
|
|
278
|
+
// Newspaper subscription -> reduced rate 8%
|
|
279
|
+
if (catId === 'books_magazines') {
|
|
280
|
+
if (containsAny(normalizedMemo, this.normalizedNewspaper)) {
|
|
281
|
+
out.consumption_tax_rate = this.config.consumption_tax.reduced_rate;
|
|
282
|
+
out.consumption_tax_reason = '定期購読の新聞 — 軽減税率8%適用';
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Meeting meals -> complex reduced-rate logic
|
|
287
|
+
if (catId === 'meeting_meal') {
|
|
288
|
+
this.resolveMeetingMealTaxRate(normalizedMemo, out);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Entertainment (交際費) -> also check food/takeout
|
|
292
|
+
if (catId === 'entertainment') {
|
|
293
|
+
// Takeout/delivery for entertainment is still 8%
|
|
294
|
+
if (containsAny(normalizedMemo, this.normalizedTakeout)) {
|
|
295
|
+
out.consumption_tax_rate = this.config.consumption_tax.reduced_rate;
|
|
296
|
+
out.consumption_tax_reason = 'テイクアウト/デリバリー(交際費) — 軽減税率8%適用の可能性あり';
|
|
297
|
+
out.warnings.push('交際費のテイクアウト判定: 軽減税率8%を適用。店内飲食の場合は10%に要修正。');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Residential rent -> exempt 0%
|
|
302
|
+
if (catId === 'rent') {
|
|
303
|
+
if (containsAny(normalizedMemo, this.normalizedResidential)) {
|
|
304
|
+
out.consumption_tax_rate = this.config.consumption_tax.exempt_rate;
|
|
305
|
+
out.consumption_tax_reason = '住居用賃料 — 消費税非課税';
|
|
306
|
+
out.tax_code_override = 0;
|
|
307
|
+
out.tax_code_reason = '住居用賃料 — 消費税非課税のため税コード0に変更';
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Detailed meeting_meal consumption tax resolution (Tier 2).
|
|
314
|
+
*
|
|
315
|
+
* Priority order:
|
|
316
|
+
* 1. Catering with service (配膳あり) -> 10% standard
|
|
317
|
+
* 2. Takeout / delivery -> 8% reduced
|
|
318
|
+
* 3. Food purchase (convenience store, supermarket) -> 8% reduced
|
|
319
|
+
* 4. Default dine-in -> 10% standard
|
|
320
|
+
*/
|
|
321
|
+
resolveMeetingMealTaxRate(normalizedMemo, out) {
|
|
322
|
+
// 1. Catering with serving staff -> standard rate
|
|
323
|
+
if (containsAny(normalizedMemo, this.normalizedCateringService)) {
|
|
324
|
+
out.consumption_tax_rate = this.config.consumption_tax.standard_rate;
|
|
325
|
+
out.consumption_tax_reason = 'ケータリング(配膳サービス付き) — 標準税率10%適用(飲食サービスに該当)';
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// 2. Takeout / delivery -> reduced rate
|
|
329
|
+
if (containsAny(normalizedMemo, this.normalizedTakeout)) {
|
|
330
|
+
out.consumption_tax_rate = this.config.consumption_tax.reduced_rate;
|
|
331
|
+
out.consumption_tax_reason = 'テイクアウト/デリバリー — 軽減税率8%適用';
|
|
332
|
+
out.warnings.push('テイクアウト判定: 軽減税率8%を適用。店内飲食の場合は10%に要修正。');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// 3. Food purchase from convenience store / supermarket -> reduced rate
|
|
336
|
+
if (containsAny(normalizedMemo, this.normalizedFoodPurchase)) {
|
|
337
|
+
out.consumption_tax_rate = this.config.consumption_tax.reduced_rate;
|
|
338
|
+
out.consumption_tax_reason = '食品購入(持ち帰り前提) — 軽減税率8%適用';
|
|
339
|
+
out.warnings.push('コンビニ/スーパー等の食品購入: 軽減税率8%を適用。イートイン利用の場合は10%に要修正。');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// 4. Default: dine-in -> standard rate
|
|
343
|
+
out.consumption_tax_rate = this.config.consumption_tax.standard_rate;
|
|
344
|
+
out.consumption_tax_reason = '会議費(飲食) — 標準税率10%(店内飲食前提)';
|
|
345
|
+
}
|
|
346
|
+
// ── (f) Invoice system checker (Tier 3) ───────────────────────
|
|
347
|
+
/**
|
|
348
|
+
* Validate an invoice registration number and calculate the transitional
|
|
349
|
+
* period deduction rate.
|
|
350
|
+
*
|
|
351
|
+
* @param registrationNumber T + 13 digits (e.g. "T1234567890123")
|
|
352
|
+
* @param txDate Transaction date (YYYY-MM-DD) for period lookup
|
|
353
|
+
* @param taxAmount Consumption tax amount for deduction calculation
|
|
354
|
+
*/
|
|
355
|
+
checkInvoice(registrationNumber, txDate, taxAmount) {
|
|
356
|
+
const warnings = [];
|
|
357
|
+
// Format validation
|
|
358
|
+
const validFormat = this.validateRegistrationNumber(registrationNumber);
|
|
359
|
+
// Period lookup
|
|
360
|
+
const { deduction_rate, label } = this.getTransitionalPeriod(txDate);
|
|
361
|
+
// Small business exception
|
|
362
|
+
const smallBizThreshold = this.config.invoice_system.small_business_threshold;
|
|
363
|
+
const smallBusinessException = (taxAmount !== undefined && taxAmount < smallBizThreshold);
|
|
364
|
+
// Calculate deductible amounts
|
|
365
|
+
let deductibleAmount;
|
|
366
|
+
let nonDeductibleAmount;
|
|
367
|
+
if (taxAmount !== undefined) {
|
|
368
|
+
if (validFormat) {
|
|
369
|
+
// Registered vendor -> full deduction
|
|
370
|
+
deductibleAmount = taxAmount;
|
|
371
|
+
nonDeductibleAmount = 0;
|
|
372
|
+
}
|
|
373
|
+
else if (smallBusinessException) {
|
|
374
|
+
// Small business exception -> full deduction from ledger alone
|
|
375
|
+
deductibleAmount = taxAmount;
|
|
376
|
+
nonDeductibleAmount = 0;
|
|
377
|
+
warnings.push(`${this.config.invoice_system.small_business_note}`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Non-registered vendor -> transitional deduction
|
|
381
|
+
deductibleAmount = Math.floor(taxAmount * (deduction_rate / 100));
|
|
382
|
+
nonDeductibleAmount = taxAmount - deductibleAmount;
|
|
383
|
+
if (deduction_rate < 100) {
|
|
384
|
+
warnings.push(`適格請求書番号なし — ${label}。` +
|
|
385
|
+
`仕入税額控除: ${deductibleAmount.toLocaleString()}円(${deduction_rate}%)、` +
|
|
386
|
+
`控除不可額: ${nonDeductibleAmount.toLocaleString()}円。`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!registrationNumber) {
|
|
391
|
+
if (!smallBusinessException) {
|
|
392
|
+
warnings.push('適格請求書発行事業者の登録番号が確認できません。仕入税額控除の可否を確認してください。');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else if (!validFormat) {
|
|
396
|
+
warnings.push(`登録番号「${registrationNumber}」の形式が不正です。正しい形式: T + 数字13桁 (例: T1234567890123)。`);
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
valid_format: validFormat,
|
|
400
|
+
registration_number: registrationNumber,
|
|
401
|
+
deduction_rate: validFormat ? 100 : deduction_rate,
|
|
402
|
+
period_label: label,
|
|
403
|
+
deductible_amount: deductibleAmount,
|
|
404
|
+
non_deductible_amount: nonDeductibleAmount,
|
|
405
|
+
small_business_exception: smallBusinessException,
|
|
406
|
+
warnings,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Validate T + 13 digits format.
|
|
411
|
+
*/
|
|
412
|
+
validateRegistrationNumber(num) {
|
|
413
|
+
if (!num)
|
|
414
|
+
return false;
|
|
415
|
+
const prefix = this.config.invoice_system.registration_prefix;
|
|
416
|
+
const digits = this.config.invoice_system.registration_digits;
|
|
417
|
+
const pattern = new RegExp(`^${prefix}\\d{${digits}}$`);
|
|
418
|
+
return pattern.test(num);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get the transitional period deduction rate for a given date.
|
|
422
|
+
*/
|
|
423
|
+
getTransitionalPeriod(txDate) {
|
|
424
|
+
const periods = this.config.invoice_system.transitional_periods;
|
|
425
|
+
for (const period of periods) {
|
|
426
|
+
if (txDate >= period.start && txDate <= period.end) {
|
|
427
|
+
return { deduction_rate: period.deduction_rate, label: period.label };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Before invoice system (pre-2023-10-01) -> full deduction
|
|
431
|
+
return { deduction_rate: 100, label: 'インボイス制度施行前 — 仕入税額控除100%' };
|
|
432
|
+
}
|
|
433
|
+
// ── Utilities ─────────────────────────────────────────────────
|
|
434
|
+
/**
|
|
435
|
+
* Check if the memo or matched keyword matches any provider in the list.
|
|
436
|
+
*/
|
|
437
|
+
matchesProvider(normalizedMemo, normalizedKeyword, providerList) {
|
|
438
|
+
// Check the matched keyword first (most specific)
|
|
439
|
+
if (normalizedKeyword && providerList.includes(normalizedKeyword)) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
// Fall back to substring match in memo
|
|
443
|
+
return containsAny(normalizedMemo, providerList);
|
|
444
|
+
}
|
|
445
|
+
getVersion() {
|
|
446
|
+
return this.config.version;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
//# sourceMappingURL=tax-rule-engine.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface WithholdingResult {
|
|
2
|
+
/** Gross payment (same as tx.amount) */
|
|
3
|
+
gross_amount: number;
|
|
4
|
+
/** Calculated withholding tax amount */
|
|
5
|
+
withholding_amount: number;
|
|
6
|
+
/** Net payment after withholding */
|
|
7
|
+
net_amount: number;
|
|
8
|
+
/** Human-readable rate description, e.g. "10.21% flat" */
|
|
9
|
+
rate_description: string;
|
|
10
|
+
}
|
|
11
|
+
export type AssetCapitalizationTier = 'expense' | 'lump_sum_3yr' | 'sme_immediate' | 'fixed_asset';
|
|
12
|
+
export interface TaxRuleResult {
|
|
13
|
+
/** If set, the engine recommends a different tax_code than the classifier assigned. */
|
|
14
|
+
tax_code_override?: number;
|
|
15
|
+
/** Why the override was applied (human-readable, Japanese). */
|
|
16
|
+
tax_code_reason?: string;
|
|
17
|
+
/** Consumption-tax rate override (0, 8, or 10). */
|
|
18
|
+
consumption_tax_rate?: number;
|
|
19
|
+
/** Reason for consumption-tax override. */
|
|
20
|
+
consumption_tax_reason?: string;
|
|
21
|
+
/** Asset capitalisation tier (only set when relevant). */
|
|
22
|
+
asset_tier?: AssetCapitalizationTier;
|
|
23
|
+
/** Human-readable asset-tier label in Japanese. */
|
|
24
|
+
asset_tier_label?: string;
|
|
25
|
+
/** Warning string for asset-tier reclassification. */
|
|
26
|
+
asset_warning?: string;
|
|
27
|
+
/** Suggested override category_id for fixed-asset reclassification. */
|
|
28
|
+
asset_category_override?: string;
|
|
29
|
+
/** Withholding-tax calculation (only for professional_fee). */
|
|
30
|
+
withholding?: WithholdingResult;
|
|
31
|
+
/** Invoice system check result (if registration number found or non-registered vendor). */
|
|
32
|
+
invoice_check?: InvoiceCheckResult;
|
|
33
|
+
/** Aggregated warnings / flags for human review. */
|
|
34
|
+
warnings: string[];
|
|
35
|
+
/** Version of the rule config that produced this result. */
|
|
36
|
+
rule_config_version: string;
|
|
37
|
+
}
|
|
38
|
+
export interface TaxRuleConfig {
|
|
39
|
+
version: string;
|
|
40
|
+
locale: string;
|
|
41
|
+
overseas_saas_providers: string[];
|
|
42
|
+
domestic_telecom_providers: string[];
|
|
43
|
+
jct_indicator_keywords: string[];
|
|
44
|
+
asset_capitalization: {
|
|
45
|
+
expense_max: number;
|
|
46
|
+
lump_sum_3yr_max: number;
|
|
47
|
+
sme_immediate_max: number;
|
|
48
|
+
};
|
|
49
|
+
withholding_tax: {
|
|
50
|
+
/** First bracket ceiling (yen). */
|
|
51
|
+
bracket_1_ceiling: number;
|
|
52
|
+
/** Rate for amounts up to bracket_1_ceiling. */
|
|
53
|
+
rate_bracket_1: number;
|
|
54
|
+
/** Rate for amount exceeding bracket_1_ceiling. */
|
|
55
|
+
rate_bracket_2: number;
|
|
56
|
+
};
|
|
57
|
+
overseas_ad_platforms: string[];
|
|
58
|
+
domestic_ad_platforms: string[];
|
|
59
|
+
consumption_tax: {
|
|
60
|
+
standard_rate: number;
|
|
61
|
+
reduced_rate: number;
|
|
62
|
+
exempt_rate: number;
|
|
63
|
+
newspaper_keywords: string[];
|
|
64
|
+
food_beverage_keywords: string[];
|
|
65
|
+
takeout_delivery_keywords: string[];
|
|
66
|
+
food_purchase_keywords: string[];
|
|
67
|
+
catering_with_service_keywords: string[];
|
|
68
|
+
residential_rent_keywords: string[];
|
|
69
|
+
non_taxable_categories: string[];
|
|
70
|
+
non_taxable_reasons: Record<string, string>;
|
|
71
|
+
};
|
|
72
|
+
invoice_system: {
|
|
73
|
+
registration_prefix: string;
|
|
74
|
+
registration_digits: number;
|
|
75
|
+
transitional_periods: {
|
|
76
|
+
start: string;
|
|
77
|
+
end: string;
|
|
78
|
+
deduction_rate: number;
|
|
79
|
+
label: string;
|
|
80
|
+
}[];
|
|
81
|
+
small_business_threshold: number;
|
|
82
|
+
small_business_note: string;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface InvoiceCheckResult {
|
|
86
|
+
/** Whether the registration number is valid format (T + 13 digits). */
|
|
87
|
+
valid_format: boolean;
|
|
88
|
+
/** The registration number checked. */
|
|
89
|
+
registration_number?: string;
|
|
90
|
+
/** Current deduction rate based on transitional period. */
|
|
91
|
+
deduction_rate: number;
|
|
92
|
+
/** Label for the current period. */
|
|
93
|
+
period_label: string;
|
|
94
|
+
/** Amount that can be deducted (tax_amount * deduction_rate). */
|
|
95
|
+
deductible_amount?: number;
|
|
96
|
+
/** Non-deductible amount. */
|
|
97
|
+
non_deductible_amount?: number;
|
|
98
|
+
/** Whether small-business exception applies (< 10,000 yen). */
|
|
99
|
+
small_business_exception: boolean;
|
|
100
|
+
/** Warnings for the tax accountant. */
|
|
101
|
+
warnings: string[];
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Types for the post-classification tax rule engine.
|
|
2
|
+
//
|
|
3
|
+
// These types augment — never replace — the ClassificationResult from
|
|
4
|
+
// src/classifier/types.ts. The engine returns adjustments that the pipeline
|
|
5
|
+
// layer merges back into the final result.
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kansei-link/bantou",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "番頭 — AI仕訳アシスタント。日本の税理士事務所向け会計自動化MCPサーバー。freee・弥生・MF・TKC対応。修正を永久記憶し、使うほど賢くなる。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bantou": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"!dist/**/__tests__/**",
|
|
14
|
+
"data/keyword-dict/jp-tax-baseline-v1.json",
|
|
15
|
+
"data/keyword-dict-schema.json",
|
|
16
|
+
"data/exclusion-rules/jp-tax-baseline-v1.json",
|
|
17
|
+
"data/exclusion-rules-schema.json",
|
|
18
|
+
"data/tax-rules/jp-tax-rules-v1.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"start": "node dist/index.js",
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"doctor:freee": "tsx src/bin/freee-doctor.ts",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"smoke": "node tests/smoke.mjs",
|
|
29
|
+
"copy-data": "node -e \"require('fs').cpSync('../../data','./data',{recursive:true})\"",
|
|
30
|
+
"clean-data": "node -e \"require('fs').rmSync('./data',{recursive:true,force:true})\"",
|
|
31
|
+
"prepublishOnly": "npm run copy-data && npm run build && npm test"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"accounting",
|
|
37
|
+
"freee",
|
|
38
|
+
"money-forward",
|
|
39
|
+
"tax-accountant",
|
|
40
|
+
"japan",
|
|
41
|
+
"automation",
|
|
42
|
+
"claude",
|
|
43
|
+
"claude-code"
|
|
44
|
+
],
|
|
45
|
+
"author": "Synapse Arrows PTE. LTD.",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"homepage": "https://github.com/michielinksee/kansei-link-cockpit",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/michielinksee/kansei-link-cockpit.git",
|
|
51
|
+
"directory": "packages/cockpit-mcp"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/michielinksee/kansei-link-cockpit/issues"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@anthropic-ai/sdk": "^0.40.1",
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
59
|
+
"better-sqlite3": "^12.9.0",
|
|
60
|
+
"zod": "^3.23.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^22.7.0",
|
|
64
|
+
"tsx": "^4.19.0",
|
|
65
|
+
"typescript": "^5.6.0",
|
|
66
|
+
"vitest": "^2.0.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=20"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
}
|
|
74
|
+
}
|