1use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8
9use crate::engine::coarse::{DEFAULT_NEGATIVE_KEYS, is_negative_candidate};
10use crate::error::{Result, RuntimeError};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct HierarchicalMapping {
17 pub name: String,
19 pub coarse_categories: IndexMap<String, String>,
21 pub fine_criteria: IndexMap<String, String>,
23 pub coarse_to_fine: IndexMap<String, Vec<String>>,
25 pub fine_to_coarse: IndexMap<String, String>,
27 #[serde(default)]
29 pub all_coarse_keys: Vec<String>,
30 #[serde(default)]
32 pub all_fine_keys: Vec<String>,
33 pub negative_keys: Vec<String>,
35}
36
37impl HierarchicalMapping {
38 pub fn builder(name: impl Into<String>) -> HierarchicalMappingBuilder {
40 HierarchicalMappingBuilder::new(name)
41 }
42
43 pub fn validate(&self) -> Result<()> {
52 if self.coarse_categories.is_empty() {
53 return Err(RuntimeError::InvalidQuestion(
54 "大分類 (coarse_categories) が 1 つも定義されていません。".to_string(),
55 ));
56 }
57
58 if self.fine_criteria.is_empty() {
59 return Err(RuntimeError::InvalidQuestion(
60 "細分類 (fine_criteria) が空です。".to_string(),
61 ));
62 }
63
64 let mut seen_fine_keys = indexmap::IndexSet::new();
66 for fine_list in self.coarse_to_fine.values() {
67 for fine_key in fine_list {
68 if !seen_fine_keys.insert(fine_key.clone()) {
69 return Err(RuntimeError::InvalidQuestion(format!(
70 "細分類 `{}` が複数の大分類に重複して割り当てられています (MECE 違反)。",
71 fine_key
72 )));
73 }
74 }
75 }
76
77 for (coarse_key, coarse_desc) in &self.coarse_categories {
79 if coarse_key.trim().is_empty() {
80 return Err(RuntimeError::InvalidQuestion(
81 "空の大分類キーが存在します。".to_string(),
82 ));
83 }
84 if coarse_desc.trim().is_empty() {
85 return Err(RuntimeError::InvalidQuestion(format!(
86 "大分類 `{}` の説明文が空です。",
87 coarse_key
88 )));
89 }
90
91 let fine_list = self.coarse_to_fine.get(coarse_key).ok_or_else(|| {
92 RuntimeError::InvalidQuestion(format!(
93 "大分類 `{}` に対する細分類リストが定義されていません。",
94 coarse_key
95 ))
96 })?;
97
98 if fine_list.is_empty() {
99 return Err(RuntimeError::InvalidQuestion(format!(
100 "大分類 `{}` 配下の細分類リストが空です。",
101 coarse_key
102 )));
103 }
104
105 for fine_key in fine_list {
106 if !self.fine_criteria.contains_key(fine_key) {
107 return Err(RuntimeError::InvalidQuestion(format!(
108 "大分類 `{}` 配下の細分類 `{}` が fine_criteria に存在しません。",
109 coarse_key, fine_key
110 )));
111 }
112
113 let mapped_coarse = self.fine_to_coarse.get(fine_key).ok_or_else(|| {
114 RuntimeError::InvalidQuestion(format!(
115 "細分類 `{}` から大分類への逆引きエントリが存在しません。",
116 fine_key
117 ))
118 })?;
119
120 if mapped_coarse != coarse_key {
121 return Err(RuntimeError::InvalidQuestion(format!(
122 "細分類 `{}` の逆引き大分類 (`{}`) が所属大分類 (`{}`) と一致しません。",
123 fine_key, mapped_coarse, coarse_key
124 )));
125 }
126 }
127 }
128
129 if seen_fine_keys.len() != self.fine_criteria.len() {
131 let missing_keys: Vec<_> = self
132 .fine_criteria
133 .keys()
134 .filter(|k| !seen_fine_keys.contains(*k))
135 .cloned()
136 .collect();
137 return Err(RuntimeError::InvalidQuestion(format!(
138 "いずれの大分類にも属していない孤立細分類が存在します: {:?}",
139 missing_keys
140 )));
141 }
142
143 Ok(())
144 }
145
146 pub fn coarse_criteria(&self) -> &IndexMap<String, String> {
148 &self.coarse_categories
149 }
150
151 pub fn fine_keys_for_coarse(&self, coarse_key: &str) -> Option<&[String]> {
153 self.coarse_to_fine.get(coarse_key).map(|v| v.as_slice())
154 }
155
156 pub fn coarse_for_fine(&self, fine_key: &str) -> Option<&str> {
158 self.fine_to_coarse.get(fine_key).map(|s| s.as_str())
159 }
160
161 pub fn total_fine_count(&self) -> usize {
163 self.fine_criteria.len()
164 }
165
166 pub fn total_coarse_count(&self) -> usize {
168 self.coarse_categories.len()
169 }
170
171 pub fn all_fine_keys(&self) -> &[String] {
173 &self.all_fine_keys
174 }
175
176 pub fn all_coarse_keys(&self) -> &[String] {
178 &self.all_coarse_keys
179 }
180
181 pub fn is_negative_candidate(&self, key: &str, description: &str) -> bool {
183 is_negative_candidate(key, description, &self.negative_keys)
184 }
185
186 pub fn banking77() -> Self {
190 let mut builder = Self::builder("Banking77-7Clusters");
191
192 builder.add_category(
194 "card_ops",
195 "カード発行・配送・更新・有効化および利用設定",
196 vec![
197 (
198 "activate_my_card",
199 "新しいカードを有効化・利用開始登録したい。",
200 ),
201 (
202 "card_about_to_expire",
203 "カードの有効期限が近づいているため更新したい。",
204 ),
205 (
206 "card_acceptance",
207 "カードが加盟店や決済端末で利用可能か確認したい。",
208 ),
209 (
210 "card_arrival",
211 "申し込んだカードがいつ届くか、配送状況を確認したい。",
212 ),
213 (
214 "card_delivery_estimate",
215 "カードの発送時期や到着予定日を知りたい。",
216 ),
217 (
218 "card_linking",
219 "カードをアプリや外部アカウントに連携・登録したい。",
220 ),
221 (
222 "card_not_working",
223 "カード決済や読み取りが正常に動作しない。",
224 ),
225 (
226 "card_swallowed",
227 "ATM や端末にカードが吸い込まれて戻らない。",
228 ),
229 (
230 "getting_spare_card",
231 "予備カードや追加カードの発行を申請したい。",
232 ),
233 (
234 "getting_virtual_card",
235 "即時利用可能なバーチャルカードを発行したい。",
236 ),
237 (
238 "order_physical_card",
239 "プラスチック製の物理カードを新規発行したい。",
240 ),
241 (
242 "pin_blocked",
243 "PIN コードの入力間違い等で暗証番号がロックされた。",
244 ),
245 ],
246 );
247
248 builder.add_category(
250 "transfers",
251 "口座間送金・銀行振込・送金エラーおよび送金取消",
252 vec![
253 (
254 "balance_not_updated_after_bank_transfer",
255 "銀行振込を行ったが残高に反映されない。",
256 ),
257 (
258 "beneficiary_not_allowed",
259 "振込先口座が登録できない、または送金不可と表示される。",
260 ),
261 (
262 "cancel_transfer",
263 "実行済みまたは予約中の送金・振込を取り消したい。",
264 ),
265 (
266 "failed_transfer",
267 "送金手続きそのものが失敗・エラーになった。",
268 ),
269 (
270 "pending_transfer",
271 "送金処理が保留中 (Pending) のまま完了しない。",
272 ),
273 (
274 "transfer_fee_charged",
275 "送金時に想定外の手数料が差し引かれた。",
276 ),
277 (
278 "transfer_into_account",
279 "外部口座から本口座への振込方法や着金を知りたい。",
280 ),
281 (
282 "transfer_not_received_by_recipient",
283 "送金完了通知が出たが受取人に届いていない。",
284 ),
285 (
286 "transfer_timing",
287 "送金が相手に届くまでの所要時間やスケジュールを知りたい。",
288 ),
289 (
290 "unable_to_verify_identity_for_transfer",
291 "送金時の本人確認ステップを通過できない。",
292 ),
293 (
294 "wrong_amount_sent",
295 "振込金額を誤って多く(または少なく)送金してしまった。",
296 ),
297 ],
298 );
299
300 builder.add_category(
302 "payments",
303 "店舗・オンライン決済・カード決済手数料および二重請求",
304 vec![
305 (
306 "card_payment_fee_charged",
307 "カード決済時に余分な手数料が請求された。",
308 ),
309 (
310 "card_payment_not_recognised",
311 "カードの利用明細に身に覚えのない請求がある。",
312 ),
313 (
314 "card_payment_wrong_exchange_rate",
315 "海外決済時の為替レートが想定と異なる。",
316 ),
317 (
318 "declined_card_payment",
319 "店舗またはオンラインでのカード決済が拒否された。",
320 ),
321 (
322 "direct_debit_payment_problem",
323 "口座引き落とし・自動引き落としの支払いに失敗した。",
324 ),
325 (
326 "disposable_card_limits",
327 "使い捨てバーチャルカードの利用限度額や制限を知りたい。",
328 ),
329 (
330 "extra_charge_on_statement",
331 "請求明細に身に覚えのない追加料金が加算されている。",
332 ),
333 (
334 "pending_card_payment",
335 "カード決済の引き落としが保留中になっている。",
336 ),
337 (
338 "refund_not_showing_up",
339 "返品・キャンセルの返金が口座に反映されない。",
340 ),
341 (
342 "transaction_charged_twice",
343 "1 回の買い物で同じ金額が二重に決済・請求された。",
344 ),
345 (
346 "unable_to_refund",
347 "アプリ上から返金処理・キャンセル処理を実行できない。",
348 ),
349 ],
350 );
351
352 builder.add_category(
354 "cash_atm",
355 "ATM 出金・現金引き出し・ATM 手数料トラブル",
356 vec![
357 (
358 "atm_support",
359 "利用可能な提携 ATM や利用方法について知りたい。",
360 ),
361 (
362 "cash_withdrawal_charge",
363 "ATM 現金引き出し時に想定外の手数料が引かれた。",
364 ),
365 (
366 "cash_withdrawal_not_recognised",
367 "利用明細に身に覚えのない ATM 現金引き出しがある。",
368 ),
369 (
370 "declined_cash_withdrawal",
371 "ATM での現金引き出しが拒否・失敗した。",
372 ),
373 (
374 "supported_cards_currencies_at_atm",
375 "ATM で利用可能なカード種別や対応通貨を知りたい。",
376 ),
377 (
378 "wrong_amount_of_cash_received",
379 "ATM から指定と異なる金額の現金が出てきた。",
380 ),
381 (
382 "wrong_exchange_rate_for_cash_withdrawal",
383 "海外 ATM で出金した際の為替レートが不当に高い。",
384 ),
385 (
386 "contactless_not_working",
387 "非接触 (タッチ) 決済がリーダーで反応しない。",
388 ),
389 (
390 "declined_transfer",
391 "システム規制や不正検知により送金が強制拒否された。",
392 ),
393 ],
394 );
395
396 builder.add_category(
398 "topup",
399 "残高チャージ・入金反映・チャージ失敗および限度額",
400 vec![
401 (
402 "automatic_top_up",
403 "残高連動のオートチャージ (自動入金) を設定・解除したい。",
404 ),
405 (
406 "balance_not_updated_after_cheque_or_cash_deposit",
407 "小切手や現金で入金したが口座残高に反映されない。",
408 ),
409 (
410 "pending_top_up",
411 "チャージ処理が処理中・保留のまま残高に加算されない。",
412 ),
413 (
414 "reverted_card_payment",
415 "カード決済が店舗側により取り消し・差し戻された。",
416 ),
417 (
418 "top_up_by_bank_transfer_charge",
419 "銀行振込による残高チャージで手数料が発生した。",
420 ),
421 (
422 "top_up_by_card_charge",
423 "クレジットカード等からのチャージで手数料が発生した。",
424 ),
425 (
426 "top_up_by_cash_or_cheque",
427 "現金や小切手による口座残高のチャージ方法を知りたい。",
428 ),
429 (
430 "top_up_failed",
431 "クレジットカード等からの残高チャージ操作が失敗した。",
432 ),
433 (
434 "top_up_limits",
435 "残高チャージの上限額や回数制限を確認したい。",
436 ),
437 (
438 "top_up_reverted",
439 "チャージした金額が取り消され元に戻ってしまった。",
440 ),
441 (
442 "verify_top_up",
443 "残高チャージ時に追加の認証や確認を求められた。",
444 ),
445 ],
446 );
447
448 builder.add_category(
450 "account_sec",
451 "口座設定・認証情報・紛失盗難対応・セキュリティ",
452 vec![
453 ("change_pin", "カードの暗証番号 (PIN) を変更したい。"),
454 (
455 "compromised_card",
456 "カード番号が漏洩・不正利用された疑いがある。",
457 ),
458 (
459 "edit_personal_details",
460 "氏名、住所、電話番号などの登録個人情報を更新したい。",
461 ),
462 (
463 "lost_or_stolen_card",
464 "カードを紛失した、または盗難に遭ったため即時停止したい。",
465 ),
466 (
467 "lost_or_stolen_phone",
468 "アプリが登録されている携帯電話を紛失・盗難された。",
469 ),
470 (
471 "passcode_forgotten",
472 "アプリのログインパスコードやパスワードを忘れた。",
473 ),
474 (
475 "terminate_account",
476 "サービスを退会し、口座・アカウントを解約したい。",
477 ),
478 (
479 "verify_my_identity",
480 "口座開設や取引制限解除のための本人確認を行いたい。",
481 ),
482 (
483 "verify_source_of_funds",
484 "資金源や入金根拠に関する書類確認を求められた。",
485 ),
486 (
487 "why_verify_identity",
488 "なぜ本人確認書類の提出が必要なのか理由を知りたい。",
489 ),
490 (
491 "age_limit",
492 "サービスの利用年齢制限や未成年者の利用条件を知りたい。",
493 ),
494 (
495 "country_support",
496 "本サービスが利用可能な対応国・地域を確認したい。",
497 ),
498 (
499 "visa_or_mastercard",
500 "発行されるカードの国際ブランド (Visa/Mastercard) を知りたい。",
501 ),
502 ],
503 );
504
505 builder.add_category(
507 "exchange_fee",
508 "外貨両替・為替手数料・アプリ内両替および付帯サービス",
509 vec![
510 (
511 "exchange_charge",
512 "通貨両替時に適用される手数料体系について知りたい。",
513 ),
514 (
515 "exchange_rate",
516 "現在の適用為替レートやリアルタイム換算レートを確認したい。",
517 ),
518 (
519 "exchange_via_app",
520 "アプリ内で複数の法定通貨・外貨を両替・保有したい。",
521 ),
522 (
523 "fiat_currency_support",
524 "サポートされている法定通貨の種類や制限を知りたい。",
525 ),
526 (
527 "pending_cash_withdrawal",
528 "ATM 現金引き出しの処理が完了待ちになっている。",
529 ),
530 (
531 "request_refund",
532 "店舗やサービス提供元に対して返金請求を行いたい。",
533 ),
534 (
535 "receiving_money",
536 "国内外からのお金の受け取り手順や受取口座情報を確認したい。",
537 ),
538 (
539 "transfer_fee_refund",
540 "誤って差し引かれた送金手数料の返還を求めたい。",
541 ),
542 (
543 "wrong_amount_refunded",
544 "店舗から返金された金額が購入金額と異なっている。",
545 ),
546 (
547 "other_or_unsupported",
548 "上記のいずれにも該当しないその他の問い合わせ。",
549 ),
550 ],
551 );
552
553 builder
554 .build()
555 .expect("Banking77 プリセットのバリデーションに合格する。")
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct HierarchicalMappingBuilder {
562 name: String,
563 coarse_categories: IndexMap<String, String>,
564 fine_criteria: IndexMap<String, String>,
565 coarse_to_fine: IndexMap<String, Vec<String>>,
566 fine_to_coarse: IndexMap<String, String>,
567 negative_keys: Vec<String>,
568}
569
570impl HierarchicalMappingBuilder {
571 pub fn new(name: impl Into<String>) -> Self {
573 Self {
574 name: name.into(),
575 coarse_categories: IndexMap::new(),
576 fine_criteria: IndexMap::new(),
577 coarse_to_fine: IndexMap::new(),
578 fine_to_coarse: IndexMap::new(),
579 negative_keys: DEFAULT_NEGATIVE_KEYS
580 .iter()
581 .map(|&s| s.to_string())
582 .collect(),
583 }
584 }
585
586 pub fn negative_keys(mut self, keys: Vec<String>) -> Self {
588 self.negative_keys = keys;
589 self
590 }
591
592 pub fn add_negative_key(mut self, key: impl Into<String>) -> Self {
594 self.negative_keys.push(key.into());
595 self
596 }
597
598 pub fn add_category(
605 &mut self,
606 coarse_key: impl Into<String>,
607 coarse_desc: impl Into<String>,
608 fine_items: Vec<(impl Into<String>, impl Into<String>)>,
609 ) -> &mut Self {
610 let coarse_k = coarse_key.into();
611 let coarse_d = coarse_desc.into();
612
613 let mut fine_keys = Vec::with_capacity(fine_items.len());
614 for (f_key, f_desc) in fine_items {
615 let fk = f_key.into();
616 let fd = f_desc.into();
617 self.fine_criteria.insert(fk.clone(), fd);
618 self.fine_to_coarse.insert(fk.clone(), coarse_k.clone());
619 fine_keys.push(fk);
620 }
621
622 self.coarse_categories.insert(coarse_k.clone(), coarse_d);
623 self.coarse_to_fine.insert(coarse_k, fine_keys);
624 self
625 }
626
627 pub fn build(self) -> Result<HierarchicalMapping> {
629 let all_coarse_keys: Vec<String> = self.coarse_categories.keys().cloned().collect();
630 let all_fine_keys: Vec<String> = self.fine_criteria.keys().cloned().collect();
631
632 let mapping = HierarchicalMapping {
633 name: self.name,
634 coarse_categories: self.coarse_categories,
635 fine_criteria: self.fine_criteria,
636 coarse_to_fine: self.coarse_to_fine,
637 fine_to_coarse: self.fine_to_coarse,
638 all_coarse_keys,
639 all_fine_keys,
640 negative_keys: self.negative_keys,
641 };
642 mapping.validate()?;
643 Ok(mapping)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn test_banking77_preset_validity() {
653 let mapping = HierarchicalMapping::banking77();
654 assert_eq!(mapping.total_coarse_count(), 7);
655 assert_eq!(mapping.total_fine_count(), 77);
656 assert!(mapping.validate().is_ok());
657
658 let card_fines = mapping.fine_keys_for_coarse("card_ops").unwrap();
660 assert_eq!(card_fines.len(), 12);
661 assert!(card_fines.contains(&"activate_my_card".to_string()));
662
663 assert_eq!(
665 mapping.coarse_for_fine("activate_my_card"),
666 Some("card_ops")
667 );
668 assert_eq!(
669 mapping.coarse_for_fine("transfer_timing"),
670 Some("transfers")
671 );
672 }
673
674 #[test]
675 fn test_validation_mece_violation() {
676 let mut builder = HierarchicalMapping::builder("InvalidMapping");
678 builder.add_category(
679 "cat1",
680 "カテゴリ 1",
681 vec![("item1", "アイテム 1"), ("dup_item", "重複アイテム")],
682 );
683 builder.add_category(
684 "cat2",
685 "カテゴリ 2",
686 vec![("item2", "アイテム 2"), ("dup_item", "重複アイテム")],
687 );
688 let res = builder.build();
689 assert!(res.is_err());
690 let err_msg = res.unwrap_err().to_string();
691 assert!(err_msg.contains("MECE 違反"));
692 }
693
694 #[test]
695 fn test_validation_empty_category() {
696 let mut builder = HierarchicalMapping::builder("EmptyCategory");
697 builder.add_category("cat1", "カテゴリ 1", vec![("item1", "アイテム 1")]);
698 builder.add_category("cat2", "カテゴリ 2", Vec::<(&str, &str)>::new());
699 let res = builder.build();
700 assert!(res.is_err());
701 let err_msg = res.unwrap_err().to_string();
702 assert!(err_msg.contains("空です"));
703 }
704
705 #[test]
706 fn test_negative_candidate_detection() {
707 let mapping = HierarchicalMapping::banking77();
708 assert!(mapping.is_negative_candidate(
709 "other_or_unsupported",
710 "上記のいずれにも該当しないその他の問い合わせ。"
711 ));
712 assert!(!mapping.is_negative_candidate("activate_my_card", "カード有効化。"));
713 }
714}