添加一家公司
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -365,6 +365,7 @@ export default {
|
||||
noPdfUrl: 'The backend did not return a PDF URL.',
|
||||
backendTotals: 'Backend totals',
|
||||
validationTitle: 'Please fix the form',
|
||||
generationErrorTitle: 'Generation failed',
|
||||
fieldErrors: {
|
||||
required: 'Required',
|
||||
invalidEmail: 'Invalid email',
|
||||
|
||||
@@ -365,6 +365,7 @@ export default {
|
||||
noPdfUrl: 'หลังบ้านไม่ได้ส่งลิงก์ PDF กลับมา',
|
||||
backendTotals: 'ยอดจากหลังบ้าน',
|
||||
validationTitle: 'กรุณาแก้ไขแบบฟอร์ม',
|
||||
generationErrorTitle: 'สร้างไม่สำเร็จ',
|
||||
fieldErrors: {
|
||||
required: 'จำเป็นต้องกรอก',
|
||||
invalidEmail: 'รูปแบบ Email ไม่ถูกต้อง',
|
||||
|
||||
@@ -365,6 +365,7 @@ export default {
|
||||
noPdfUrl: '后端未返回 PDF 链接。',
|
||||
backendTotals: '后端返回金额',
|
||||
validationTitle: '请先修正表单',
|
||||
generationErrorTitle: '生成失败',
|
||||
fieldErrors: {
|
||||
required: '必填',
|
||||
invalidEmail: 'Email 格式不正确',
|
||||
|
||||
@@ -816,6 +816,24 @@ describe('ReservationManualInvoiceView', () => {
|
||||
expect(wrapper.text()).toContain('PDF 生成超时')
|
||||
})
|
||||
|
||||
it('does not label backend generation failures as form validation errors', async () => {
|
||||
vi.mocked(service.generateManualReservationInvoice).mockRejectedValue(
|
||||
new ApiError('Request failed with status 502', 502, {
|
||||
error_code: 'RESERVATION_INVOICE_GENERATION_FAILED',
|
||||
message: 'Invoice PDF 生成失败,请稍后重试。',
|
||||
}),
|
||||
)
|
||||
const wrapper = mountView()
|
||||
|
||||
await wrapper.find('[data-testid="recipient-company-code"]').setValue('QBD')
|
||||
await fillMinimumInvoiceForm(wrapper)
|
||||
await wrapper.find('[data-testid="manual-invoice-submit"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('生成失败')
|
||||
expect(wrapper.text()).not.toContain(zhCN.manualInvoice.validationTitle)
|
||||
})
|
||||
|
||||
it('keeps backend raw details in a folded technical detail block', async () => {
|
||||
vi.mocked(service.generateManualReservationInvoice).mockRejectedValue(
|
||||
new ApiError('Request failed with status 422', 422, {
|
||||
|
||||
@@ -922,6 +922,19 @@ const recipientCompanies: RecipientCompanySeed[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
company_code: 'CENTURY_WAN_CHENG',
|
||||
company: 'Century Wan Cheng Tourism Co., Ltd.',
|
||||
contacts: [
|
||||
{
|
||||
contact_id: 'CENTURY_WAN_CHENG_KHUN_NID',
|
||||
attention: 'Khun Nid',
|
||||
address: '622 Soi Ratchada Niwet, Intersection 20, Huai Khwang District, Bangkok 10310',
|
||||
telephone: '081-736-9808',
|
||||
email: 'meojiutravel.hotel@gmail.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
company_code: 'HANATOUR',
|
||||
company: 'HANATOUR TD CO., LTD.',
|
||||
@@ -1015,6 +1028,7 @@ const messages = ref<string[]>([])
|
||||
const technicalDetails = ref<string[]>([])
|
||||
const downloadMessageKey = ref('')
|
||||
const alertTone = ref<'error' | 'success'>('error')
|
||||
const alertKind = ref<'validation' | 'generation'>('validation')
|
||||
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
|
||||
const documentDatesEdited = ref(false)
|
||||
const primaryNightsEdited = ref(false)
|
||||
@@ -1044,7 +1058,9 @@ const alertTitle = computed(() => {
|
||||
if (alertTone.value === 'success') {
|
||||
return t('manualInvoice.resultSection')
|
||||
}
|
||||
return t('manualInvoice.validationTitle')
|
||||
return alertKind.value === 'generation'
|
||||
? t('manualInvoice.generationErrorTitle')
|
||||
: t('manualInvoice.validationTitle')
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -1131,6 +1147,7 @@ async function submitInvoice(): Promise<void> {
|
||||
validationAttempted.value = true
|
||||
messages.value = validateForm()
|
||||
alertTone.value = 'error'
|
||||
alertKind.value = 'validation'
|
||||
if (messages.value.length > 0) {
|
||||
return
|
||||
}
|
||||
@@ -1144,6 +1161,7 @@ async function submitInvoice(): Promise<void> {
|
||||
const errorDescription = describeGenerationError(error)
|
||||
messages.value = [errorDescription.message]
|
||||
technicalDetails.value = errorDescription.technicalDetails
|
||||
alertKind.value = isBackendValidationError(error) ? 'validation' : 'generation'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -1457,6 +1475,10 @@ function errorCodeFromDetails(details: unknown): string {
|
||||
return 'UNKNOWN'
|
||||
}
|
||||
|
||||
function isBackendValidationError(error: unknown): boolean {
|
||||
return error instanceof ApiError && errorCodeFromDetails(error.details) === 'RESERVATION_INVOICE_VALIDATION_FAILED'
|
||||
}
|
||||
|
||||
function detailMessages(details: unknown): string[] {
|
||||
if (!isRecord(details)) {
|
||||
return []
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| Version | 0.2 |
|
||||
| Version | 0.3 |
|
||||
| Scope | 可复用软件工程标准 |
|
||||
| Audience | 人类开发者、产品人员、架构师、AI Agent |
|
||||
|
||||
@@ -33,6 +33,12 @@ Prompt 是临时的,Context 是长期资产。重要知识应该沉淀在仓
|
||||
|
||||
文档不是一次性产物。文档随着项目成长,开发结束时文档也应该同步结束。
|
||||
|
||||
### Progressive Complexity
|
||||
|
||||
软件开发默认从简单到复杂。第一版应优先交付可运行、可验证的功能闭环,不为假想的未来复杂度提前引入过度抽象、复杂架构或难以理解的通用框架。
|
||||
|
||||
高级安全、高并发、性能优化、完整可观测性、复杂扩展性和容灾能力可以按风险和阶段后续迭代;但基础安全底线必须从第一版保留,包括 Secret 管理、基础权限边界、输入校验、敏感日志控制、数据隔离和错误响应脱敏。
|
||||
|
||||
### AI as Team Member
|
||||
|
||||
AI 不是单纯的代码生成器,而是项目成员。它可以参与需求分析、产品设计、架构设计、开发、Review 和维护。
|
||||
@@ -210,12 +216,24 @@ Idea
|
||||
|
||||
Documentation Update 属于 Definition of Done,不能跳过。
|
||||
|
||||
### 8.1 Definition of Ready
|
||||
### 8.1 Version Scope and Progressive Hardening
|
||||
|
||||
每个 Feature 或产品版本应显式区分:
|
||||
|
||||
- 第一版必须完成的功能闭环。
|
||||
- 第一版必须保留的基础安全底线。
|
||||
- 本阶段明确不做的高级安全、高并发、性能、可观测性、扩展性或容灾能力。
|
||||
- 为后续迭代保留的低耦合边界和扩展点。
|
||||
|
||||
中文说明:第一版可以不追求完整平台化和高负载能力,但不能用“后续再做”为理由破坏基本安全、数据边界或模块职责。
|
||||
|
||||
### 8.2 Definition of Ready
|
||||
|
||||
进入实现前,复杂 Feature 或会影响跨模块协作的变更必须满足:
|
||||
|
||||
- 需求来源明确,知道是谁提出、解决什么问题。
|
||||
- 目标和非目标明确,避免实现时扩大范围。
|
||||
- 第一版范围、基础安全底线和后续加固项已经区分。
|
||||
- 受影响的用户、业务流程、接口、数据模型、权限、安全、审计或外部系统边界已经列出。
|
||||
- 前端、后端、测试或第三方系统的分工已经明确。
|
||||
- 验收标准已经可测试,至少有主要 Given / When / Then 场景。
|
||||
@@ -223,7 +241,7 @@ Documentation Update 属于 Definition of Done,不能跳过。
|
||||
|
||||
简单 bugfix 或纯内部重构可以不用新建完整 Spec,但仍应在任务说明中写清目标、范围和验证方式。
|
||||
|
||||
### 8.2 Definition of Done
|
||||
### 8.3 Definition of Done
|
||||
|
||||
Feature 完成前必须确认:
|
||||
|
||||
@@ -233,7 +251,7 @@ Feature 完成前必须确认:
|
||||
- 对外或跨团队契约发生变化时,调用方文档和测试说明已同步。
|
||||
- 没有把 Secret、真实客户数据、构建产物、临时文件或无关本地变更纳入交付。
|
||||
|
||||
### 8.3 Change Request
|
||||
### 8.4 Change Request
|
||||
|
||||
当一个已 Approved 或 Implemented 的 Spec 发生需求变更时,优先新增或更新 Change Request,而不是把讨论散落到聊天记录中。
|
||||
|
||||
@@ -249,7 +267,7 @@ Change Request 至少说明:
|
||||
|
||||
小变更可以直接追加到原 Spec 的“变更记录”章节;跨前后端、权限、安全、数据模型或第三方契约的变更应单独成文。
|
||||
|
||||
### 8.4 Traceability Matrix
|
||||
### 8.5 Traceability Matrix
|
||||
|
||||
复杂 Feature 应维护需求追踪表,用于连接需求、实现、测试和文档。
|
||||
|
||||
@@ -261,7 +279,7 @@ Change Request 至少说明:
|
||||
|
||||
追踪表可以放在 Spec、Project State 或专项 checkpoint 文档中;关键是让新 Agent 能快速判断“文档写了但代码没做、代码做了但文档没写、后端做了但前端没做、前端需要但后端没做”。
|
||||
|
||||
### 8.5 Agent Handoff
|
||||
### 8.6 Agent Handoff
|
||||
|
||||
给 AI Agent 分派任务时,建议使用统一交接结构:
|
||||
|
||||
@@ -281,6 +299,7 @@ Change Request 至少说明:
|
||||
- 复杂功能默认先 Spec,不直接实现。
|
||||
- Bug 默认先定位,不猜测修复。
|
||||
- UI 默认保持一致性,不过度设计。
|
||||
- 第一版默认先实现可验证的功能闭环,避免提前设计复杂平台化能力;高级安全和高并发能力可以后续迭代,但基础安全底线必须从第一版保留。
|
||||
- 代码修改前先确认目标、边界和验收标准。
|
||||
- 涉及接口、安全、权限、数据模型或外部系统时,先读相关契约文档。
|
||||
- 涉及跨 agent 协作时,先确认 Spec、Change Request 或 handoff 是否足够清楚。
|
||||
|
||||
@@ -22,54 +22,64 @@
|
||||
- 不做事项 1:
|
||||
- 不做事项 2:
|
||||
|
||||
## 4. 用户与场景
|
||||
## 4. 第一版范围与后续加固
|
||||
|
||||
- 第一版必须完成的功能闭环:
|
||||
- 第一版必须保留的基础安全底线:Secret 管理 / 基础权限边界 / 输入校验 / 敏感日志控制 / 数据隔离 / 错误响应脱敏
|
||||
- 本阶段后置的高级安全、高并发、性能、完整可观测性、复杂扩展性或容灾能力:
|
||||
- 为后续迭代保留的低耦合边界和扩展点:
|
||||
|
||||
## 5. 用户与场景
|
||||
|
||||
说明谁会使用这个能力,在哪些场景使用。
|
||||
|
||||
## 5. Definition of Ready
|
||||
## 6. Definition of Ready
|
||||
|
||||
- 需求来源已确认:
|
||||
- 目标和非目标已确认:
|
||||
- 第一版范围、基础安全底线和后续加固项已确认:
|
||||
- 影响范围已确认:
|
||||
- 权限、安全、审计和数据边界已确认:
|
||||
- 前后端 / 测试分工已确认:
|
||||
- 未确认问题已列出:
|
||||
|
||||
## 6. 业务规则
|
||||
## 7. 业务规则
|
||||
|
||||
- 规则 1:
|
||||
- 规则 2:
|
||||
|
||||
## 7. 接口或交互契约
|
||||
## 8. 接口或交互契约
|
||||
|
||||
说明请求、响应、权限、安全、审计和兼容性要求。
|
||||
|
||||
## 8. 需求追踪表
|
||||
## 9. 需求追踪表
|
||||
|
||||
| 需求项 | 后端状态 | 前端状态 | 测试状态 | 文档位置 | 当前状态 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 需求 1 | Pending / Done / N/A | Pending / Done / N/A | Pending / Done / Blocked | | Draft / Approved / Implemented |
|
||||
|
||||
## 9. 验收标准
|
||||
## 10. 验收标准
|
||||
|
||||
- Given / When / Then:
|
||||
- Given / When / Then:
|
||||
|
||||
## 10. 测试范围
|
||||
## 11. 测试范围
|
||||
|
||||
- 单元测试:
|
||||
- 集成测试:
|
||||
- 手工验证:
|
||||
- 本阶段未覆盖的高并发、安全加固、性能或可观测性验证:
|
||||
|
||||
## 11. Definition of Done
|
||||
## 12. Definition of Done
|
||||
|
||||
- 实现满足 Spec:
|
||||
- 测试已运行或说明无法运行原因:
|
||||
- 需求追踪表已更新:
|
||||
- Project State 已更新:
|
||||
- 接口、安全、权限、审计、集成契约已同步:
|
||||
- 后续加固项已记录,不被误标为已完成:
|
||||
- 无 Secret、真实数据、构建产物或无关本地变更:
|
||||
|
||||
## 12. 文档更新
|
||||
## 13. 文档更新
|
||||
|
||||
完成后检查 Domain、Architecture、Workflow、ADR、Project State 是否需要更新。
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
- 遇到不确定的技术栈、目录、接口契约或数据模型,先确认再继续。
|
||||
- 新项目初始化或技术栈升级前,必须检查前端、后端、构建工具、测试工具和运行时版本兼容性。
|
||||
- 输出分层结构、目录树、数据模型、字段映射或接口示例时,必须补充中文说明,不能只依赖英文命名表达业务含义。
|
||||
- 软件开发默认从简单到复杂;第一版优先实现可运行、可验证的功能闭环,不为假想未来提前引入复杂架构或大而全抽象。
|
||||
- 高级安全、高并发、性能、完整可观测性和复杂扩展性可以后续迭代;基础安全底线必须从第一版保留,包括 Secret 管理、基础权限边界、输入校验、敏感日志控制、数据隔离和错误响应脱敏。
|
||||
- 如果把某些加固能力后置,必须在 Spec、Project State 或任务输出中明确记录,不能让后续 agent 误判为已经完成。
|
||||
|
||||
## 3. 分支与提交
|
||||
|
||||
@@ -57,6 +60,8 @@
|
||||
|
||||
项目各功能模块必须保持低耦合、高内聚和高可维护性。新增功能时,应先明确模块边界、输入输出、依赖方向和验收标准,再开始实现。
|
||||
|
||||
第一版实现应保持结构清晰但不过度设计:只抽取已经有真实重复或稳定语义的公共能力,不为了“未来可能需要”提前建立复杂框架。后续扩展点应通过清晰模块边界、接口契约和测试保护,而不是通过难以理解的通用模型堆叠。
|
||||
|
||||
落地要求:
|
||||
|
||||
- 每个模块只负责一个清晰业务能力。
|
||||
@@ -86,7 +91,7 @@
|
||||
- 外部系统通过 Port / Adapter 隔离,业务层不直接依赖厂商 SDK 或外部 DTO。
|
||||
- 数据库变更必须可追踪;已发布 migration 不直接修改。
|
||||
- 新项目建表前必须明确数据库字符集和 collation 策略;MySQL 项目默认建议使用 `utf8mb4_bin`,避免外部 ID、Token、哈希、状态码或业务代码因大小写不敏感而误判。
|
||||
- 写接口要考虑幂等、并发版本、审计、失败恢复和脱敏。
|
||||
- 第一版写接口必须保留输入校验、权限边界、错误脱敏和 Secret 不泄露;幂等、并发版本、审计、失败恢复等能力按业务风险决定当前实现深度,并在后置项中记录未完成边界。
|
||||
|
||||
Java 后端代码默认参考 Alibaba Java Coding Guidelines。可复用摘要见 `docs/import/reusable/alibaba-java-coding-guidelines-summary.md`。
|
||||
|
||||
@@ -124,6 +129,7 @@ Java 后端代码默认参考 Alibaba Java Coding Guidelines。可复用摘要
|
||||
- 不假装测试通过。
|
||||
- 优先做小的可验证闭环,再逐步扩展业务能力。
|
||||
- 高风险改动需要补充更接近真实使用路径的测试。
|
||||
- 如果本版本只覆盖功能闭环,必须说明尚未覆盖的高并发、安全加固、性能或可观测性验证范围。
|
||||
|
||||
## 12. Agent 协作规则
|
||||
|
||||
|
||||
@@ -448,7 +448,7 @@ M009 后端 CP2 已实现:页面可不依赖订单或任务,用户手工填
|
||||
- 从任务进入时,后续可以使用 `GET /api/reservation/tasks/{taskId}` 的 `fields[]`、草稿或确认 payload 预填;从订单进入时,需要明确选择具体任务或提示仅使用订单摘要,避免一个订单多任务时字段来源不清。
|
||||
- Company、Attention、Address、Tel、Email、Booking Date 第一阶段可以按“选择 + Manual 手填”控件处理。
|
||||
- Company、Attention、Address、Tel、Email 是一组收件方联系人档案,不是五个互相独立字段;选择 Company 后应刷新 Attention 候选,选择 Attention 后应带出 Address、Tel、Email。Booking Date 展示在同一区域,但不属于联系人档案,应作为 `document.booking_date` 独立提交。
|
||||
- 第一阶段已确认七组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`TEIGER_TRAVEL` / `TEIGER TRAVEL GROUP CO.,LTD` + `Khun Nutkritta Upamai (Kwan)`;`CLOVER_HOLIDAY` / `CLOVER HOLIDAY` + `KHUN JANE`;`KANGLER_TRAVEL` / `KANGLER TRAVEL GROUP CO.,LTD` + `Khun Dow`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。
|
||||
- 第一阶段已确认八组客户 / 旅行社种子数据:`LIAN_TAI` / `LIAN TAI TRAVEL (THAILAND) CO., LTD.` + `Khun Ann`;`QBD` / `Q.B.D. TRAVEL GROUP CO., LTD` + `Jitdanun Panaphuchong`;`MING_SONG` / `MING SONG CO.LTD` + `KHUN JIN`;`TEIGER_TRAVEL` / `TEIGER TRAVEL GROUP CO.,LTD` + `Khun Nutkritta Upamai (Kwan)`;`CLOVER_HOLIDAY` / `CLOVER HOLIDAY` + `KHUN JANE`;`KANGLER_TRAVEL` / `KANGLER TRAVEL GROUP CO.,LTD` + `Khun Dow`;`CENTURY_WAN_CHENG` / `Century Wan Cheng Tourism Co., Ltd.` + `Khun Nid`;`HANATOUR` / `HANATOUR TD CO., LTD.` + 7 个联系人。另保留 Manual 手工填写选项;完整数据以 `docs/project/requirements/M009-manual-invoice-generation-v1.md` 为准。
|
||||
- Room Type、Room Rate、Extra Bed 建议也预留“选择 + 可手填 / 可覆盖”能力,后续接 PMS 房型、Rate Code 或价格配置。
|
||||
- Room Rate / 费用明细 Rate 第一阶段只校验必填、数字格式和非负数,允许 `0`;Quantity 和 Nights 仍必须大于 `0`。
|
||||
- No.of Night(s) 前端默认按 `Departure Date - Arrival Date` 自动填入第一条费用明细 Night(s),用户手工修改后保留用户输入,不再因日期变化覆盖。
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
- `Address` 可在公司级和联系人级之间复用;如果同一公司多个联系人共用地址,不应在每个联系人手工重复维护多份无关数据。
|
||||
- 用户选择 `Manual` 时,才允许脱离目录手工填写;提交时仍保存最终文本值,目录 code 仅用于审计和后续追溯。
|
||||
|
||||
第一阶段确认的种子数据:
|
||||
第一阶段确认的种子数据(当前共八家公司,另保留 Manual 手工填写选项):
|
||||
|
||||
| company_code | Company | contact_id | Attention | Address | Tel | Email |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
@@ -117,6 +117,7 @@
|
||||
| `TEIGER_TRAVEL` | `TEIGER TRAVEL GROUP CO.,LTD` | `TEIGER_TRAVEL_KHUN_NUTKRITTA_UPAMAI` | `Khun Nutkritta Upamai (Kwan)` | `20,22 Soi Rinrada,Srinakarin, Pattanakarn,Bangkok,TH, 10250` | `063-516-8294` | `nutkritta.upamai@gmail.com` |
|
||||
| `CLOVER_HOLIDAY` | `CLOVER HOLIDAY` | `CLOVER_HOLIDAY_KHUN_JANE` | `KHUN JANE` | `290 SOI Lat Phrao 94 (Panjamit), Phlapphla Subdistrict, 555,555/1 Moo 1, Na Jomtien, Sattahip` | `083-1470352` | `longtai.maleemalee@gmail.com` |
|
||||
| `KANGLER_TRAVEL` | `KANGLER TRAVEL GROUP CO.,LTD` | `KANGLER_TRAVEL_KHUN_DOW` | `Khun Dow` | `เลขที่290/27, Soi Ladprao 84, Wangthonglang,Bangkok,10310` | `081-1237170` | `kanglertravel.op91@gmail.com` |
|
||||
| `CENTURY_WAN_CHENG` | `Century Wan Cheng Tourism Co., Ltd.` | `CENTURY_WAN_CHENG_KHUN_NID` | `Khun Nid` | `622 Soi Ratchada Niwet, Intersection 20, Huai Khwang District, Bangkok 10310` | `081-736-9808` | `meojiutravel.hotel@gmail.com` |
|
||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_WICHIENPRAKARN` | `Wichienprakarn, Nuanphae, Khun.` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `066 124 - 7297` | `HI219@hanatour.com` |
|
||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_PHONGBUPPA` | `Phongbuppa, Buppachat` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `096 051 3587` | `HI223@hanatour.com` |
|
||||
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_KANG_SUNG_HWA` | `Kang,Sung Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 051 804 0707` | `k8040707@nave.com` |
|
||||
@@ -130,6 +131,7 @@
|
||||
- 以上数据第一阶段可以作为后端种子配置、数据库初始化数据或前端原型静态数据,但正式业务接口生成 PDF 时应以请求中的最终文本值为准。
|
||||
- 后续如果接客户 / 联系人维护后台,`company_code` 和 `contact_id` 应保持稳定,不使用 Company 或 Attention 展示文案做业务主键。
|
||||
- Booking Date 虽然展示在 Basic Information 区域,但它不是联系人档案的一部分,应独立保存在 `invoice_payload.document.booking_date`。
|
||||
- 2026-09-09 按用户提供的样张新增 `CENTURY_WAN_CHENG`,沿用选择公司后自动回填联系人资料、允许人工覆盖的流程。样张中的客户 Tax ID 为 `0105568097215`,当前 Recipient 请求结构未提供客户税号字段,先作为资料记录,不写入酒店 Tax ID;样张中的 Booking Date / Booking No. 不作为公司默认值。本次仅扩充现有公司候选,未引入新 UI 或设计 skill;手工开票页面和服务的已有 30 项测试、前端类型检查、构建及页面 lint 已通过。Domain、Architecture、Workflow、ADR 和安全边界文档无需变更。
|
||||
|
||||
### 4.2 Reservation
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -46,6 +48,7 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoiceGenerationService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReservationInvoiceGenerationServiceImpl.class);
|
||||
private static final String DEFAULT_TEMPLATE_CODE = "PROFORMA_INVOICE_V1";
|
||||
private static final String TEMPLATE_VERSION = "v1";
|
||||
private static final String CURRENCY = "THB";
|
||||
@@ -137,6 +140,7 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi
|
||||
exception.getErrorCode(),
|
||||
safeSummary(exception.getMessage()),
|
||||
nowUtc());
|
||||
logInvoiceGenerationFailure(generationId, hotelId, exception.getErrorCode(), exception.getMessage(), exception);
|
||||
throw exception;
|
||||
} catch (DocumentConversionException exception) {
|
||||
invoiceGenerationRepository.markFailed(
|
||||
@@ -144,6 +148,7 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi
|
||||
exception.getErrorCode(),
|
||||
safeSummary(exception.getMessage()),
|
||||
nowUtc());
|
||||
logInvoiceGenerationFailure(generationId, hotelId, exception.getErrorCode(), exception.getMessage(), exception);
|
||||
throw new ReservationInvoiceGenerationException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
@@ -154,6 +159,12 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi
|
||||
"RESERVATION_INVOICE_GENERATION_FAILED",
|
||||
safeSummary(exception.getMessage()),
|
||||
nowUtc());
|
||||
logInvoiceGenerationFailure(
|
||||
generationId,
|
||||
hotelId,
|
||||
"RESERVATION_INVOICE_GENERATION_FAILED",
|
||||
exception.getMessage(),
|
||||
exception);
|
||||
throw new ReservationInvoiceGenerationException(
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
"RESERVATION_INVOICE_GENERATION_FAILED",
|
||||
@@ -528,6 +539,23 @@ public class ReservationInvoiceGenerationServiceImpl implements ReservationInvoi
|
||||
return normalized.length() <= 500 ? normalized : normalized.substring(0, 500);
|
||||
}
|
||||
|
||||
private void logInvoiceGenerationFailure(
|
||||
Long generationId,
|
||||
String hotelId,
|
||||
String safeErrorCode,
|
||||
String message,
|
||||
RuntimeException exception) {
|
||||
log.warn(
|
||||
"Reservation Invoice generation failed. generation_id={}, hotel_id={}, safe_error_code={}, "
|
||||
+ "safe_error_summary={}, exception_class={}",
|
||||
generationId,
|
||||
hotelId,
|
||||
safeErrorCode,
|
||||
safeSummary(message),
|
||||
exception.getClass().getSimpleName(),
|
||||
exception);
|
||||
}
|
||||
|
||||
private String safeFileName(String fileName, String fallback) {
|
||||
String value = trimToNull(fileName);
|
||||
if (value == null) {
|
||||
|
||||
Reference in New Issue
Block a user