sophhub 0.4.21 → 0.4.23

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.
Files changed (78) hide show
  1. package/agents/parent-toddler/.config.json +7 -1
  2. package/agents/parent-toddler/HEARTBEAT.md +4 -4
  3. package/agents/parent-toddler/TOOLS.md +9 -0
  4. package/agents/parent-toddler/scripts/compact_sessions_over_threshold.py +368 -0
  5. package/package.json +1 -1
  6. package/skills/agent-install/src/SKILL.md +2 -0
  7. package/skills/agent-install/src/scripts/common.py +20 -0
  8. package/skills/agent-install/src/scripts/update_openclaw.py +5 -0
  9. package/skills/image-classify/skill.json +12 -5
  10. package/skills/image-classify/src/SKILL.md +1 -1
  11. package/skills/image-classify/src/references/config.json +1 -1
  12. package/skills/insurance-customer-policy/skill.json +12 -0
  13. package/skills/insurance-customer-policy/src/SKILL.md +121 -0
  14. package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
  15. package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
  16. package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
  17. package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
  18. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
  19. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
  20. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
  21. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
  22. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
  23. package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
  24. package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
  25. package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
  26. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
  27. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
  28. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
  29. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
  30. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
  31. package/skills/insurance-product-analysis/skill.json +12 -0
  32. package/skills/insurance-product-analysis/src/SKILL.md +99 -0
  33. package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
  34. package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
  35. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
  36. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
  37. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
  38. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
  39. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
  40. package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
  41. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
  42. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
  43. package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
  44. package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
  45. package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
  46. package/skills/insurance-sales-pipeline/skill.json +12 -0
  47. package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
  48. package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
  49. package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
  50. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
  51. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
  52. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
  53. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
  54. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
  55. package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
  56. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
  57. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
  58. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
  59. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
  60. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
  61. package/skills/insurance-schedule-renewal/skill.json +12 -0
  62. package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
  63. package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
  64. package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
  65. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
  66. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
  67. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
  68. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
  69. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
  70. package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
  71. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
  72. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
  73. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
  74. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
  75. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
  76. package/skills/insurance-shared-data/skill.json +20 -0
  77. package/skills/insurance-shared-data/src/SKILL.md +33 -0
  78. package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
