sophhub 0.4.45 → 0.4.47

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 (37) hide show
  1. package/package.json +1 -1
  2. package/skills/sophnet-age-appearance/skill.json +9 -2
  3. package/skills/sophnet-age-appearance/src/scripts/age_appearance.py +8 -10
  4. package/skills/sophnet-id-photo/skill.json +9 -2
  5. package/skills/sophnet-id-photo/src/scripts/id_photo.py +5 -7
  6. package/skills/sophnet-image-edit/skill.json +9 -2
  7. package/skills/sophnet-image-edit/src/scripts/edit_image.py +5 -7
  8. package/skills/sophnet-image-generate/skill.json +9 -2
  9. package/skills/sophnet-image-generate/src/scripts/generate_image.py +51 -1
  10. package/skills/sophnet-infinite-talk/skill.json +9 -2
  11. package/skills/sophnet-infinite-talk/src/scripts/gen.py +39 -0
  12. package/skills/sophnet-invoice-assistant/skill.json +21 -0
  13. package/skills/sophnet-invoice-assistant/src/SKILL.md +186 -0
  14. package/skills/sophnet-invoice-assistant/src/pyproject.toml +8 -0
  15. package/skills/sophnet-invoice-assistant/src/references/api-contract.md +73 -0
  16. package/skills/sophnet-invoice-assistant/src/references/conversation-examples.md +16 -0
  17. package/skills/sophnet-invoice-assistant/src/references/error-cases.md +16 -0
  18. package/skills/sophnet-invoice-assistant/src/scripts/__init__.py +1 -0
  19. package/skills/sophnet-invoice-assistant/src/scripts/auth_context.py +74 -0
  20. package/skills/sophnet-invoice-assistant/src/scripts/cli.py +136 -0
  21. package/skills/sophnet-invoice-assistant/src/scripts/download_flow.py +89 -0
  22. package/skills/sophnet-invoice-assistant/src/scripts/dto.py +46 -0
  23. package/skills/sophnet-invoice-assistant/src/scripts/invoice_client.py +164 -0
  24. package/skills/sophnet-invoice-assistant/src/scripts/output.py +63 -0
  25. package/skills/sophnet-invoice-assistant/src/scripts/prepare_flow.py +58 -0
  26. package/skills/sophnet-invoice-assistant/src/scripts/risk_rules.py +95 -0
  27. package/skills/sophnet-invoice-assistant/src/scripts/status_flow.py +69 -0
  28. package/skills/sophnet-invoice-assistant/src/scripts/submit_flow.py +472 -0
  29. package/skills/sophnet-invoice-assistant/src/scripts/template_flow.py +196 -0
  30. package/skills/sophnet-invoice-assistant/src/scripts/update_flow.py +223 -0
  31. package/skills/sophnet-sticker-edit/skill.json +9 -2
  32. package/skills/sophnet-sticker-edit/src/scripts/edit_sticker_image.py +5 -7
  33. package/skills/sophnet-video-generate/skill.json +9 -2
  34. package/skills/sophnet-video-generate/src/SKILL.md +21 -12
  35. package/skills/sophnet-video-generate/src/scripts/gen_video.py +10 -15
  36. package/skills/store-marketing/skill.json +9 -2
  37. package/skills/store-marketing/src/scripts/generate_poster.py +39 -1
