sophhub 0.4.21 → 0.4.22
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/package.json +1 -1
- package/skills/insurance-customer-policy/skill.json +12 -0
- package/skills/insurance-customer-policy/src/SKILL.md +121 -0
- package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
- package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
- package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
- package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
- package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
- package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
- package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
- package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
- package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
- package/skills/insurance-product-analysis/skill.json +12 -0
- package/skills/insurance-product-analysis/src/SKILL.md +99 -0
- package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
- package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
- package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
- package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
- package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
- package/skills/insurance-sales-pipeline/skill.json +12 -0
- package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
- package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
- package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
- package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
- package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
- package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
- package/skills/insurance-schedule-renewal/skill.json +12 -0
- package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
- package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
- package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
- package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
- package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
- package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
- package/skills/insurance-shared-data/skill.json +20 -0
- package/skills/insurance-shared-data/src/SKILL.md +33 -0
- package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
"""Insurance customer & policy CLI.
|
|
2
|
+
|
|
3
|
+
Subcommands cover customer / policy / followup CRUD, gap-analysis,
|
|
4
|
+
customer-segment, dashboard, and internal helpers used by sibling skills.
|
|
5
|
+
All commands print JSON to stdout: {"ok": true|false, ...}.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sqlite3
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from insurance_customer_cli import db
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
21
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
22
|
+
except (AttributeError, OSError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
DATE_FMT = "%Y-%m-%d"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def print_json(obj: Any) -> None:
|
|
29
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def next_id(conn: sqlite3.Connection, prefix: str, table: str) -> str:
|
|
33
|
+
rows = conn.execute(
|
|
34
|
+
f"SELECT id FROM {table} WHERE id GLOB ?", (f"{prefix}[0-9]*",)
|
|
35
|
+
).fetchall()
|
|
36
|
+
max_n = 0
|
|
37
|
+
for r in rows:
|
|
38
|
+
m = re.match(rf"^{prefix}(\d+)$", r["id"])
|
|
39
|
+
if m:
|
|
40
|
+
max_n = max(max_n, int(m.group(1)))
|
|
41
|
+
return f"{prefix}{max_n + 1:03d}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_date(s: str | None) -> datetime | None:
|
|
45
|
+
if not s:
|
|
46
|
+
return None
|
|
47
|
+
return datetime.strptime(s.strip()[:10], DATE_FMT)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_years(d: datetime, years: int) -> datetime:
|
|
51
|
+
try:
|
|
52
|
+
return d.replace(year=d.year + years)
|
|
53
|
+
except ValueError:
|
|
54
|
+
return d.replace(year=d.year + years, day=28)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_months(d: datetime, months: int) -> datetime:
|
|
58
|
+
m = d.month - 1 + months
|
|
59
|
+
y = d.year + m // 12
|
|
60
|
+
nm = m % 12 + 1
|
|
61
|
+
day = min(d.day, [31, 29 if y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) else 28,
|
|
62
|
+
31, 30, 31, 30, 31, 31, 30, 31, 30, 31][nm - 1])
|
|
63
|
+
return d.replace(year=y, month=nm, day=day)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def derive_dates(effective_date: str, pay_period: str | None) -> tuple[str | None, str | None]:
|
|
67
|
+
"""Compute (expire_date, next_pay_date) from effective_date + pay_period.
|
|
68
|
+
|
|
69
|
+
Heuristics:
|
|
70
|
+
- "趸交" / "1年" : next_pay_date = effective + 1y, expire = None (开放期/终身保单常见)
|
|
71
|
+
- "N年缴" / "N年" : next_pay_date = effective + 1y; expire = effective + Ny
|
|
72
|
+
- 其他 / 留空 : 保守地把 next_pay_date 设为 effective + 1y。
|
|
73
|
+
"""
|
|
74
|
+
eff = parse_date(effective_date)
|
|
75
|
+
if eff is None:
|
|
76
|
+
return None, None
|
|
77
|
+
next_pay = add_years(eff, 1).strftime(DATE_FMT)
|
|
78
|
+
expire: str | None = None
|
|
79
|
+
if pay_period:
|
|
80
|
+
m = re.match(r"^\s*(\d+)\s*年", pay_period)
|
|
81
|
+
if m:
|
|
82
|
+
expire = add_years(eff, int(m.group(1))).strftime(DATE_FMT)
|
|
83
|
+
elif "趸" in pay_period:
|
|
84
|
+
expire = None
|
|
85
|
+
return expire, next_pay
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------- customer commands ----------
|
|
89
|
+
|
|
90
|
+
def cmd_customer_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
91
|
+
cid = args.customer_id or next_id(conn, "C", "customers")
|
|
92
|
+
household = args.household or "{}"
|
|
93
|
+
tags = args.tags or "[]"
|
|
94
|
+
fields = args.fields or "{}"
|
|
95
|
+
try:
|
|
96
|
+
json.loads(household)
|
|
97
|
+
json.loads(tags)
|
|
98
|
+
json.loads(fields)
|
|
99
|
+
except json.JSONDecodeError as e:
|
|
100
|
+
print_json({"ok": False, "error": "bad_json", "hint": str(e)})
|
|
101
|
+
return 1
|
|
102
|
+
conn.execute(
|
|
103
|
+
"""INSERT INTO customers
|
|
104
|
+
(id, name, phone, age, gender, income, household_json, occupation,
|
|
105
|
+
birthday, tags_json, fields_json)
|
|
106
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
|
|
107
|
+
(
|
|
108
|
+
cid,
|
|
109
|
+
args.name,
|
|
110
|
+
args.phone or "",
|
|
111
|
+
args.age,
|
|
112
|
+
args.gender or "",
|
|
113
|
+
args.income,
|
|
114
|
+
household,
|
|
115
|
+
args.occupation or "",
|
|
116
|
+
args.birthday or "",
|
|
117
|
+
tags,
|
|
118
|
+
fields,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
conn.commit()
|
|
122
|
+
print_json({"ok": True, "id": cid, "name": args.name})
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _row_customer(row: sqlite3.Row, conn: sqlite3.Connection | None = None) -> dict:
|
|
127
|
+
d = dict(row)
|
|
128
|
+
for jk in ("household_json", "tags_json", "fields_json"):
|
|
129
|
+
if jk in d and isinstance(d[jk], str):
|
|
130
|
+
try:
|
|
131
|
+
d[jk[: -len("_json")]] = json.loads(d[jk])
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
d[jk[: -len("_json")]] = None
|
|
134
|
+
if conn is not None:
|
|
135
|
+
cnt = conn.execute(
|
|
136
|
+
"SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
|
|
137
|
+
(d["id"],),
|
|
138
|
+
).fetchone()
|
|
139
|
+
d["policy_count"] = int(cnt["n"]) if cnt else 0
|
|
140
|
+
last = conn.execute(
|
|
141
|
+
"SELECT created_at FROM followups WHERE customer_id=? ORDER BY created_at DESC LIMIT 1",
|
|
142
|
+
(d["id"],),
|
|
143
|
+
).fetchone()
|
|
144
|
+
d["last_followup_at"] = last["created_at"] if last else None
|
|
145
|
+
return d
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_customer_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
149
|
+
cur = conn.cursor()
|
|
150
|
+
rows: list[sqlite3.Row]
|
|
151
|
+
if args.customer_id:
|
|
152
|
+
rows = list(
|
|
153
|
+
cur.execute(
|
|
154
|
+
"SELECT * FROM customers WHERE id=? AND status='active'",
|
|
155
|
+
(args.customer_id,),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
elif args.keyword:
|
|
159
|
+
kw = f"%{args.keyword}%"
|
|
160
|
+
rows = list(
|
|
161
|
+
cur.execute(
|
|
162
|
+
"SELECT * FROM customers WHERE status='active' AND (name LIKE ? OR phone LIKE ?) ORDER BY created_at DESC",
|
|
163
|
+
(kw, kw),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
rows = list(cur.execute("SELECT * FROM customers WHERE status='active' ORDER BY created_at DESC"))
|
|
168
|
+
out = [_row_customer(r, conn) for r in rows]
|
|
169
|
+
print_json({"ok": True, "customers": out, "count": len(out)})
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_CUSTOMER_UPDATE_FIELDS = {
|
|
174
|
+
"name": "name",
|
|
175
|
+
"phone": "phone",
|
|
176
|
+
"age": "age",
|
|
177
|
+
"gender": "gender",
|
|
178
|
+
"income": "income",
|
|
179
|
+
"occupation": "occupation",
|
|
180
|
+
"birthday": "birthday",
|
|
181
|
+
"household": "household_json",
|
|
182
|
+
"tags": "tags_json",
|
|
183
|
+
"fields": "fields_json",
|
|
184
|
+
"children": None,
|
|
185
|
+
"spouse": None,
|
|
186
|
+
"elderly": None,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_customer_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
191
|
+
try:
|
|
192
|
+
payload = json.loads(args.set)
|
|
193
|
+
except json.JSONDecodeError as e:
|
|
194
|
+
print_json({"ok": False, "error": "bad_json", "hint": str(e)})
|
|
195
|
+
return 1
|
|
196
|
+
row = conn.execute(
|
|
197
|
+
"SELECT household_json FROM customers WHERE id=? AND status='active'",
|
|
198
|
+
(args.customer_id,),
|
|
199
|
+
).fetchone()
|
|
200
|
+
if not row:
|
|
201
|
+
print_json({"ok": False, "error": "customer_not_found"})
|
|
202
|
+
return 1
|
|
203
|
+
sets: list[str] = []
|
|
204
|
+
vals: list[Any] = []
|
|
205
|
+
household_now = json.loads(row["household_json"] or "{}")
|
|
206
|
+
household_dirty = False
|
|
207
|
+
for k, v in payload.items():
|
|
208
|
+
if k in ("children", "spouse", "elderly"):
|
|
209
|
+
household_now[k] = v
|
|
210
|
+
household_dirty = True
|
|
211
|
+
continue
|
|
212
|
+
col = _CUSTOMER_UPDATE_FIELDS.get(k)
|
|
213
|
+
if col is None:
|
|
214
|
+
continue
|
|
215
|
+
if col.endswith("_json"):
|
|
216
|
+
sets.append(f"{col}=?")
|
|
217
|
+
vals.append(json.dumps(v, ensure_ascii=False) if not isinstance(v, str) else v)
|
|
218
|
+
else:
|
|
219
|
+
sets.append(f"{col}=?")
|
|
220
|
+
vals.append(v)
|
|
221
|
+
if household_dirty:
|
|
222
|
+
sets.append("household_json=?")
|
|
223
|
+
vals.append(json.dumps(household_now, ensure_ascii=False))
|
|
224
|
+
if sets:
|
|
225
|
+
sets.append("updated_at=datetime('now','localtime')")
|
|
226
|
+
vals.append(args.customer_id)
|
|
227
|
+
conn.execute(f"UPDATE customers SET {', '.join(sets)} WHERE id=?", vals)
|
|
228
|
+
conn.commit()
|
|
229
|
+
print_json({"ok": True, "id": args.customer_id})
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cmd_customer_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
234
|
+
row = conn.execute(
|
|
235
|
+
"SELECT id FROM customers WHERE id=? AND status='active'",
|
|
236
|
+
(args.customer_id,),
|
|
237
|
+
).fetchone()
|
|
238
|
+
if not row:
|
|
239
|
+
print_json({"ok": False, "error": "customer_not_found"})
|
|
240
|
+
return 1
|
|
241
|
+
cnt = conn.execute(
|
|
242
|
+
"SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
|
|
243
|
+
(args.customer_id,),
|
|
244
|
+
).fetchone()
|
|
245
|
+
if int(cnt["n"]) > 0:
|
|
246
|
+
print_json({"ok": False, "error": "has_policies", "policy_count": int(cnt["n"])})
|
|
247
|
+
return 1
|
|
248
|
+
if not args.yes:
|
|
249
|
+
print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
|
|
250
|
+
return 1
|
|
251
|
+
conn.execute(
|
|
252
|
+
"UPDATE customers SET status='deleted', updated_at=datetime('now','localtime') WHERE id=?",
|
|
253
|
+
(args.customer_id,),
|
|
254
|
+
)
|
|
255
|
+
conn.commit()
|
|
256
|
+
print_json({"ok": True})
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def cmd_customer_exists(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
261
|
+
row = conn.execute(
|
|
262
|
+
"SELECT id FROM customers WHERE id=? AND status='active'",
|
|
263
|
+
(args.customer_id,),
|
|
264
|
+
).fetchone()
|
|
265
|
+
print_json({"ok": True, "exists": bool(row)})
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------- policy commands ----------
|
|
270
|
+
|
|
271
|
+
def cmd_policy_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
272
|
+
cust = conn.execute(
|
|
273
|
+
"SELECT id FROM customers WHERE id=? AND status='active'", (args.customer_id,)
|
|
274
|
+
).fetchone()
|
|
275
|
+
if not cust:
|
|
276
|
+
print_json({"ok": False, "error": "customer_not_found"})
|
|
277
|
+
return 1
|
|
278
|
+
pid = args.policy_id or next_id(conn, "P", "policies")
|
|
279
|
+
expire = args.expire_date
|
|
280
|
+
next_pay = args.next_pay_date
|
|
281
|
+
if not expire or not next_pay:
|
|
282
|
+
de, dn = derive_dates(args.effective_date, args.pay_period)
|
|
283
|
+
expire = expire or de
|
|
284
|
+
next_pay = next_pay or dn
|
|
285
|
+
conn.execute(
|
|
286
|
+
"""INSERT INTO policies
|
|
287
|
+
(id, customer_id, company, product_name, product_type, coverage, premium,
|
|
288
|
+
pay_period, effective_date, expire_date, next_pay_date,
|
|
289
|
+
insured_name, beneficiary, notes)
|
|
290
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
291
|
+
(
|
|
292
|
+
pid,
|
|
293
|
+
args.customer_id,
|
|
294
|
+
args.company,
|
|
295
|
+
args.product,
|
|
296
|
+
args.product_type,
|
|
297
|
+
float(args.coverage),
|
|
298
|
+
float(args.premium),
|
|
299
|
+
args.pay_period or "",
|
|
300
|
+
args.effective_date,
|
|
301
|
+
expire or "",
|
|
302
|
+
next_pay or "",
|
|
303
|
+
args.insured or "",
|
|
304
|
+
args.beneficiary or "",
|
|
305
|
+
args.notes or "",
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
conn.commit()
|
|
309
|
+
print_json(
|
|
310
|
+
{
|
|
311
|
+
"ok": True,
|
|
312
|
+
"id": pid,
|
|
313
|
+
"customer_id": args.customer_id,
|
|
314
|
+
"expire_date": expire,
|
|
315
|
+
"next_pay_date": next_pay,
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def cmd_policy_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
322
|
+
cur = conn.cursor()
|
|
323
|
+
rows: list[sqlite3.Row]
|
|
324
|
+
if args.policy_id:
|
|
325
|
+
rows = list(cur.execute("SELECT * FROM policies WHERE id=? AND status='active'", (args.policy_id,)))
|
|
326
|
+
elif args.customer_id:
|
|
327
|
+
rows = list(
|
|
328
|
+
cur.execute(
|
|
329
|
+
"SELECT * FROM policies WHERE customer_id=? AND status='active' ORDER BY effective_date DESC",
|
|
330
|
+
(args.customer_id,),
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
elif args.expire_before:
|
|
334
|
+
rows = list(
|
|
335
|
+
cur.execute(
|
|
336
|
+
"SELECT * FROM policies WHERE status='active' AND expire_date != '' AND expire_date <= ? ORDER BY expire_date",
|
|
337
|
+
(args.expire_before,),
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
elif args.next_pay_before:
|
|
341
|
+
rows = list(
|
|
342
|
+
cur.execute(
|
|
343
|
+
"SELECT * FROM policies WHERE status='active' AND next_pay_date != '' AND next_pay_date <= ? ORDER BY next_pay_date",
|
|
344
|
+
(args.next_pay_before,),
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
elif args.product_type:
|
|
348
|
+
rows = list(
|
|
349
|
+
cur.execute(
|
|
350
|
+
"SELECT * FROM policies WHERE status='active' AND product_type=? ORDER BY effective_date DESC",
|
|
351
|
+
(args.product_type,),
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
rows = list(cur.execute("SELECT * FROM policies WHERE status='active' ORDER BY effective_date DESC"))
|
|
356
|
+
print_json({"ok": True, "policies": [dict(r) for r in rows], "count": len(rows)})
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
_POLICY_UPDATE_FIELDS = {
|
|
361
|
+
"company": "company",
|
|
362
|
+
"product_name": "product_name",
|
|
363
|
+
"product_type": "product_type",
|
|
364
|
+
"coverage": "coverage",
|
|
365
|
+
"premium": "premium",
|
|
366
|
+
"pay_period": "pay_period",
|
|
367
|
+
"effective_date": "effective_date",
|
|
368
|
+
"expire_date": "expire_date",
|
|
369
|
+
"next_pay_date": "next_pay_date",
|
|
370
|
+
"insured_name": "insured_name",
|
|
371
|
+
"beneficiary": "beneficiary",
|
|
372
|
+
"notes": "notes",
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def cmd_policy_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
377
|
+
try:
|
|
378
|
+
payload = json.loads(args.set)
|
|
379
|
+
except json.JSONDecodeError as e:
|
|
380
|
+
print_json({"ok": False, "error": "bad_json", "hint": str(e)})
|
|
381
|
+
return 1
|
|
382
|
+
row = conn.execute("SELECT id FROM policies WHERE id=? AND status='active'", (args.policy_id,)).fetchone()
|
|
383
|
+
if not row:
|
|
384
|
+
print_json({"ok": False, "error": "policy_not_found"})
|
|
385
|
+
return 1
|
|
386
|
+
sets: list[str] = []
|
|
387
|
+
vals: list[Any] = []
|
|
388
|
+
for k, v in payload.items():
|
|
389
|
+
col = _POLICY_UPDATE_FIELDS.get(k)
|
|
390
|
+
if col is None:
|
|
391
|
+
continue
|
|
392
|
+
sets.append(f"{col}=?")
|
|
393
|
+
vals.append(v)
|
|
394
|
+
if sets:
|
|
395
|
+
sets.append("updated_at=datetime('now','localtime')")
|
|
396
|
+
vals.append(args.policy_id)
|
|
397
|
+
conn.execute(f"UPDATE policies SET {', '.join(sets)} WHERE id=?", vals)
|
|
398
|
+
conn.commit()
|
|
399
|
+
print_json({"ok": True, "id": args.policy_id})
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cmd_policy_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
404
|
+
row = conn.execute("SELECT id FROM policies WHERE id=? AND status='active'", (args.policy_id,)).fetchone()
|
|
405
|
+
if not row:
|
|
406
|
+
print_json({"ok": False, "error": "policy_not_found"})
|
|
407
|
+
return 1
|
|
408
|
+
if not args.yes:
|
|
409
|
+
print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
|
|
410
|
+
return 1
|
|
411
|
+
conn.execute(
|
|
412
|
+
"UPDATE policies SET status='cancelled', updated_at=datetime('now','localtime') WHERE id=?",
|
|
413
|
+
(args.policy_id,),
|
|
414
|
+
)
|
|
415
|
+
conn.commit()
|
|
416
|
+
print_json({"ok": True})
|
|
417
|
+
return 0
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cmd_policy_count_by_customer(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
421
|
+
row = conn.execute(
|
|
422
|
+
"SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
|
|
423
|
+
(args.customer_id,),
|
|
424
|
+
).fetchone()
|
|
425
|
+
print_json({"ok": True, "customer_id": args.customer_id, "count": int(row["n"])})
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ---------- followup commands ----------
|
|
430
|
+
|
|
431
|
+
def cmd_followup_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
432
|
+
cust = conn.execute(
|
|
433
|
+
"SELECT id FROM customers WHERE id=? AND status='active'", (args.customer_id,)
|
|
434
|
+
).fetchone()
|
|
435
|
+
if not cust:
|
|
436
|
+
print_json({"ok": False, "error": "customer_not_found"})
|
|
437
|
+
return 1
|
|
438
|
+
cur = conn.execute(
|
|
439
|
+
"""INSERT INTO followups (customer_id, content, next_step, next_date, channel)
|
|
440
|
+
VALUES (?,?,?,?,?)""",
|
|
441
|
+
(
|
|
442
|
+
args.customer_id,
|
|
443
|
+
args.content,
|
|
444
|
+
args.next_step or "",
|
|
445
|
+
args.next_date or "",
|
|
446
|
+
args.channel or "",
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
conn.commit()
|
|
450
|
+
print_json({"ok": True, "id": cur.lastrowid, "customer_id": args.customer_id})
|
|
451
|
+
return 0
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def cmd_followup_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
455
|
+
cur = conn.cursor()
|
|
456
|
+
rows: list[sqlite3.Row]
|
|
457
|
+
if args.customer_id:
|
|
458
|
+
rows = list(
|
|
459
|
+
cur.execute(
|
|
460
|
+
"SELECT * FROM followups WHERE customer_id=? ORDER BY created_at DESC",
|
|
461
|
+
(args.customer_id,),
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
elif args.days is not None:
|
|
465
|
+
threshold = (datetime.now() - timedelta(days=int(args.days))).strftime("%Y-%m-%d %H:%M:%S")
|
|
466
|
+
rows = list(
|
|
467
|
+
cur.execute(
|
|
468
|
+
"SELECT * FROM followups WHERE created_at >= ? ORDER BY created_at DESC",
|
|
469
|
+
(threshold,),
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
rows = list(cur.execute("SELECT * FROM followups ORDER BY created_at DESC LIMIT 100"))
|
|
474
|
+
print_json({"ok": True, "followups": [dict(r) for r in rows], "count": len(rows)})
|
|
475
|
+
return 0
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ---------- gap-analysis ----------
|
|
479
|
+
|
|
480
|
+
GAP_TYPES = ["重疾险", "医疗险", "意外险", "寿险", "养老险", "教育金"]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _gap_for_customer(conn: sqlite3.Connection, customer_id: str) -> dict:
|
|
484
|
+
cust = conn.execute(
|
|
485
|
+
"SELECT id, name, household_json, age, income FROM customers WHERE id=? AND status='active'",
|
|
486
|
+
(customer_id,),
|
|
487
|
+
).fetchone()
|
|
488
|
+
if not cust:
|
|
489
|
+
return {"customer_id": customer_id, "found": False}
|
|
490
|
+
rows = conn.execute(
|
|
491
|
+
"SELECT product_type FROM policies WHERE customer_id=? AND status='active'",
|
|
492
|
+
(customer_id,),
|
|
493
|
+
).fetchall()
|
|
494
|
+
have = {r["product_type"] for r in rows}
|
|
495
|
+
has_children = False
|
|
496
|
+
try:
|
|
497
|
+
hh = json.loads(cust["household_json"] or "{}")
|
|
498
|
+
kids = hh.get("children")
|
|
499
|
+
has_children = bool(kids) and (kids is True or (isinstance(kids, (int, float)) and kids > 0))
|
|
500
|
+
except json.JSONDecodeError:
|
|
501
|
+
pass
|
|
502
|
+
coverage = {}
|
|
503
|
+
gaps: list[str] = []
|
|
504
|
+
for t in GAP_TYPES:
|
|
505
|
+
present = t in have
|
|
506
|
+
coverage[t] = present
|
|
507
|
+
if not present:
|
|
508
|
+
if t == "教育金" and not has_children:
|
|
509
|
+
continue
|
|
510
|
+
gaps.append(t)
|
|
511
|
+
return {
|
|
512
|
+
"customer_id": cust["id"],
|
|
513
|
+
"name": cust["name"],
|
|
514
|
+
"found": True,
|
|
515
|
+
"coverage": coverage,
|
|
516
|
+
"gaps": gaps,
|
|
517
|
+
"has_children": has_children,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def cmd_gap_analysis(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
522
|
+
if args.customer_id:
|
|
523
|
+
result = _gap_for_customer(conn, args.customer_id)
|
|
524
|
+
if not result.get("found"):
|
|
525
|
+
print_json({"ok": False, "error": "customer_not_found"})
|
|
526
|
+
return 1
|
|
527
|
+
print_json({"ok": True, "result": result})
|
|
528
|
+
return 0
|
|
529
|
+
rows = conn.execute("SELECT id FROM customers WHERE status='active'").fetchall()
|
|
530
|
+
results = [_gap_for_customer(conn, r["id"]) for r in rows]
|
|
531
|
+
if args.gap_type:
|
|
532
|
+
results = [r for r in results if r.get("found") and args.gap_type in r["gaps"]]
|
|
533
|
+
print_json({"ok": True, "results": results, "count": len(results)})
|
|
534
|
+
return 0
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ---------- customer-segment ----------
|
|
538
|
+
|
|
539
|
+
def _premium_total(conn: sqlite3.Connection, customer_id: str) -> float:
|
|
540
|
+
row = conn.execute(
|
|
541
|
+
"SELECT COALESCE(SUM(premium),0) AS s FROM policies WHERE customer_id=? AND status='active'",
|
|
542
|
+
(customer_id,),
|
|
543
|
+
).fetchone()
|
|
544
|
+
return float(row["s"] or 0)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _last_followup_at(conn: sqlite3.Connection, customer_id: str) -> str | None:
|
|
548
|
+
row = conn.execute(
|
|
549
|
+
"SELECT created_at FROM followups WHERE customer_id=? ORDER BY created_at DESC LIMIT 1",
|
|
550
|
+
(customer_id,),
|
|
551
|
+
).fetchone()
|
|
552
|
+
return row["created_at"] if row else None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def cmd_customer_segment(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
556
|
+
now = datetime.now()
|
|
557
|
+
rows = conn.execute("SELECT id, name, created_at FROM customers WHERE status='active'").fetchall()
|
|
558
|
+
hi_value: list[dict] = []
|
|
559
|
+
active: list[dict] = []
|
|
560
|
+
dormant: list[dict] = []
|
|
561
|
+
new_clients: list[dict] = []
|
|
562
|
+
for r in rows:
|
|
563
|
+
cid = r["id"]
|
|
564
|
+
name = r["name"]
|
|
565
|
+
premium = _premium_total(conn, cid)
|
|
566
|
+
last = _last_followup_at(conn, cid)
|
|
567
|
+
last_dt = None
|
|
568
|
+
if last:
|
|
569
|
+
try:
|
|
570
|
+
last_dt = datetime.strptime(last[:19], "%Y-%m-%d %H:%M:%S")
|
|
571
|
+
except ValueError:
|
|
572
|
+
last_dt = None
|
|
573
|
+
days_since = (now - last_dt).days if last_dt else None
|
|
574
|
+
try:
|
|
575
|
+
created_dt = datetime.strptime(r["created_at"][:19], "%Y-%m-%d %H:%M:%S")
|
|
576
|
+
except (ValueError, TypeError):
|
|
577
|
+
created_dt = None
|
|
578
|
+
item = {
|
|
579
|
+
"customer_id": cid,
|
|
580
|
+
"name": name,
|
|
581
|
+
"premium_total": premium,
|
|
582
|
+
"last_followup_at": last,
|
|
583
|
+
"days_since_followup": days_since,
|
|
584
|
+
}
|
|
585
|
+
if premium > 50000:
|
|
586
|
+
hi_value.append(item)
|
|
587
|
+
if days_since is not None and days_since <= 30:
|
|
588
|
+
active.append(item)
|
|
589
|
+
if days_since is not None and days_since > 60:
|
|
590
|
+
dormant.append(item)
|
|
591
|
+
elif days_since is None and created_dt and (now - created_dt).days > 60:
|
|
592
|
+
dormant.append(item)
|
|
593
|
+
if created_dt and (now - created_dt).days <= 30:
|
|
594
|
+
new_clients.append(item)
|
|
595
|
+
print_json(
|
|
596
|
+
{
|
|
597
|
+
"ok": True,
|
|
598
|
+
"hi_value": hi_value,
|
|
599
|
+
"active": active,
|
|
600
|
+
"dormant": dormant,
|
|
601
|
+
"new": new_clients,
|
|
602
|
+
"summary": {
|
|
603
|
+
"total": len(rows),
|
|
604
|
+
"hi_value": len(hi_value),
|
|
605
|
+
"active": len(active),
|
|
606
|
+
"dormant": len(dormant),
|
|
607
|
+
"new": len(new_clients),
|
|
608
|
+
},
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
return 0
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ---------- dashboard ----------
|
|
615
|
+
|
|
616
|
+
def cmd_dashboard(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
617
|
+
cust_total = int(conn.execute("SELECT COUNT(*) AS n FROM customers WHERE status='active'").fetchone()["n"])
|
|
618
|
+
pol_row = conn.execute(
|
|
619
|
+
"SELECT COUNT(*) AS n, COALESCE(SUM(premium),0) AS s FROM policies WHERE status='active'"
|
|
620
|
+
).fetchone()
|
|
621
|
+
pol_total = int(pol_row["n"])
|
|
622
|
+
premium_total = float(pol_row["s"] or 0)
|
|
623
|
+
now = datetime.now()
|
|
624
|
+
seg_rows = conn.execute("SELECT id, created_at FROM customers WHERE status='active'").fetchall()
|
|
625
|
+
hi = active = dormant = new_n = 0
|
|
626
|
+
for r in seg_rows:
|
|
627
|
+
cid = r["id"]
|
|
628
|
+
premium = _premium_total(conn, cid)
|
|
629
|
+
last = _last_followup_at(conn, cid)
|
|
630
|
+
last_dt = None
|
|
631
|
+
if last:
|
|
632
|
+
try:
|
|
633
|
+
last_dt = datetime.strptime(last[:19], "%Y-%m-%d %H:%M:%S")
|
|
634
|
+
except ValueError:
|
|
635
|
+
last_dt = None
|
|
636
|
+
days_since = (now - last_dt).days if last_dt else None
|
|
637
|
+
try:
|
|
638
|
+
created_dt = datetime.strptime(r["created_at"][:19], "%Y-%m-%d %H:%M:%S")
|
|
639
|
+
except (ValueError, TypeError):
|
|
640
|
+
created_dt = None
|
|
641
|
+
if premium > 50000:
|
|
642
|
+
hi += 1
|
|
643
|
+
if days_since is not None and days_since <= 30:
|
|
644
|
+
active += 1
|
|
645
|
+
if (days_since is not None and days_since > 60) or (
|
|
646
|
+
days_since is None and created_dt and (now - created_dt).days > 60
|
|
647
|
+
):
|
|
648
|
+
dormant += 1
|
|
649
|
+
if created_dt and (now - created_dt).days <= 30:
|
|
650
|
+
new_n += 1
|
|
651
|
+
print_json(
|
|
652
|
+
{
|
|
653
|
+
"ok": True,
|
|
654
|
+
"skill": "insurance-customer-policy",
|
|
655
|
+
"customers_total": cust_total,
|
|
656
|
+
"hi_value": hi,
|
|
657
|
+
"active": active,
|
|
658
|
+
"dormant": dormant,
|
|
659
|
+
"new": new_n,
|
|
660
|
+
"policies_total": pol_total,
|
|
661
|
+
"total_premium": premium_total,
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
return 0
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
# ---------- custom field commands ----------
|
|
668
|
+
|
|
669
|
+
def cmd_customer_field_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
670
|
+
conn.execute(
|
|
671
|
+
"INSERT OR REPLACE INTO customer_fields (name, type) VALUES (?,?)",
|
|
672
|
+
(args.name, args.type or "text"),
|
|
673
|
+
)
|
|
674
|
+
conn.commit()
|
|
675
|
+
print_json({"ok": True, "name": args.name, "type": args.type or "text"})
|
|
676
|
+
return 0
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def cmd_customer_field_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
|
|
680
|
+
rows = conn.execute("SELECT name, type FROM customer_fields ORDER BY name").fetchall()
|
|
681
|
+
print_json({"ok": True, "fields": [dict(r) for r in rows]})
|
|
682
|
+
return 0
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# ---------- argparse ----------
|
|
686
|
+
|
|
687
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
688
|
+
p = argparse.ArgumentParser(prog="insurance_customer_cli")
|
|
689
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
690
|
+
|
|
691
|
+
a = sub.add_parser("customer-add")
|
|
692
|
+
a.add_argument("--customer-id")
|
|
693
|
+
a.add_argument("--name", required=True)
|
|
694
|
+
a.add_argument("--phone")
|
|
695
|
+
a.add_argument("--age", type=int)
|
|
696
|
+
a.add_argument("--gender")
|
|
697
|
+
a.add_argument("--income", type=float)
|
|
698
|
+
a.add_argument("--household")
|
|
699
|
+
a.add_argument("--occupation")
|
|
700
|
+
a.add_argument("--birthday")
|
|
701
|
+
a.add_argument("--tags")
|
|
702
|
+
a.add_argument("--fields")
|
|
703
|
+
a.set_defaults(func=cmd_customer_add)
|
|
704
|
+
|
|
705
|
+
a = sub.add_parser("customer-query")
|
|
706
|
+
a.add_argument("--customer-id")
|
|
707
|
+
a.add_argument("--keyword")
|
|
708
|
+
a.set_defaults(func=cmd_customer_query)
|
|
709
|
+
|
|
710
|
+
a = sub.add_parser("customer-update")
|
|
711
|
+
a.add_argument("--customer-id", required=True)
|
|
712
|
+
a.add_argument("--set", required=True)
|
|
713
|
+
a.set_defaults(func=cmd_customer_update)
|
|
714
|
+
|
|
715
|
+
a = sub.add_parser("customer-delete")
|
|
716
|
+
a.add_argument("--customer-id", required=True)
|
|
717
|
+
a.add_argument("--yes", action="store_true")
|
|
718
|
+
a.set_defaults(func=cmd_customer_delete)
|
|
719
|
+
|
|
720
|
+
a = sub.add_parser("customer-exists")
|
|
721
|
+
a.add_argument("--customer-id", required=True)
|
|
722
|
+
a.add_argument("--json", action="store_true")
|
|
723
|
+
a.set_defaults(func=cmd_customer_exists)
|
|
724
|
+
|
|
725
|
+
a = sub.add_parser("policy-add")
|
|
726
|
+
a.add_argument("--customer-id", required=True)
|
|
727
|
+
a.add_argument("--policy-id")
|
|
728
|
+
a.add_argument("--company", required=True)
|
|
729
|
+
a.add_argument("--product", required=True)
|
|
730
|
+
a.add_argument("--product-type", "--type", dest="product_type", required=True)
|
|
731
|
+
a.add_argument("--coverage", required=True)
|
|
732
|
+
a.add_argument("--premium", required=True)
|
|
733
|
+
a.add_argument("--pay-period")
|
|
734
|
+
a.add_argument("--effective-date", required=True)
|
|
735
|
+
a.add_argument("--expire-date")
|
|
736
|
+
a.add_argument("--next-pay-date")
|
|
737
|
+
a.add_argument("--insured")
|
|
738
|
+
a.add_argument("--beneficiary")
|
|
739
|
+
a.add_argument("--notes")
|
|
740
|
+
a.set_defaults(func=cmd_policy_add)
|
|
741
|
+
|
|
742
|
+
a = sub.add_parser("policy-query")
|
|
743
|
+
a.add_argument("--policy-id")
|
|
744
|
+
a.add_argument("--customer-id")
|
|
745
|
+
a.add_argument("--expire-before")
|
|
746
|
+
a.add_argument("--next-pay-before")
|
|
747
|
+
a.add_argument("--product-type")
|
|
748
|
+
a.set_defaults(func=cmd_policy_query)
|
|
749
|
+
|
|
750
|
+
a = sub.add_parser("policy-update")
|
|
751
|
+
a.add_argument("--policy-id", required=True)
|
|
752
|
+
a.add_argument("--set", required=True)
|
|
753
|
+
a.set_defaults(func=cmd_policy_update)
|
|
754
|
+
|
|
755
|
+
a = sub.add_parser("policy-delete")
|
|
756
|
+
a.add_argument("--policy-id", required=True)
|
|
757
|
+
a.add_argument("--yes", action="store_true")
|
|
758
|
+
a.set_defaults(func=cmd_policy_delete)
|
|
759
|
+
|
|
760
|
+
a = sub.add_parser("policy-count-by-customer")
|
|
761
|
+
a.add_argument("--customer-id", required=True)
|
|
762
|
+
a.add_argument("--json", action="store_true")
|
|
763
|
+
a.set_defaults(func=cmd_policy_count_by_customer)
|
|
764
|
+
|
|
765
|
+
a = sub.add_parser("followup-add")
|
|
766
|
+
a.add_argument("--customer-id", required=True)
|
|
767
|
+
a.add_argument("--content", required=True)
|
|
768
|
+
a.add_argument("--next-step")
|
|
769
|
+
a.add_argument("--next-date")
|
|
770
|
+
a.add_argument("--channel")
|
|
771
|
+
a.set_defaults(func=cmd_followup_add)
|
|
772
|
+
|
|
773
|
+
a = sub.add_parser("followup-query")
|
|
774
|
+
a.add_argument("--customer-id")
|
|
775
|
+
a.add_argument("--days", type=int)
|
|
776
|
+
a.set_defaults(func=cmd_followup_query)
|
|
777
|
+
|
|
778
|
+
a = sub.add_parser("gap-analysis")
|
|
779
|
+
a.add_argument("--customer-id")
|
|
780
|
+
a.add_argument("--all", action="store_true")
|
|
781
|
+
a.add_argument("--gap-type")
|
|
782
|
+
a.set_defaults(func=cmd_gap_analysis)
|
|
783
|
+
|
|
784
|
+
a = sub.add_parser("customer-segment")
|
|
785
|
+
a.set_defaults(func=cmd_customer_segment)
|
|
786
|
+
|
|
787
|
+
a = sub.add_parser("dashboard")
|
|
788
|
+
a.add_argument("--json", action="store_true")
|
|
789
|
+
a.set_defaults(func=cmd_dashboard)
|
|
790
|
+
|
|
791
|
+
a = sub.add_parser("customer-field-add")
|
|
792
|
+
a.add_argument("--name", required=True)
|
|
793
|
+
a.add_argument("--type")
|
|
794
|
+
a.set_defaults(func=cmd_customer_field_add)
|
|
795
|
+
|
|
796
|
+
a = sub.add_parser("customer-field-query")
|
|
797
|
+
a.set_defaults(func=cmd_customer_field_query)
|
|
798
|
+
|
|
799
|
+
return p
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def main(argv: list[str] | None = None) -> int:
|
|
803
|
+
parser = build_parser()
|
|
804
|
+
args = parser.parse_args(argv)
|
|
805
|
+
conn = db.connect()
|
|
806
|
+
try:
|
|
807
|
+
return int(args.func(conn, args) or 0)
|
|
808
|
+
except sqlite3.Error as e:
|
|
809
|
+
print_json({"ok": False, "error": "sqlite_error", "hint": str(e)})
|
|
810
|
+
return 1
|
|
811
|
+
finally:
|
|
812
|
+
conn.close()
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
if __name__ == "__main__":
|
|
816
|
+
raise SystemExit(main())
|