@@ -0,0 +1,180 @@
1
+ """Shared SQLite schema for insurance skills."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sqlite3
6
+ from pathlib import Path
7
+
8
+
9
+ def default_db_path() -> Path:
10
+ base = Path.home() / ".config" / "insurance"
11
+ base.mkdir(parents=True, exist_ok=True)
12
+ return base / "insurance.sqlite3"
13
+
14
+
15
+ def get_db_path() -> Path:
16
+ for key in ("INSURANCE_DB_PATH", "INSURANCE_SCHEDULE_DB_PATH"):
17
+ p = os.environ.get(key)
18
+ if p:
19
+ path = Path(p)
20
+ path.parent.mkdir(parents=True, exist_ok=True)
21
+ return path
22
+ return default_db_path()
23
+
24
+
25
+ def connect() -> sqlite3.Connection:
26
+ conn = sqlite3.connect(str(get_db_path()))
27
+ conn.row_factory = sqlite3.Row
28
+ conn.execute("PRAGMA foreign_keys=ON")
29
+ init_schema(conn)
30
+ return conn
31
+
32
+
33
+ def init_schema(conn: sqlite3.Connection) -> None:
34
+ conn.executescript(
35
+ """
36
+ CREATE TABLE IF NOT EXISTS customers (
37
+ id TEXT PRIMARY KEY,
38
+ name TEXT NOT NULL,
39
+ phone TEXT,
40
+ age INTEGER,
41
+ gender TEXT,
42
+ income REAL,
43
+ household_json TEXT NOT NULL DEFAULT '{}',
44
+ occupation TEXT,
45
+ birthday TEXT,
46
+ tags_json TEXT NOT NULL DEFAULT '[]',
47
+ fields_json TEXT NOT NULL DEFAULT '{}',
48
+ status TEXT NOT NULL DEFAULT 'active',
49
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
50
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
51
+ );
52
+ CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone);
53
+ CREATE INDEX IF NOT EXISTS idx_customers_birthday ON customers(birthday);
54
+
55
+ CREATE TABLE IF NOT EXISTS policies (
56
+ id TEXT PRIMARY KEY,
57
+ customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
58
+ company TEXT NOT NULL,
59
+ product_name TEXT NOT NULL,
60
+ product_type TEXT NOT NULL,
61
+ coverage REAL NOT NULL DEFAULT 0,
62
+ premium REAL NOT NULL DEFAULT 0,
63
+ pay_period TEXT,
64
+ effective_date TEXT NOT NULL,
65
+ expire_date TEXT,
66
+ next_pay_date TEXT,
67
+ insured_name TEXT,
68
+ beneficiary TEXT,
69
+ notes TEXT,
70
+ status TEXT NOT NULL DEFAULT 'active',
71
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
72
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
73
+ );
74
+ CREATE INDEX IF NOT EXISTS idx_policies_customer ON policies(customer_id, status);
75
+ CREATE INDEX IF NOT EXISTS idx_policies_expire ON policies(expire_date, status);
76
+ CREATE INDEX IF NOT EXISTS idx_policies_next_pay ON policies(next_pay_date, status);
77
+ CREATE INDEX IF NOT EXISTS idx_policies_type ON policies(product_type, status);
78
+
79
+ CREATE TABLE IF NOT EXISTS followups (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
82
+ content TEXT NOT NULL,
83
+ next_step TEXT,
84
+ next_date TEXT,
85
+ channel TEXT,
86
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
87
+ );
88
+ CREATE INDEX IF NOT EXISTS idx_followups_customer ON followups(customer_id, created_at DESC);
89
+
90
+ CREATE TABLE IF NOT EXISTS customer_fields (
91
+ name TEXT PRIMARY KEY,
92
+ type TEXT NOT NULL DEFAULT 'text'
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS products (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ name TEXT NOT NULL UNIQUE,
98
+ company TEXT NOT NULL,
99
+ product_type TEXT NOT NULL,
100
+ coverage_min REAL,
101
+ coverage_max REAL,
102
+ premium_min REAL,
103
+ premium_max REAL,
104
+ age_min INTEGER,
105
+ age_max INTEGER,
106
+ waiting_period TEXT,
107
+ deductible TEXT,
108
+ renewal_terms TEXT,
109
+ highlights_json TEXT NOT NULL DEFAULT '[]',
110
+ target_audience TEXT,
111
+ file_path TEXT,
112
+ raw_text TEXT,
113
+ status TEXT NOT NULL DEFAULT 'active',
114
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
115
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
116
+ );
117
+ CREATE INDEX IF NOT EXISTS idx_products_type ON products(product_type, status);
118
+ CREATE INDEX IF NOT EXISTS idx_products_company ON products(company, status);
119
+
120
+ CREATE TABLE IF NOT EXISTS product_analysis_cache (
121
+ product_id INTEGER PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
122
+ key_terms_json TEXT,
123
+ selling_points_json TEXT,
124
+ fit_audience_json TEXT,
125
+ pitch_script TEXT,
126
+ analyzed_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
127
+ );
128
+
129
+ CREATE TABLE IF NOT EXISTS deals (
130
+ id TEXT PRIMARY KEY,
131
+ customer_id TEXT NOT NULL,
132
+ customer_name TEXT,
133
+ product_name TEXT,
134
+ product_type TEXT,
135
+ stage TEXT NOT NULL,
136
+ estimated_premium REAL NOT NULL DEFAULT 0,
137
+ actual_premium REAL,
138
+ expected_close_date TEXT,
139
+ closed_at TEXT,
140
+ lost_reason TEXT,
141
+ lost_note TEXT,
142
+ note TEXT,
143
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
144
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
145
+ );
146
+ CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage, expected_close_date);
147
+ CREATE INDEX IF NOT EXISTS idx_deals_customer ON deals(customer_id);
148
+
149
+ CREATE TABLE IF NOT EXISTS deal_stage_log (
150
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
151
+ deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
152
+ from_stage TEXT,
153
+ to_stage TEXT NOT NULL,
154
+ note TEXT,
155
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
156
+ );
157
+ CREATE INDEX IF NOT EXISTS idx_stage_log_deal ON deal_stage_log(deal_id, created_at);
158
+
159
+ CREATE TABLE IF NOT EXISTS monthly_targets (
160
+ month TEXT PRIMARY KEY,
161
+ target_premium REAL NOT NULL,
162
+ note TEXT,
163
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
164
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
165
+ );
166
+
167
+ CREATE TABLE IF NOT EXISTS sent_reminders (
168
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
169
+ reminder_type TEXT NOT NULL,
170
+ target_id TEXT NOT NULL,
171
+ bucket TEXT NOT NULL,
172
+ sent_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
173
+ status TEXT NOT NULL DEFAULT 'sent',
174
+ note TEXT,
175
+ UNIQUE (reminder_type, target_id, bucket)
176
+ );
177
+ CREATE INDEX IF NOT EXISTS idx_sent_target ON sent_reminders(target_id, reminder_type);
178
+ """
179
+ )
180
+ conn.commit()
@@ -0,0 +1,94 @@
1
+ """Shared-db read helpers for insurance-schedule-renewal."""
2
+ from __future__ import annotations
3
+
4
+ from insurance_schedule_cli import db
5
+
6
+
7
+ def policy_query(expire_before: str | None = None,
8
+ next_pay_before: str | None = None,
9
+ customer_id: str | None = None) -> dict:
10
+ conn = db.connect()
11
+ try:
12
+ sql = "SELECT * FROM policies WHERE status='active'"
13
+ vals: list[str] = []
14
+ if customer_id:
15
+ sql += " AND customer_id=?"
16
+ vals.append(customer_id)
17
+ if expire_before:
18
+ sql += " AND expire_date<>'' AND expire_date IS NOT NULL AND expire_date<=?"
19
+ vals.append(expire_before)
20
+ if next_pay_before:
21
+ sql += " AND next_pay_date<>'' AND next_pay_date IS NOT NULL AND next_pay_date<=?"
22
+ vals.append(next_pay_before)
23
+ sql += " ORDER BY effective_date DESC"
24
+ rows = list(conn.execute(sql, tuple(vals)))
25
+ return {"ok": True, "policies": [dict(r) for r in rows], "count": len(rows)}
26
+ finally:
27
+ conn.close()
28
+
29
+
30
+ def customer_query(customer_id: str | None = None) -> dict:
31
+ conn = db.connect()
32
+ try:
33
+ if customer_id:
34
+ rows = list(
35
+ conn.execute("SELECT * FROM customers WHERE id=? AND status='active'", (customer_id,))
36
+ )
37
+ else:
38
+ rows = list(conn.execute("SELECT * FROM customers WHERE status='active' ORDER BY created_at DESC"))
39
+ return {"ok": True, "customers": [dict(r) for r in rows], "count": len(rows)}
40
+ finally:
41
+ conn.close()
42
+
43
+
44
+ def followup_query(customer_id: str | None = None, days: int | None = None) -> dict:
45
+ conn = db.connect()
46
+ try:
47
+ if customer_id:
48
+ rows = list(
49
+ conn.execute(
50
+ "SELECT * FROM followups WHERE customer_id=? ORDER BY created_at DESC",
51
+ (customer_id,),
52
+ )
53
+ )
54
+ elif days is not None:
55
+ rows = list(
56
+ conn.execute(
57
+ "SELECT * FROM followups WHERE created_at >= datetime('now', ?) ORDER BY created_at DESC",
58
+ (f"-{int(days)} days",),
59
+ )
60
+ )
61
+ else:
62
+ rows = list(conn.execute("SELECT * FROM followups ORDER BY created_at DESC"))
63
+ return {"ok": True, "followups": [dict(r) for r in rows], "count": len(rows)}
64
+ finally:
65
+ conn.close()
66
+
67
+
68
+ def gap_analysis(customer_id: str) -> dict:
69
+ conn = db.connect()
70
+ try:
71
+ row = conn.execute(
72
+ "SELECT id, name FROM customers WHERE id=? AND status='active'",
73
+ (customer_id,),
74
+ ).fetchone()
75
+ if not row:
76
+ return {"ok": False, "error": "customer_not_found"}
77
+ need_types = ["重疾险", "医疗险", "意外险", "寿险", "养老险", "教育金"]
78
+ owned = {
79
+ r["product_type"]
80
+ for r in conn.execute(
81
+ "SELECT DISTINCT product_type FROM policies WHERE customer_id=? AND status='active'",
82
+ (customer_id,),
83
+ )
84
+ if r["product_type"]
85
+ }
86
+ result = {
87
+ "customer_id": row["id"],
88
+ "name": row["name"],
89
+ "owned_types": sorted(owned),
90
+ "gaps": [t for t in need_types if t not in owned],
91
+ }
92
+ return {"ok": True, "result": result}
93
+ finally:
94
+ conn.close()
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """E2E smoke for insurance-schedule-renewal on shared insurance DB."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+
14
+ HERE = Path(__file__).resolve().parent
15
+
16
+
17
+ def _discover_skills_root() -> Path:
18
+ env = os.environ.get("INSURANCE_SKILLS_ROOT") or os.environ.get("SKILLS_ROOT")
19
+ if env:
20
+ p = Path(env).expanduser().resolve()
21
+ if p.is_dir():
22
+ return p
23
+ candidates = [HERE.parents[2], HERE.parents[1], Path("/app/skills"), Path.cwd() / "skills"]
24
+ for c in candidates:
25
+ try:
26
+ if (c / "insurance-customer-policy").is_dir():
27
+ return c.resolve()
28
+ except OSError:
29
+ continue
30
+ raise SystemExit("[FAIL] cannot discover skills root for smoke test")
31
+
32
+
33
+ def _resolve_customer_pkg(skills_root: Path) -> Path:
34
+ candidates = [
35
+ skills_root / "insurance-customer-policy" / "scripts",
36
+ skills_root / "insurance-customer-policy" / "src" / "scripts",
37
+ skills_root / "insurance-customer-policy" / "src" / "src",
38
+ ]
39
+ for p in candidates:
40
+ try:
41
+ if (p / "insurance_customer_cli" / "__init__.py").is_file():
42
+ return p
43
+ except OSError:
44
+ continue
45
+ raise SystemExit("[FAIL] cannot locate insurance_customer_cli package for smoke test")
46
+
47
+
48
+ SKILLS_ROOT = _discover_skills_root()
49
+ CUSTOMER_PKG = _resolve_customer_pkg(SKILLS_ROOT)
50
+
51
+
52
+ def _resolve_cli(skill_folder: str) -> Path:
53
+ candidates = [
54
+ SKILLS_ROOT / skill_folder / "scripts" / "cli.py",
55
+ SKILLS_ROOT / skill_folder / "src" / "scripts" / "cli.py",
56
+ ]
57
+ for p in candidates:
58
+ if p.is_file():
59
+ return p
60
+ raise SystemExit(f"[FAIL] cannot locate cli.py for {skill_folder}")
61
+
62
+
63
+ SCHEDULE_CLI = _resolve_cli("insurance-schedule-renewal")
64
+ CUSTOMER_CLI = _resolve_cli("insurance-customer-policy")
65
+
66
+
67
+ def _run_json(cmd: list[str], env: dict[str, str], label: str, timeout: int = 120) -> dict:
68
+ r = subprocess.run(cmd, env=env, capture_output=True, text=True, encoding="utf-8", timeout=timeout)
69
+ if r.returncode not in (0, 1):
70
+ raise SystemExit(f"{label} rc={r.returncode}\nCMD: {' '.join(cmd)}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}")
71
+ out = (r.stdout or "").strip()
72
+ if not out:
73
+ raise SystemExit(f"{label} empty stdout\nCMD: {' '.join(cmd)}\nrc={r.returncode}\nSTDERR:\n{r.stderr}")
74
+ try:
75
+ return json.loads(out)
76
+ except json.JSONDecodeError as e:
77
+ raise SystemExit(f"{label} bad json: {e}\nCMD: {' '.join(cmd)}\nSTDOUT:\n{out}\nSTDERR:\n{r.stderr}")
78
+
79
+
80
+ def run_schedule(*args: str) -> dict:
81
+ cmd = [sys.executable, str(SCHEDULE_CLI), *args]
82
+ env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
83
+ return _run_json(cmd, env, "schedule", timeout=120)
84
+
85
+
86
+ def run_customer(*args: str) -> dict:
87
+ cmd = [sys.executable, str(CUSTOMER_CLI), *args]
88
+ env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
89
+ return _run_json(cmd, env, "customer", timeout=60)
90
+
91
+
92
+ def assert_ok(d: dict, *args: str) -> dict:
93
+ if not d.get("ok"):
94
+ raise SystemExit(f"expected ok=True got {d} args={args}")
95
+ return d
96
+
97
+
98
+ def main() -> int:
99
+ tmp = tempfile.mkdtemp(prefix="ins_sched_smoke_")
100
+ shared = str(Path(tmp) / "insurance.sqlite3")
101
+ os.environ["INSURANCE_DB_PATH"] = shared
102
+ os.environ["INSURANCE_SCHEDULE_DB_PATH"] = shared
103
+ os.environ["INSURANCE_CUSTOMER_DB_PATH"] = shared
104
+
105
+ today = datetime.now()
106
+ in_15 = (today + timedelta(days=15)).strftime("%Y-%m-%d")
107
+ in_45 = (today + timedelta(days=45)).strftime("%Y-%m-%d")
108
+ long_ago = (today - timedelta(days=400)).strftime("%Y-%m-%d")
109
+
110
+ print("[setup] customers")
111
+ assert_ok(run_customer("customer-add", "--name", "张先生", "--phone", "13800138000",
112
+ "--age", "35", "--income", "300000",
113
+ "--household", '{"spouse":true,"children":2}',
114
+ "--birthday", f"1989-{today.month:02d}-09"))
115
+ assert_ok(run_customer("customer-add", "--name", "李女士", "--phone", "13900139000",
116
+ "--age", "40", "--birthday",
117
+ f"1985-{(today.month % 12) + 1:02d}-15"))
118
+ assert_ok(run_customer("customer-add", "--name", "王先生", "--phone", "13700137000", "--age", "50"))
119
+
120
+ print("[setup] policies")
121
+ p1 = assert_ok(run_customer("policy-add", "--customer-id", "C001",
122
+ "--company", "A保险", "--product", "重疾险XX",
123
+ "--type", "重疾险", "--coverage", "500000", "--premium", "8000",
124
+ "--effective-date", long_ago,
125
+ "--expire-date", in_15,
126
+ "--next-pay-date", in_15))
127
+ assert p1["id"] == "P001"
128
+ eff_recent = (today - timedelta(days=20)).strftime("%Y-%m-%d") # 周年月份
129
+ eff_recent = f"{today.year - 2}-{today.month:02d}-15"
130
+ assert_ok(run_customer("policy-add", "--customer-id", "C002",
131
+ "--company", "B保险", "--product", "百万医疗",
132
+ "--type", "医疗险", "--coverage", "1000000", "--premium", "500",
133
+ "--effective-date", eff_recent,
134
+ "--expire-date", in_45))
135
+ assert_ok(run_customer("policy-add", "--customer-id", "C003",
136
+ "--company", "C保险", "--product", "意外险",
137
+ "--type", "意外险", "--coverage", "500000", "--premium", "300",
138
+ "--effective-date", "2020-01-01"))
139
+
140
+ print("[setup] followups (C001 has recent, C002/C003 backdated to >30d/>60d)")
141
+ assert_ok(run_customer("followup-add", "--customer-id", "C001", "--content", "刚刚跟进过"))
142
+ # Backdate customer created_at so 'lapsed' logic kicks in.
143
+ conn = sqlite3.connect(os.environ["INSURANCE_DB_PATH"])
144
+ backdated_30 = (today - timedelta(days=45)).strftime("%Y-%m-%d %H:%M:%S")
145
+ backdated_60 = (today - timedelta(days=80)).strftime("%Y-%m-%d %H:%M:%S")
146
+ conn.execute("UPDATE customers SET created_at=? WHERE id='C002'", (backdated_30,))
147
+ conn.execute("UPDATE customers SET created_at=? WHERE id='C003'", (backdated_60,))
148
+ conn.commit()
149
+ conn.close()
150
+
151
+ print("[1] renewal-query days=30 (P001 expire in 15d)")
152
+ d = assert_ok(run_schedule("renewal-query", "--days", "30"))
153
+ assert any(it["policy_id"] == "P001" for it in d["items"]), d
154
+ assert d["items"][0]["customer_name"] == "张先生", d
155
+ assert d["count"] >= 1
156
+
157
+ print("[2] renewal-query days=60 (catches P002 too)")
158
+ d = assert_ok(run_schedule("renewal-query", "--days", "60"))
159
+ assert any(it["policy_id"] == "P002" for it in d["items"]), d
160
+
161
+ print("[3] renewal-remind --mark-sent (first run -> new)")
162
+ d = assert_ok(run_schedule("renewal-remind", "--days", "30", "--mark-sent"))
163
+ assert d["new_count"] >= 1
164
+ assert d["skipped_count"] == 0
165
+
166
+ print("[4] renewal-remind same bucket (second run -> already_sent)")
167
+ d = assert_ok(run_schedule("renewal-remind", "--days", "30", "--mark-sent"))
168
+ assert d["new_count"] == 0
169
+ assert d["skipped_count"] >= 1
170
+
171
+ print("[5] followup-check days=30 (C002, C003 lapsed; C001 recent)")
172
+ d = assert_ok(run_schedule("followup-check", "--days", "30"))
173
+ cids = [c["customer_id"] for c in d["items"]]
174
+ assert "C001" not in cids
175
+ assert "C002" in cids and "C003" in cids
176
+
177
+ print("[6] followup-remind dedup")
178
+ a = assert_ok(run_schedule("followup-remind", "--days", "30", "--mark-sent"))
179
+ assert a["new_count"] >= 2 and a["skipped_count"] == 0
180
+ b = assert_ok(run_schedule("followup-remind", "--days", "30", "--mark-sent"))
181
+ assert b["new_count"] == 0 and b["skipped_count"] >= 2
182
+
183
+ print("[7] dormant-query days=60 (only C003 lapsed >60d)")
184
+ d = assert_ok(run_schedule("dormant-query", "--days", "60"))
185
+ cids = [c["customer_id"] for c in d["items"]]
186
+ assert "C003" in cids and "C001" not in cids, d
187
+
188
+ print("[8] dormant-wake (script generated)")
189
+ d = assert_ok(run_schedule("dormant-wake", "--days", "60"))
190
+ angles = {it["angle"] for it in d["items"]}
191
+ assert angles, d
192
+ assert all(it["script"] for it in d["items"]), d
193
+
194
+ print("[9] special-date-query birthday (current month)")
195
+ d = assert_ok(run_schedule("special-date-query", "--type", "birthday",
196
+ "--month", str(today.month)))
197
+ assert any(it["customer_id"] == "C001" for it in d["items"]), d
198
+
199
+ print("[10] special-date-query anniversary (current month)")
200
+ d = assert_ok(run_schedule("special-date-query", "--type", "anniversary",
201
+ "--month", str(today.month)))
202
+ assert any(it["policy_id"] == "P002" for it in d["items"]), d
203
+
204
+ print("[11] daily-summary")
205
+ d = assert_ok(run_schedule("daily-summary"))
206
+ assert d["summary"]["renewal_30d"] >= 1
207
+ assert "续保提醒" in d["markdown"]
208
+
209
+ print("[12] dashboard")
210
+ d = assert_ok(run_schedule("dashboard", "--json"))
211
+ assert d["skill"] == "insurance-schedule-renewal"
212
+
213
+ print("ALL OK")
214
+ return 0
215
+
216
+
217
+ if __name__ == "__main__":
218
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from insurance_schedule_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())