sophhub 0.4.46 → 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-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
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Risk classification rules for the invoice assistant skeleton."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
KEYWORD_LAST = "\u4e0a\u6b21"
|
|
6
|
+
KEYWORD_RECENT = "\u6700\u8fd1"
|
|
7
|
+
KEYWORD_LATEST = "\u6700\u65b0"
|
|
8
|
+
|
|
9
|
+
TEMPLATE_FIELD_NAMES = (
|
|
10
|
+
"buyerType",
|
|
11
|
+
"title",
|
|
12
|
+
"taxNo",
|
|
13
|
+
"addressPhone",
|
|
14
|
+
"bankAccount",
|
|
15
|
+
"invoiceType",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_text(value):
|
|
20
|
+
if value is None:
|
|
21
|
+
return ""
|
|
22
|
+
return str(value).strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _selected_trans_nos(request):
|
|
26
|
+
return [item.strip() for item in str(request.recharge_trans_nos or "").split(",") if item.strip()]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _requests_latest_template(request):
|
|
30
|
+
text = request.user_request or ""
|
|
31
|
+
return bool(
|
|
32
|
+
request.use_latest_template
|
|
33
|
+
or KEYWORD_LAST in text
|
|
34
|
+
or KEYWORD_RECENT in text
|
|
35
|
+
or KEYWORD_LATEST in text
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _same_template(left, right):
|
|
40
|
+
if not left or not right:
|
|
41
|
+
return False
|
|
42
|
+
left_id = left.get("id")
|
|
43
|
+
right_id = right.get("id")
|
|
44
|
+
if left_id is not None and right_id is not None:
|
|
45
|
+
return str(left_id) == str(right_id)
|
|
46
|
+
return _normalize_text(left.get("templateName")) == _normalize_text(right.get("templateName"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _payload_matches_template(payload, template):
|
|
50
|
+
if not payload or not template:
|
|
51
|
+
return False
|
|
52
|
+
for field_name in TEMPLATE_FIELD_NAMES:
|
|
53
|
+
if _normalize_text(payload.get(field_name)) != _normalize_text(template.get(field_name)):
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def classify_submit_risk(request, payload=None, template=None, latest_template=None, selected_records=None):
|
|
59
|
+
selected_records = selected_records or []
|
|
60
|
+
values = _selected_trans_nos(request)
|
|
61
|
+
|
|
62
|
+
if request.save_as_template:
|
|
63
|
+
return {
|
|
64
|
+
"level": "high",
|
|
65
|
+
"reason": "save_as_template_requires_confirmation",
|
|
66
|
+
}
|
|
67
|
+
if len(values) != 1 or len(selected_records) != 1:
|
|
68
|
+
return {
|
|
69
|
+
"level": "high",
|
|
70
|
+
"reason": "single_recharge_record_required",
|
|
71
|
+
}
|
|
72
|
+
if not _requests_latest_template(request):
|
|
73
|
+
return {
|
|
74
|
+
"level": "high",
|
|
75
|
+
"reason": "latest_template_reuse_not_requested",
|
|
76
|
+
}
|
|
77
|
+
if not template or not latest_template:
|
|
78
|
+
return {
|
|
79
|
+
"level": "high",
|
|
80
|
+
"reason": "latest_template_not_available",
|
|
81
|
+
}
|
|
82
|
+
if not _same_template(template, latest_template):
|
|
83
|
+
return {
|
|
84
|
+
"level": "high",
|
|
85
|
+
"reason": "selected_template_is_not_latest",
|
|
86
|
+
}
|
|
87
|
+
if not _payload_matches_template(payload, template):
|
|
88
|
+
return {
|
|
89
|
+
"level": "high",
|
|
90
|
+
"reason": "invoice_fields_changed_from_template",
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
"level": "low",
|
|
94
|
+
"reason": "latest_template_with_single_record_and_matching_fields",
|
|
95
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Invoice application status query skeleton."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from invoice_client import InvoiceClientError
|
|
6
|
+
from output import build_result, failure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_status_message(application):
|
|
10
|
+
status = application.get("status")
|
|
11
|
+
application_no = application.get("applicationNo")
|
|
12
|
+
if status == "ISSUED":
|
|
13
|
+
return "Invoice application {0} has been issued and is ready for download.".format(application_no)
|
|
14
|
+
if status == "REJECTED":
|
|
15
|
+
return "Invoice application {0} was rejected. Check the reject reason before retrying.".format(application_no)
|
|
16
|
+
return "Invoice application {0} is still being processed.".format(application_no)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_status(request, auth_context, client):
|
|
20
|
+
if not auth_context.is_authenticated:
|
|
21
|
+
return failure(
|
|
22
|
+
action=request.action,
|
|
23
|
+
error_code="AUTH_CONTEXT_MISSING",
|
|
24
|
+
error_message="Current Sophnet auth context is missing. Please log in again.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
org = client.get_current_personal_org(request.org_id)
|
|
29
|
+
params = {"pageNum": 1, "pageSize": 10}
|
|
30
|
+
if request.application_no:
|
|
31
|
+
params["applicationNo"] = request.application_no
|
|
32
|
+
page = client.list_invoice_applications(org.get("id"), params=params)
|
|
33
|
+
except InvoiceClientError as exc:
|
|
34
|
+
return failure(
|
|
35
|
+
action=request.action,
|
|
36
|
+
error_code=exc.code,
|
|
37
|
+
error_message=exc.message,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
items = []
|
|
41
|
+
if isinstance(page, dict):
|
|
42
|
+
items = page.get("list") or []
|
|
43
|
+
if not items:
|
|
44
|
+
return failure(
|
|
45
|
+
action=request.action,
|
|
46
|
+
error_code="NO_APPLICATION_FOUND",
|
|
47
|
+
error_message="No invoice application was found under the current personal org.",
|
|
48
|
+
extra={"ORG_ID": str(org.get("id"))},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
application = items[0]
|
|
52
|
+
return build_result(
|
|
53
|
+
status="succeeded",
|
|
54
|
+
action=request.action,
|
|
55
|
+
message=_build_status_message(application),
|
|
56
|
+
extra={
|
|
57
|
+
"ORG_ID": str(org.get("id")),
|
|
58
|
+
"ORG_NAME": org.get("name") or "",
|
|
59
|
+
"APPLICATION_NO": application.get("applicationNo") or "",
|
|
60
|
+
"APPLICATION_STATUS": application.get("status") or "",
|
|
61
|
+
"TITLE": application.get("title") or "",
|
|
62
|
+
"AMOUNT": str(application.get("amount") or ""),
|
|
63
|
+
"HAS_INVOICE_FILE": str(bool(application.get("hasInvoiceFile"))).lower(),
|
|
64
|
+
"APPLIED_AT": application.get("appliedAt") or "",
|
|
65
|
+
"ISSUED_AT": application.get("issuedAt") or "",
|
|
66
|
+
"REJECT_REASON": application.get("rejectReason") or "",
|
|
67
|
+
"RESULT_COUNT": str(len(items)),
|
|
68
|
+
},
|
|
69
|
+
)
|