@@ -0,0 +1,16 @@
1
+ # Error Cases
2
+
3
+ ## 登录态缺失
4
+
5
+ - `AUTH_CONTEXT_MISSING`
6
+ - 提示:当前 Sophnet 登录态缺失,请重新登录后再试。
7
+
8
+ ## 个人组织缺失
9
+
10
+ - `PERSONAL_ORG_NOT_FOUND`
11
+ - 提示:当前账号下未找到可用的个人组织,暂时无法代你提交发票申请。
12
+
13
+ ## 真实接口未接入
14
+
15
+ - `API_NOT_IMPLEMENTED`
16
+ - 提示:当前 skill 仅完成骨架搭建,尚未接入真实 Sophnet 发票接口。
@@ -0,0 +1 @@
1
+ """sophnet-invoice-assistant skill scripts package."""
@@ -0,0 +1,74 @@
1
+ """Read Sophnet auth context from runtime environment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from dto import AuthContext
10
+
11
+ DEFAULT_JWT_PATHS = (
12
+ "/home/node/.openclaw/jwt.json",
13
+ "~/.openclaw/jwt.json",
14
+ )
15
+
16
+
17
+ def _normalize_api_base_url(raw_value):
18
+ value = (raw_value or "").strip()
19
+ if not value:
20
+ return "https://www.sophnet.com/api"
21
+ value = value.rstrip("/")
22
+ if value.endswith("/api"):
23
+ return value
24
+ return value + "/api"
25
+
26
+
27
+ def _normalize_access_token(raw_value):
28
+ value = (raw_value or "").strip()
29
+ if not value:
30
+ return None
31
+ if value.lower().startswith("bearer "):
32
+ value = value[7:].strip()
33
+ return value or None
34
+
35
+
36
+ def _candidate_jwt_paths():
37
+ custom_path = (os.environ.get("OPENCLAW_JWT_PATH") or "").strip()
38
+ if custom_path:
39
+ yield Path(custom_path).expanduser()
40
+
41
+ for raw_path in DEFAULT_JWT_PATHS:
42
+ yield Path(raw_path).expanduser()
43
+
44
+
45
+ def _load_access_token_from_jwt():
46
+ for path in _candidate_jwt_paths():
47
+ try:
48
+ with path.open("r", encoding="utf-8") as fp:
49
+ payload = json.load(fp)
50
+ except (OSError, ValueError):
51
+ continue
52
+ if not isinstance(payload, dict):
53
+ continue
54
+ token = _normalize_access_token(payload.get("web_jwt"))
55
+ if token:
56
+ return token
57
+ return None
58
+
59
+
60
+ def _resolve_access_token():
61
+ explicit_token = _normalize_access_token(os.environ.get("SOPHNET_ACCESS_TOKEN"))
62
+ if explicit_token:
63
+ return explicit_token
64
+ return _load_access_token_from_jwt()
65
+
66
+
67
+ def load_auth_context():
68
+ return AuthContext(
69
+ api_base_url=_normalize_api_base_url(
70
+ os.environ.get("SOPHNET_API_BASE_URL") or os.environ.get("SOPHNET_BASE_URL")
71
+ ),
72
+ access_token=_resolve_access_token(),
73
+ cookie=os.environ.get("SOPHNET_COOKIE"),
74
+ )
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry for sophnet-invoice-assistant."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+
8
+ from auth_context import load_auth_context
9
+ from download_flow import run_download
10
+ from dto import ActionRequest
11
+ from invoice_client import InvoiceClient
12
+ from output import emit_result, failure
13
+ from prepare_flow import run_prepare
14
+ from status_flow import run_status
15
+ from submit_flow import run_submit
16
+ from template_flow import run_template_delete, run_template_list, run_template_save
17
+ from update_flow import run_update
18
+
19
+
20
+ def parse_args():
21
+ parser = argparse.ArgumentParser(description="Sophnet personal invoice assistant skeleton")
22
+ parser.add_argument(
23
+ "--action",
24
+ required=True,
25
+ choices=["prepare", "submit", "update", "status", "download", "template_list", "template_save", "template_delete"],
26
+ help="Action to perform",
27
+ )
28
+ parser.add_argument(
29
+ "--user-request",
30
+ required=True,
31
+ help="Original user request text",
32
+ )
33
+ parser.add_argument(
34
+ "--org-id",
35
+ help="Known personal org id",
36
+ )
37
+ parser.add_argument(
38
+ "--application-no",
39
+ help="Known invoice application number",
40
+ )
41
+ parser.add_argument(
42
+ "--template-id",
43
+ help="Known invoice template id",
44
+ )
45
+ parser.add_argument(
46
+ "--confirm",
47
+ default="false",
48
+ choices=["true", "false"],
49
+ help="Whether user already confirmed the submit action",
50
+ )
51
+ parser.add_argument("--buyer-type", choices=["INDIVIDUAL", "ENTERPRISE"])
52
+ parser.add_argument("--title")
53
+ parser.add_argument("--tax-no")
54
+ parser.add_argument("--address-phone")
55
+ parser.add_argument("--bank-account")
56
+ parser.add_argument("--invoice-type", choices=["NORMAL", "SPECIAL"])
57
+ parser.add_argument(
58
+ "--recharge-trans-nos",
59
+ help="Comma-separated recharge transNo list",
60
+ )
61
+ parser.add_argument(
62
+ "--issuing-mode",
63
+ choices=["AGGREGATED", "SEPARATE"],
64
+ help="Required when multiple recharge records are selected",
65
+ )
66
+ parser.add_argument(
67
+ "--save-as-template",
68
+ default="false",
69
+ choices=["true", "false"],
70
+ )
71
+ parser.add_argument("--template-name")
72
+ parser.add_argument(
73
+ "--use-latest-template",
74
+ default="false",
75
+ choices=["true", "false"],
76
+ )
77
+ return parser.parse_args()
78
+
79
+
80
+ def build_request(args):
81
+ return ActionRequest(
82
+ action=args.action,
83
+ user_request=args.user_request,
84
+ org_id=args.org_id,
85
+ application_no=args.application_no,
86
+ template_id=args.template_id,
87
+ confirm=args.confirm == "true",
88
+ buyer_type=args.buyer_type,
89
+ title=args.title,
90
+ tax_no=args.tax_no,
91
+ address_phone=args.address_phone,
92
+ bank_account=args.bank_account,
93
+ invoice_type=args.invoice_type,
94
+ recharge_trans_nos=args.recharge_trans_nos,
95
+ issuing_mode=args.issuing_mode,
96
+ save_as_template=args.save_as_template == "true",
97
+ template_name=args.template_name,
98
+ use_latest_template=args.use_latest_template == "true",
99
+ )
100
+
101
+
102
+ def dispatch(request, auth_context, client):
103
+ if request.action == "prepare":
104
+ return run_prepare(request, auth_context, client)
105
+ if request.action == "submit":
106
+ return run_submit(request, auth_context, client)
107
+ if request.action == "status":
108
+ return run_status(request, auth_context, client)
109
+ if request.action == "download":
110
+ return run_download(request, auth_context, client)
111
+ if request.action == "update":
112
+ return run_update(request, auth_context, client)
113
+ if request.action == "template_list":
114
+ return run_template_list(request, auth_context, client)
115
+ if request.action == "template_save":
116
+ return run_template_save(request, auth_context, client)
117
+ if request.action == "template_delete":
118
+ return run_template_delete(request, auth_context, client)
119
+ return failure(
120
+ action=request.action,
121
+ error_code="UNKNOWN_ACTION",
122
+ error_message="Unknown action: {0}".format(request.action),
123
+ )
124
+
125
+
126
+ def main():
127
+ args = parse_args()
128
+ request = build_request(args)
129
+ auth_context = load_auth_context()
130
+ client = InvoiceClient(auth_context)
131
+ result = dispatch(request, auth_context, client)
132
+ emit_result(result)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,89 @@
1
+ """Issued invoice download skeleton."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoice_client import InvoiceClientError
6
+ from output import build_result, failure, need_confirmation
7
+
8
+
9
+ def _candidate_summary(items):
10
+ values = []
11
+ for item in items[:5]:
12
+ values.append(
13
+ "{0}({1})".format(
14
+ item.get("applicationNo") or "",
15
+ item.get("title") or item.get("status") or "unknown",
16
+ )
17
+ )
18
+ return "Multiple issued invoices were found. Specify an application number before downloading: {0}".format(", ".join(values))
19
+
20
+
21
+ def run_download(request, auth_context, client):
22
+ if not auth_context.is_authenticated:
23
+ return failure(
24
+ action=request.action,
25
+ error_code="AUTH_CONTEXT_MISSING",
26
+ error_message="Current Sophnet auth context is missing. Please log in again.",
27
+ )
28
+
29
+ try:
30
+ org = client.get_current_personal_org(request.org_id)
31
+ target_application_no = request.application_no
32
+ if not target_application_no:
33
+ page = client.list_invoice_applications(
34
+ org.get("id"),
35
+ params={"pageNum": 1, "pageSize": 10, "status": "ISSUED"},
36
+ )
37
+ items = page.get("list") or [] if isinstance(page, dict) else []
38
+ if not items:
39
+ return failure(
40
+ action=request.action,
41
+ error_code="NO_ISSUED_APPLICATION_FOUND",
42
+ error_message="No issued invoice is currently available for download under the personal org.",
43
+ extra={"ORG_ID": str(org.get("id"))},
44
+ )
45
+ if len(items) > 1:
46
+ return need_confirmation(
47
+ action=request.action,
48
+ summary=_candidate_summary(items),
49
+ message="Multiple downloadable invoices were found. Please specify an application number.",
50
+ extra={
51
+ "ORG_ID": str(org.get("id")),
52
+ "CANDIDATE_APPLICATION_NOS": ",".join(
53
+ [str(item.get("applicationNo") or "") for item in items[:10]]
54
+ ),
55
+ },
56
+ )
57
+ target_application_no = items[0].get("applicationNo")
58
+
59
+ if not target_application_no:
60
+ return failure(
61
+ action=request.action,
62
+ error_code="APPLICATION_NO_REQUIRED",
63
+ error_message="The invoice application number is required before downloading.",
64
+ )
65
+
66
+ file_info = client.download_invoice_file(org.get("id"), target_application_no)
67
+ except InvoiceClientError as exc:
68
+ return failure(
69
+ action=request.action,
70
+ error_code=exc.code,
71
+ error_message=exc.message,
72
+ )
73
+
74
+ return build_result(
75
+ status="succeeded",
76
+ action=request.action,
77
+ message="Invoice download details were retrieved successfully.",
78
+ extra={
79
+ "ORG_ID": str(org.get("id")),
80
+ "ORG_NAME": org.get("name") or "",
81
+ "APPLICATION_NO": file_info.get("applicationNo") or str(target_application_no),
82
+ "FILE_NAME": file_info.get("fileName") or "",
83
+ "CONTENT_TYPE": file_info.get("contentType") or "",
84
+ "FILE_SIZE": str(file_info.get("fileSize") or ""),
85
+ "PREVIEW_URL": file_info.get("previewUrl") or "",
86
+ "DOWNLOAD_URL": file_info.get("downloadUrl") or "",
87
+ "EXPIRES_IN": str(file_info.get("expiresIn") or ""),
88
+ },
89
+ )
@@ -0,0 +1,46 @@
1
+ """Data transfer objects for the invoice assistant skeleton."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Dict, Optional
7
+
8
+
9
+ @dataclass
10
+ class ActionRequest(object):
11
+ action: str
12
+ user_request: str
13
+ org_id: Optional[str] = None
14
+ application_no: Optional[str] = None
15
+ template_id: Optional[str] = None
16
+ confirm: bool = False
17
+ buyer_type: Optional[str] = None
18
+ title: Optional[str] = None
19
+ tax_no: Optional[str] = None
20
+ address_phone: Optional[str] = None
21
+ bank_account: Optional[str] = None
22
+ invoice_type: Optional[str] = None
23
+ recharge_trans_nos: Optional[str] = None
24
+ issuing_mode: Optional[str] = None
25
+ save_as_template: bool = False
26
+ template_name: Optional[str] = None
27
+ use_latest_template: bool = False
28
+
29
+
30
+ @dataclass
31
+ class AuthContext(object):
32
+ api_base_url: str
33
+ access_token: Optional[str]
34
+ cookie: Optional[str]
35
+
36
+ @property
37
+ def is_authenticated(self) -> bool:
38
+ return bool(self.access_token or self.cookie)
39
+
40
+
41
+ @dataclass
42
+ class ActionResult(object):
43
+ status: str
44
+ action: str
45
+ message: str
46
+ extra: Optional[Dict[str, str]] = None
@@ -0,0 +1,164 @@
1
+ """Sophnet invoice API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import requests
6
+
7
+
8
+ class InvoiceClientError(RuntimeError):
9
+ def __init__(self, code, message):
10
+ super(InvoiceClientError, self).__init__(message)
11
+ self.code = code
12
+ self.message = message
13
+
14
+
15
+ class InvoiceClient(object):
16
+ def __init__(self, auth_context):
17
+ self.auth_context = auth_context
18
+ self.base_url = auth_context.api_base_url.rstrip("/")
19
+ self.timeout = 30
20
+
21
+ def _headers(self):
22
+ headers = {
23
+ "Accept": "application/json",
24
+ }
25
+ if self.auth_context.access_token:
26
+ headers["Authorization"] = "Bearer {0}".format(self.auth_context.access_token.strip())
27
+ if self.auth_context.cookie:
28
+ headers["Cookie"] = self.auth_context.cookie.strip()
29
+ return headers
30
+
31
+ def _unwrap_result(self, payload):
32
+ if not isinstance(payload, dict):
33
+ return payload
34
+ status = payload.get("status")
35
+ if status not in (None, 0):
36
+ message = payload.get("message") or "Sophnet API returned an error"
37
+ raise InvoiceClientError("SOPHNET_API_ERROR", message)
38
+ return payload.get("result")
39
+
40
+ def _request(self, method, path, params=None, json_body=None):
41
+ url = "{0}{1}".format(self.base_url, path)
42
+ try:
43
+ response = requests.request(
44
+ method=method,
45
+ url=url,
46
+ headers=self._headers(),
47
+ params=params,
48
+ json=json_body,
49
+ timeout=self.timeout,
50
+ )
51
+ except requests.RequestException as exc:
52
+ raise InvoiceClientError("NETWORK_ERROR", "Failed to request Sophnet: {0}".format(exc))
53
+
54
+ if response.status_code in (401, 403):
55
+ raise InvoiceClientError("AUTH_EXPIRED", "Current Sophnet auth has expired. Please log in again.")
56
+
57
+ try:
58
+ payload = response.json()
59
+ except ValueError:
60
+ payload = None
61
+
62
+ if not response.ok:
63
+ if isinstance(payload, dict):
64
+ message = payload.get("message") or payload.get("error") or response.text
65
+ else:
66
+ message = response.text
67
+ raise InvoiceClientError(
68
+ "HTTP_{0}".format(response.status_code),
69
+ "Sophnet API request failed: {0}".format((message or "").strip()[:300]),
70
+ )
71
+
72
+ return self._unwrap_result(payload)
73
+
74
+ def list_joined_orgs(self):
75
+ result = self._request(
76
+ "GET",
77
+ "/orgs/joined-org",
78
+ params={"pageNum": 1, "pageSize": 9999},
79
+ )
80
+ if not isinstance(result, dict):
81
+ raise InvoiceClientError("UNEXPECTED_RESPONSE", "joined-org returned an unexpected response shape.")
82
+ return result
83
+
84
+ def get_current_personal_org(self, explicit_org_id=None):
85
+ page = self.list_joined_orgs()
86
+ org_list = page.get("list") or []
87
+ if explicit_org_id is not None:
88
+ for org in org_list:
89
+ if str(org.get("id")) == str(explicit_org_id):
90
+ if org.get("isSpecificOrg") is True:
91
+ return org
92
+ raise InvoiceClientError(
93
+ "PERSONAL_ORG_REQUIRED",
94
+ "The specified org is not a personal org. This skill only supports personal invoice flows.",
95
+ )
96
+ raise InvoiceClientError("ORG_NOT_FOUND", "The specified personal org was not found.")
97
+
98
+ for org in org_list:
99
+ if org.get("isSpecificOrg") is True:
100
+ return org
101
+
102
+ raise InvoiceClientError("PERSONAL_ORG_NOT_FOUND", "No available personal org was found for the current account.")
103
+
104
+ def list_invoice_templates(self, org_id):
105
+ return self._request("GET", "/orgs/{0}/invoices/templates".format(org_id))
106
+
107
+ def save_invoice_template(self, org_id, payload):
108
+ return self._request(
109
+ "POST",
110
+ "/orgs/{0}/invoices/templates".format(org_id),
111
+ json_body=payload,
112
+ )
113
+
114
+ def delete_invoice_template(self, org_id, template_id):
115
+ return self._request(
116
+ "DELETE",
117
+ "/orgs/{0}/invoices/templates/{1}".format(org_id, template_id),
118
+ )
119
+
120
+ def list_invoiceable_recharge_records(self, org_id, params=None):
121
+ return self._request(
122
+ "GET",
123
+ "/orgs/{0}/invoices/recharge-records".format(org_id),
124
+ params=params or {"pageNum": 1, "pageSize": 20},
125
+ )
126
+
127
+ def create_invoice_application(self, org_id, payload):
128
+ return self._request(
129
+ "POST",
130
+ "/orgs/{0}/invoices/applications".format(org_id),
131
+ json_body=payload,
132
+ )
133
+
134
+ def update_invoice_application(self, org_id, application_no, payload):
135
+ return self._request(
136
+ "PUT",
137
+ "/orgs/{0}/invoices/applications/{1}".format(org_id, application_no),
138
+ json_body=payload,
139
+ )
140
+
141
+ def list_invoice_applications(self, org_id, params=None):
142
+ return self._request(
143
+ "GET",
144
+ "/orgs/{0}/invoices/applications".format(org_id),
145
+ params=params or {},
146
+ )
147
+
148
+ def get_invoice_application(self, org_id, application_no):
149
+ page = self.list_invoice_applications(
150
+ org_id,
151
+ params={"pageNum": 1, "pageSize": 1, "applicationNo": application_no},
152
+ )
153
+ if not isinstance(page, dict):
154
+ raise InvoiceClientError("UNEXPECTED_RESPONSE", "Invoice list API returned an unexpected response shape.")
155
+ items = page.get("list") or []
156
+ if not items:
157
+ raise InvoiceClientError("APPLICATION_NOT_FOUND", "The target invoice application was not found.")
158
+ return items[0]
159
+
160
+ def download_invoice_file(self, org_id, application_no):
161
+ return self._request(
162
+ "GET",
163
+ "/orgs/{0}/invoices/applications/{1}/download".format(org_id, application_no),
164
+ )
@@ -0,0 +1,63 @@
1
+ """Structured output helpers for the invoice assistant skeleton."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from dto import ActionResult
8
+
9
+
10
+ def build_result(status, action, message, extra=None):
11
+ return ActionResult(status=status, action=action, message=message, extra=extra or {})
12
+
13
+
14
+ def emit_result(result):
15
+ payload = {
16
+ "STATUS": result.status,
17
+ "ACTION": result.action,
18
+ "MESSAGE": result.message,
19
+ }
20
+ payload.update(result.extra or {})
21
+
22
+ for key, value in payload.items():
23
+ if value is None:
24
+ continue
25
+ if isinstance(value, (dict, list, tuple)):
26
+ value = json.dumps(value, ensure_ascii=False)
27
+ print("{0}={1}".format(key, value))
28
+
29
+
30
+ def failure(action, error_code, error_message, extra=None):
31
+ data = {"ERROR_CODE": error_code, "ERROR_MESSAGE": error_message}
32
+ if extra:
33
+ data.update(extra)
34
+ return build_result(
35
+ status="failed",
36
+ action=action,
37
+ message=error_message,
38
+ extra=data,
39
+ )
40
+
41
+
42
+ def not_implemented(action, message):
43
+ return build_result(
44
+ status="not_implemented",
45
+ action=action,
46
+ message=message,
47
+ extra={"ERROR_CODE": "API_NOT_IMPLEMENTED"},
48
+ )
49
+
50
+
51
+ def need_confirmation(action, summary, message, extra=None):
52
+ data = {
53
+ "CONFIRM_REQUIRED": "true",
54
+ "CONFIRM_SUMMARY": summary,
55
+ }
56
+ if extra:
57
+ data.update(extra)
58
+ return build_result(
59
+ status="need_confirmation",
60
+ action=action,
61
+ message=message,
62
+ extra=data,
63
+ )
@@ -0,0 +1,58 @@
1
+ """Prepare context for invoice actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoice_client import InvoiceClientError
6
+ from output import build_result, failure
7
+ from submit_flow import INVOICE_ELIGIBLE_START_DATE, _filter_eligible_records
8
+
9
+
10
+ def run_prepare(request, auth_context, client):
11
+ if not auth_context.is_authenticated:
12
+ return failure(
13
+ action=request.action,
14
+ error_code="AUTH_CONTEXT_MISSING",
15
+ error_message="Current Sophnet auth context is missing. Please log in again.",
16
+ )
17
+
18
+ try:
19
+ org = client.get_current_personal_org(request.org_id)
20
+ templates = client.list_invoice_templates(org.get("id")) or []
21
+ records_page = client.list_invoiceable_recharge_records(
22
+ org.get("id"),
23
+ params={"pageNum": 1, "pageSize": 20, "invoiceableOnly": "true"},
24
+ )
25
+ applications_page = client.list_invoice_applications(
26
+ org.get("id"),
27
+ params={"pageNum": 1, "pageSize": 10},
28
+ )
29
+ except InvoiceClientError as exc:
30
+ return failure(
31
+ action=request.action,
32
+ error_code=exc.code,
33
+ error_message=exc.message,
34
+ )
35
+
36
+ records = records_page.get("list") or [] if isinstance(records_page, dict) else []
37
+ eligible_records, ineligible_records = _filter_eligible_records(records)
38
+ applications = applications_page.get("list") or [] if isinstance(applications_page, dict) else []
39
+
40
+ return build_result(
41
+ status="succeeded",
42
+ action=request.action,
43
+ message="Resolved the current personal org and loaded invoice context.",
44
+ extra={
45
+ "ORG_ID": str(org.get("id")),
46
+ "ORG_NAME": org.get("name") or "",
47
+ "IS_SPECIFIC_ORG": str(bool(org.get("isSpecificOrg"))).lower(),
48
+ "TEMPLATE_COUNT": str(len(templates)),
49
+ "AVAILABLE_RECHARGE_RECORD_COUNT": str(len(eligible_records)),
50
+ "INELIGIBLE_RECHARGE_RECORD_COUNT": str(len(ineligible_records)),
51
+ "INVOICE_ELIGIBLE_START_DATE": INVOICE_ELIGIBLE_START_DATE,
52
+ "APPLICATION_COUNT": str(len(applications)),
53
+ "TEMPLATES": templates[:10],
54
+ "AVAILABLE_RECHARGE_RECORDS": eligible_records[:10],
55
+ "INELIGIBLE_RECHARGE_RECORDS": ineligible_records[:10],
56
+ "RECENT_APPLICATIONS": applications[:10],
57
+ },
58
+ )