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,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
+ )
@@ -0,0 +1,472 @@
1
+ """Submit invoice application flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoice_client import InvoiceClientError
6
+ from output import build_result, failure, need_confirmation
7
+ from risk_rules import classify_submit_risk
8
+
9
+
10
+ VALID_BUYER_TYPES = ("INDIVIDUAL", "ENTERPRISE")
11
+ VALID_INVOICE_TYPES = ("NORMAL", "SPECIAL")
12
+ VALID_ISSUING_MODES = ("AGGREGATED", "SEPARATE")
13
+ INVOICE_ELIGIBLE_START_DATE = "2026-06-24"
14
+
15
+ KEYWORD_ENTERPRISE = "\u4f01\u4e1a"
16
+ KEYWORD_COMPANY = "\u516c\u53f8"
17
+ KEYWORD_SPECIAL_SHORT = "\u4e13\u7968"
18
+ KEYWORD_SPECIAL_LONG = "\u4e13\u7528\u53d1\u7968"
19
+ KEYWORD_NORMAL_SHORT = "\u666e\u7968"
20
+ KEYWORD_NORMAL_LONG = "\u666e\u901a\u53d1\u7968"
21
+ KEYWORD_LAST = "\u4e0a\u6b21"
22
+ KEYWORD_RECENT = "\u6700\u8fd1"
23
+ KEYWORD_LATEST = "\u6700\u65b0"
24
+ KEYWORD_THAT_ONE = "\u90a3\u7b14"
25
+ KEYWORD_AGGREGATED = "\u805a\u5408"
26
+ KEYWORD_SUM = "\u5408\u5e76"
27
+ KEYWORD_TOTAL = "\u6c47\u603b"
28
+ KEYWORD_SINGLE = "\u5206\u5f00"
29
+ KEYWORD_SEPARATE = "\u5355\u72ec"
30
+ KEYWORD_EACH = "\u6bcf\u6761"
31
+
32
+
33
+ def _normalize_text(value):
34
+ if value is None:
35
+ return None
36
+ text = str(value).strip()
37
+ return text or None
38
+
39
+
40
+ def _normalize_tax_no(value):
41
+ text = _normalize_text(value)
42
+ return text.upper() if text else None
43
+
44
+
45
+ def _normalize_trans_nos(value):
46
+ if value is None:
47
+ return []
48
+ if isinstance(value, (list, tuple)):
49
+ raw_values = value
50
+ else:
51
+ raw_values = str(value).split(",")
52
+
53
+ result = []
54
+ seen = set()
55
+ for item in raw_values:
56
+ current = str(item).strip()
57
+ if not current or current in seen:
58
+ continue
59
+ seen.add(current)
60
+ result.append(current)
61
+ return result
62
+
63
+
64
+ def _record_created_at(record):
65
+ value = record.get("createdAt") or record.get("rechargedAt")
66
+ if value is None:
67
+ return ""
68
+ return str(value)
69
+
70
+
71
+ def _is_record_eligible(record):
72
+ created_at = _record_created_at(record)
73
+ if not created_at:
74
+ return False
75
+ return created_at[:10] >= INVOICE_ELIGIBLE_START_DATE
76
+
77
+
78
+ def _filter_eligible_records(records):
79
+ eligible = []
80
+ ineligible = []
81
+ for item in records or []:
82
+ if _is_record_eligible(item):
83
+ eligible.append(item)
84
+ else:
85
+ ineligible.append(item)
86
+ return eligible, ineligible
87
+
88
+
89
+ def _resolve_issuing_mode(request, trans_nos, fallback_mode=None):
90
+ explicit = _normalize_text(request.issuing_mode)
91
+ if explicit in VALID_ISSUING_MODES:
92
+ return explicit, None
93
+
94
+ normalized_fallback = _normalize_text(fallback_mode)
95
+ if normalized_fallback in VALID_ISSUING_MODES and len(trans_nos) > 1:
96
+ return normalized_fallback, None
97
+
98
+ if len(trans_nos) <= 1:
99
+ return None, None
100
+
101
+ text = request.user_request or ""
102
+ if KEYWORD_AGGREGATED in text or KEYWORD_SUM in text or KEYWORD_TOTAL in text:
103
+ return "AGGREGATED", None
104
+ if KEYWORD_SINGLE in text or KEYWORD_SEPARATE in text or KEYWORD_EACH in text:
105
+ return "SEPARATE", None
106
+
107
+ return None, "issuingMode"
108
+
109
+
110
+ def _parse_buyer_type(request):
111
+ if request.buyer_type in VALID_BUYER_TYPES:
112
+ return request.buyer_type
113
+ text = request.user_request or ""
114
+ if KEYWORD_ENTERPRISE in text or KEYWORD_COMPANY in text:
115
+ return "ENTERPRISE"
116
+ return "INDIVIDUAL"
117
+
118
+
119
+ def _parse_invoice_type(request, buyer_type):
120
+ if request.invoice_type in VALID_INVOICE_TYPES:
121
+ return request.invoice_type
122
+ text = request.user_request or ""
123
+ if KEYWORD_SPECIAL_SHORT in text or KEYWORD_SPECIAL_LONG in text:
124
+ return "SPECIAL"
125
+ if KEYWORD_NORMAL_SHORT in text or KEYWORD_NORMAL_LONG in text:
126
+ return "NORMAL"
127
+ if buyer_type == "ENTERPRISE":
128
+ return "SPECIAL"
129
+ return "NORMAL"
130
+
131
+
132
+ def _get_field_requirements(buyer_type, invoice_type):
133
+ if buyer_type == "INDIVIDUAL":
134
+ return {
135
+ "tax_no": False,
136
+ "address_phone": False,
137
+ "bank_account": False,
138
+ "allow_special": False,
139
+ }
140
+ if invoice_type == "NORMAL":
141
+ return {
142
+ "tax_no": True,
143
+ "address_phone": False,
144
+ "bank_account": False,
145
+ "allow_special": True,
146
+ }
147
+ return {
148
+ "tax_no": True,
149
+ "address_phone": True,
150
+ "bank_account": True,
151
+ "allow_special": True,
152
+ }
153
+
154
+
155
+ def _sort_templates(templates):
156
+ def sort_key(item):
157
+ return item.get("updatedAt") or ""
158
+
159
+ return sorted(templates or [], key=sort_key, reverse=True)
160
+
161
+
162
+ def _find_latest_template(templates):
163
+ sorted_items = _sort_templates(templates)
164
+ return sorted_items[0] if sorted_items else None
165
+
166
+
167
+ def _find_template_by_name(templates, template_name):
168
+ target = _normalize_text(template_name)
169
+ if not target:
170
+ return None
171
+ for item in templates or []:
172
+ if _normalize_text(item.get("templateName")) == target:
173
+ return item
174
+ return None
175
+
176
+
177
+ def _resolve_template(request, templates):
178
+ named = _find_template_by_name(templates, request.template_name)
179
+ if named is not None:
180
+ return named
181
+ text = request.user_request or ""
182
+ if request.use_latest_template or KEYWORD_LAST in text or KEYWORD_RECENT in text or KEYWORD_LATEST in text:
183
+ return _find_latest_template(templates)
184
+ return None
185
+
186
+
187
+ def _infer_recharge_records(request, records):
188
+ explicit = _normalize_trans_nos(request.recharge_trans_nos)
189
+ if explicit:
190
+ return explicit, None
191
+
192
+ eligible_records, ineligible_records = _filter_eligible_records(records)
193
+ available = [item for item in eligible_records if item.get("invoiceStatus") == "AVAILABLE"]
194
+ if not available:
195
+ if ineligible_records:
196
+ return [], (
197
+ "Recharge records before 2026-06-24 are not invoiceable. "
198
+ "Please choose recharge records on or after 2026-06-24."
199
+ )
200
+ return [], "No invoiceable recharge records are currently available."
201
+
202
+ if len(available) == 1:
203
+ return [available[0].get("transNo")], None
204
+
205
+ text = request.user_request or ""
206
+ if KEYWORD_RECENT in text or KEYWORD_THAT_ONE in text or KEYWORD_LATEST in text:
207
+ return [available[0].get("transNo")], None
208
+
209
+ return [], "Multiple invoiceable recharge records are available. Please specify rechargeTransNos."
210
+
211
+
212
+ def _select_record_map(records, trans_nos):
213
+ lookup = {}
214
+ for item in records or []:
215
+ trans_no = item.get("transNo")
216
+ if trans_no:
217
+ lookup[str(trans_no)] = item
218
+ return [lookup[item] for item in trans_nos if item in lookup]
219
+
220
+
221
+ def _sum_amount(records):
222
+ total = 0.0
223
+ for item in records:
224
+ try:
225
+ total += float(item.get("changeAmount") or 0)
226
+ except (TypeError, ValueError):
227
+ continue
228
+ return total
229
+
230
+
231
+ def _validate_fields(buyer_type, invoice_type, title, tax_no, address_phone, bank_account):
232
+ requirements = _get_field_requirements(buyer_type, invoice_type)
233
+ missing = []
234
+ if not title:
235
+ missing.append("title")
236
+ if invoice_type == "SPECIAL" and not requirements["allow_special"]:
237
+ return ["invoiceType"]
238
+ if requirements["tax_no"] and not tax_no:
239
+ missing.append("taxNo")
240
+ if requirements["address_phone"] and not address_phone:
241
+ missing.append("addressPhone")
242
+ if requirements["bank_account"] and not bank_account:
243
+ missing.append("bankAccount")
244
+ return missing
245
+
246
+
247
+ def _resolve_template_name(request, template):
248
+ return (
249
+ _normalize_text(request.template_name)
250
+ or _normalize_text(template.get("templateName") if template else None)
251
+ or _normalize_text(request.title)
252
+ )
253
+
254
+
255
+ def _build_payload(request, template, records):
256
+ buyer_type = _parse_buyer_type(request)
257
+ invoice_type = _parse_invoice_type(request, buyer_type)
258
+
259
+ title = _normalize_text(request.title) or _normalize_text(template.get("title") if template else None)
260
+ tax_no = _normalize_tax_no(request.tax_no) or _normalize_tax_no(template.get("taxNo") if template else None)
261
+ address_phone = _normalize_text(request.address_phone) or _normalize_text(template.get("addressPhone") if template else None)
262
+ bank_account = _normalize_text(request.bank_account) or _normalize_text(template.get("bankAccount") if template else None)
263
+
264
+ eligible_records, _ = _filter_eligible_records(records)
265
+ trans_nos, trans_no_error = _infer_recharge_records(request, eligible_records)
266
+ if trans_no_error:
267
+ return None, [trans_no_error], []
268
+
269
+ selected_records = _select_record_map(eligible_records, trans_nos)
270
+ if len(selected_records) != len(trans_nos):
271
+ return None, [
272
+ "One or more rechargeTransNos are not invoiceable. "
273
+ "Recharge records before 2026-06-24 are not supported."
274
+ ], trans_nos
275
+
276
+ issuing_mode, issuing_mode_error = _resolve_issuing_mode(request, trans_nos)
277
+ if issuing_mode_error:
278
+ return None, [issuing_mode_error], trans_nos
279
+
280
+ missing_fields = _validate_fields(
281
+ buyer_type,
282
+ invoice_type,
283
+ title,
284
+ tax_no,
285
+ address_phone,
286
+ bank_account,
287
+ )
288
+ if not trans_nos:
289
+ missing_fields.append("rechargeTransNos")
290
+ resolved_template_name = _resolve_template_name(request, template)
291
+ if not resolved_template_name:
292
+ missing_fields.append("templateName")
293
+ if missing_fields:
294
+ return None, missing_fields, trans_nos
295
+
296
+ payload = {
297
+ "buyerType": buyer_type,
298
+ "title": title,
299
+ "invoiceType": invoice_type,
300
+ "rechargeTransNos": trans_nos,
301
+ }
302
+ if tax_no:
303
+ payload["taxNo"] = tax_no
304
+ if address_phone:
305
+ payload["addressPhone"] = address_phone
306
+ if bank_account:
307
+ payload["bankAccount"] = bank_account
308
+ if issuing_mode:
309
+ payload["issuingMode"] = issuing_mode
310
+ payload["saveAsTemplate"] = True
311
+ payload["templateName"] = resolved_template_name
312
+ return payload, [], trans_nos
313
+
314
+
315
+ def _build_confirmation_summary(payload, selected_records, template):
316
+ source = "latest template" if template else "manual input"
317
+ return (
318
+ "Submit invoice with buyerType={buyerType}, title={title}, invoiceType={invoiceType}, "
319
+ "rechargeTransNos={transNos}, issuingMode={issuingMode}, amount={amount:.2f}, source={source}"
320
+ ).format(
321
+ buyerType=payload.get("buyerType"),
322
+ title=payload.get("title"),
323
+ invoiceType=payload.get("invoiceType"),
324
+ transNos=",".join(payload.get("rechargeTransNos") or []),
325
+ issuingMode=payload.get("issuingMode") or "",
326
+ amount=_sum_amount(selected_records),
327
+ source=source,
328
+ )
329
+
330
+
331
+ def _requests_latest_template(request, template, latest_template):
332
+ text = request.user_request or ""
333
+ if not template or not latest_template:
334
+ return False
335
+ template_id = template.get("id")
336
+ latest_template_id = latest_template.get("id")
337
+ same_latest = False
338
+ if template_id is not None and latest_template_id is not None:
339
+ same_latest = str(template_id) == str(latest_template_id)
340
+ elif _normalize_text(template.get("templateName")) and _normalize_text(template.get("templateName")) == _normalize_text(latest_template.get("templateName")):
341
+ same_latest = True
342
+ return bool(
343
+ same_latest and (
344
+ request.use_latest_template
345
+ or KEYWORD_LAST in text
346
+ or KEYWORD_RECENT in text
347
+ or KEYWORD_LATEST in text
348
+ )
349
+ )
350
+
351
+
352
+ def run_submit(request, auth_context, client):
353
+ if not auth_context.is_authenticated:
354
+ return failure(
355
+ action=request.action,
356
+ error_code="AUTH_CONTEXT_MISSING",
357
+ error_message="Current Sophnet auth context is missing. Please log in again.",
358
+ )
359
+
360
+ try:
361
+ org = client.get_current_personal_org(request.org_id)
362
+ templates = client.list_invoice_templates(org.get("id")) or []
363
+ records_page = client.list_invoiceable_recharge_records(
364
+ org.get("id"),
365
+ params={"pageNum": 1, "pageSize": 100, "invoiceableOnly": "true"},
366
+ )
367
+ records = records_page.get("list") or [] if isinstance(records_page, dict) else []
368
+ eligible_records, ineligible_records = _filter_eligible_records(records)
369
+ latest_template = _find_latest_template(templates)
370
+ template = _resolve_template(request, templates)
371
+ payload, missing_fields, trans_nos = _build_payload(request, template, eligible_records)
372
+ if payload is None:
373
+ if len(missing_fields) == 1 and "Multiple invoiceable recharge records" in missing_fields[0]:
374
+ available_records = [item for item in eligible_records if item.get("invoiceStatus") == "AVAILABLE"]
375
+ return need_confirmation(
376
+ action=request.action,
377
+ summary="Selectable recharge records: {0}".format(
378
+ ", ".join([str(item.get("transNo") or "") for item in available_records[:10]])
379
+ ),
380
+ message="Please specify rechargeTransNos before submitting the invoice request.",
381
+ extra={
382
+ "ORG_ID": str(org.get("id")),
383
+ "CANDIDATE_RECHARGE_TRANS_NOS": ",".join(
384
+ [str(item.get("transNo") or "") for item in available_records[:10]]
385
+ ),
386
+ },
387
+ )
388
+ if "issuingMode" in missing_fields:
389
+ available_records = [item for item in eligible_records if item.get("invoiceStatus") == "AVAILABLE"]
390
+ return need_confirmation(
391
+ action=request.action,
392
+ summary="Selected multiple recharge records. Please choose issuingMode=AGGREGATED or SEPARATE.",
393
+ message="When one invoice application contains multiple recharge records, you must choose aggregated or separate invoicing.",
394
+ extra={
395
+ "ORG_ID": str(org.get("id")),
396
+ "CANDIDATE_RECHARGE_TRANS_NOS": ",".join(
397
+ [str(item.get("transNo") or "") for item in available_records[:10]]
398
+ ),
399
+ "AVAILABLE_ISSUING_MODES": ",".join(VALID_ISSUING_MODES),
400
+ },
401
+ )
402
+ return failure(
403
+ action=request.action,
404
+ error_code="MISSING_REQUIRED_FIELDS",
405
+ error_message="Missing required submit fields: {0}".format(",".join(missing_fields)),
406
+ extra={
407
+ "ORG_ID": str(org.get("id")),
408
+ "INELIGIBLE_RECHARGE_RECORD_COUNT": str(len(ineligible_records)),
409
+ },
410
+ )
411
+
412
+ selected_records = _select_record_map(eligible_records, trans_nos)
413
+ request.buyer_type = payload.get("buyerType")
414
+ request.invoice_type = payload.get("invoiceType")
415
+ request.recharge_trans_nos = ",".join(payload.get("rechargeTransNos") or [])
416
+ request.issuing_mode = payload.get("issuingMode")
417
+ request.use_latest_template = _requests_latest_template(
418
+ request,
419
+ template,
420
+ latest_template,
421
+ )
422
+ risk = classify_submit_risk(
423
+ request,
424
+ payload=payload,
425
+ template=template,
426
+ latest_template=latest_template,
427
+ selected_records=selected_records,
428
+ )
429
+ if risk["level"] == "high" and not request.confirm:
430
+ return need_confirmation(
431
+ action=request.action,
432
+ summary=_build_confirmation_summary(payload, selected_records, template),
433
+ message="Please confirm the invoice details before submission.",
434
+ extra={
435
+ "ORG_ID": str(org.get("id")),
436
+ "RISK_LEVEL": risk.get("level") or "",
437
+ "RISK_REASON": risk.get("reason") or "",
438
+ "BUYER_TYPE": payload.get("buyerType") or "",
439
+ "TITLE": payload.get("title") or "",
440
+ "INVOICE_TYPE": payload.get("invoiceType") or "",
441
+ "RECHARGE_TRANS_NOS": ",".join(payload.get("rechargeTransNos") or []),
442
+ "ISSUING_MODE": payload.get("issuingMode") or "",
443
+ },
444
+ )
445
+
446
+ application = client.create_invoice_application(org.get("id"), payload)
447
+ except InvoiceClientError as exc:
448
+ return failure(
449
+ action=request.action,
450
+ error_code=exc.code,
451
+ error_message=exc.message,
452
+ )
453
+
454
+ return build_result(
455
+ status="succeeded",
456
+ action=request.action,
457
+ message="Invoice application was submitted successfully.",
458
+ extra={
459
+ "ORG_ID": str(org.get("id")),
460
+ "ORG_NAME": org.get("name") or "",
461
+ "RISK_LEVEL": risk.get("level") or "",
462
+ "RISK_REASON": risk.get("reason") or "",
463
+ "APPLICATION_NO": application.get("applicationNo") or "",
464
+ "APPLICATION_STATUS": application.get("status") or "",
465
+ "TITLE": application.get("title") or "",
466
+ "AMOUNT": str(application.get("amount") or ""),
467
+ "ISSUING_MODE": payload.get("issuingMode") or "",
468
+ "HAS_INVOICE_FILE": str(bool(application.get("hasInvoiceFile"))).lower(),
469
+ "INELIGIBLE_RECHARGE_RECORD_COUNT": str(len(ineligible_records)),
470
+ "INVOICE_ELIGIBLE_START_DATE": INVOICE_ELIGIBLE_START_DATE,
471
+ },
472
+ )