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.
- package/package.json +1 -1
- package/skills/sophnet-age-appearance/skill.json +9 -2
- package/skills/sophnet-age-appearance/src/scripts/age_appearance.py +8 -10
- package/skills/sophnet-id-photo/skill.json +9 -2
- package/skills/sophnet-id-photo/src/scripts/id_photo.py +5 -7
- package/skills/sophnet-image-edit/skill.json +9 -2
- package/skills/sophnet-image-edit/src/scripts/edit_image.py +5 -7
- package/skills/sophnet-image-generate/skill.json +9 -2
- package/skills/sophnet-image-generate/src/scripts/generate_image.py +51 -1
- package/skills/sophnet-infinite-talk/skill.json +9 -2
- package/skills/sophnet-infinite-talk/src/scripts/gen.py +39 -0
- package/skills/sophnet-invoice-assistant/skill.json +21 -0
- package/skills/sophnet-invoice-assistant/src/SKILL.md +186 -0
- package/skills/sophnet-invoice-assistant/src/pyproject.toml +8 -0
- package/skills/sophnet-invoice-assistant/src/references/api-contract.md +73 -0
- package/skills/sophnet-invoice-assistant/src/references/conversation-examples.md +16 -0
- package/skills/sophnet-invoice-assistant/src/references/error-cases.md +16 -0
- package/skills/sophnet-invoice-assistant/src/scripts/__init__.py +1 -0
- package/skills/sophnet-invoice-assistant/src/scripts/auth_context.py +74 -0
- package/skills/sophnet-invoice-assistant/src/scripts/cli.py +136 -0
- package/skills/sophnet-invoice-assistant/src/scripts/download_flow.py +89 -0
- package/skills/sophnet-invoice-assistant/src/scripts/dto.py +46 -0
- package/skills/sophnet-invoice-assistant/src/scripts/invoice_client.py +164 -0
- package/skills/sophnet-invoice-assistant/src/scripts/output.py +63 -0
- package/skills/sophnet-invoice-assistant/src/scripts/prepare_flow.py +58 -0
- package/skills/sophnet-invoice-assistant/src/scripts/risk_rules.py +95 -0
- package/skills/sophnet-invoice-assistant/src/scripts/status_flow.py +69 -0
- package/skills/sophnet-invoice-assistant/src/scripts/submit_flow.py +472 -0
- package/skills/sophnet-invoice-assistant/src/scripts/template_flow.py +196 -0
- package/skills/sophnet-invoice-assistant/src/scripts/update_flow.py +223 -0
- package/skills/sophnet-sticker-edit/skill.json +9 -2
- package/skills/sophnet-sticker-edit/src/scripts/edit_sticker_image.py +5 -7
- package/skills/sophnet-video-generate/skill.json +9 -2
- package/skills/sophnet-video-generate/src/SKILL.md +21 -12
- package/skills/sophnet-video-generate/src/scripts/gen_video.py +10 -15
- package/skills/store-marketing/skill.json +9 -2
- 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
|
+
)
|