# Business Type History Rate Frontend Implementation Plan

> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add business type selection and historical loan-contract single-selection to the personal and corporate workflow creation UI, then verify the real page with browser-use.

**Architecture:** Reuse the current customer-map driven creation flow. Add one shared `HistoryContractSelector` component, one API function, fields and validation in both create dialogs, and detail display in both detail components. Final browser verification must use `browser-use:browser` with the in-app browser, not Playwright CLI or prototype pages.

**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, nvm Node 14.21.3, browser-use `iab` runtime for real page testing.

---

## File Structure

- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
  - Add `queryHistoryContracts(custIsn)`.
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
  - Shared modal for historical contract query results and single selection.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
  - Add business type, history-rate display, query trigger, selection handling, and submit validation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
  - Same behavior for corporate creation.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
  - Show business type and historical loan rate.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
  - Show business type and historical loan rate.
- Modify: `ruoyi-ui/package.json`
  - Add `test:business-type-history-rate`.
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
  - Static coverage for API, selector, create dialogs, validation, clearing, and detail display.
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
  - Add frontend and browser-use verification notes after execution.

## Task 1: Add Frontend API and Static Test Skeleton

**Files:**
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- Modify: `ruoyi-ui/package.json`
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`

- [ ] **Step 1: Write failing API assertions**

Create `ruoyi-ui/tests/business-type-history-rate.test.js`:

```js
const fs = require('fs')
const path = require('path')
const assert = require('assert')

function read(relativePath) {
  return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
}

const workflowApi = read('src/api/loanPricing/workflow.js')

assert(
  workflowApi.includes('export function queryHistoryContracts') &&
    workflowApi.includes("url: '/loanPricing/workflow/history-contract'") &&
    workflowApi.includes('params: { custIsn: custIsn }'),
  '缺少历史贷款合同查询 API'
)

console.log('business type history rate assertions passed')
```

Add package script:

```json
"test:business-type-history-rate": "node tests/business-type-history-rate.test.js"
```

- [ ] **Step 2: Run test to verify it fails**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: FAIL because the API function does not exist.

- [ ] **Step 3: Implement API function**

Append to `workflow.js`:

```js
// 查询历史贷款合同
export function queryHistoryContracts(custIsn) {
  return request({
    url: '/loanPricing/workflow/history-contract',
    method: 'get',
    params: { custIsn: custIsn }
  })
}
```

- [ ] **Step 4: Run API test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add ruoyi-ui/src/api/loanPricing/workflow.js ruoyi-ui/package.json ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同前端接口"
```

## Task 2: Create Historical Contract Selector

**Files:**
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`

- [ ] **Step 1: Extend failing selector assertions**

Add:

```js
const historySelector = read('src/views/loanPricing/workflow/components/HistoryContractSelector.vue')

assert(
  historySelector.includes('title="历史贷款合同选择"') &&
    historySelector.includes('width="80%"') &&
    historySelector.includes(':data="contracts"'),
  '历史贷款合同选择弹窗缺少标题、宽度或表格数据'
)

;[
  'cust_isn',
  'loan_contract_history',
  'guar_type_history',
  'product_code_history',
  'loan_rate_history',
  'loan_amount_history',
  'loan_sign_date_history'
].forEach((field) => {
  assert(historySelector.includes(`prop="${field}"`) || historySelector.includes(`row.${field}`), `历史合同弹窗缺少字段 ${field}`)
})

assert(
  historySelector.includes('type="radio"') &&
    historySelector.includes('selectedContract') &&
    historySelector.includes("this.$emit('select', this.selectedContract)") &&
    historySelector.includes('请选择历史贷款合同'),
  '历史合同弹窗缺少单选、确定选择或未选提示'
)
```

- [ ] **Step 2: Run test to verify it fails**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: FAIL because the component does not exist.

- [ ] **Step 3: Implement `HistoryContractSelector.vue`**

Use a small presentational component. It receives already-loaded `contracts` and does not call the API itself.

```vue
<template>
  <el-dialog title="历史贷款合同选择" :visible.sync="dialogVisible" width="80%" append-to-body
             :close-on-click-modal="false" @close="handleClose">
    <el-table :data="contracts" v-loading="loading" @row-click="handleRowClick">
      <el-table-column label="选择" align="center" width="70">
        <template slot-scope="scope">
          <el-radio v-model="selectedContract" :label="scope.row">&nbsp;</el-radio>
        </template>
      </el-table-column>
      <el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
      <el-table-column label="历史贷款合同号" prop="loan_contract_history" align="center" :show-overflow-tooltip="true"/>
      <el-table-column label="历史贷款担保方式" prop="guar_type_history" align="center"/>
      <el-table-column label="历史贷款产品代码" prop="product_code_history" align="center"/>
      <el-table-column label="历史贷款利率" prop="loan_rate_history" align="center"/>
      <el-table-column label="历史贷款金额" prop="loan_amount_history" align="center"/>
      <el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
    </el-table>
    <div slot="footer" class="dialog-footer">
      <el-button type="primary" @click="confirmSelect">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </div>
  </el-dialog>
</template>
```

Script:

```js
export default {
  name: "HistoryContractSelector",
  props: {
    visible: { type: Boolean, default: false },
    contracts: { type: Array, default: () => [] },
    loading: { type: Boolean, default: false }
  },
  data() {
    return { selectedContract: null }
  },
  computed: {
    dialogVisible: {
      get() { return this.visible },
      set(val) { this.$emit('update:visible', val) }
    }
  },
  methods: {
    handleRowClick(row) {
      this.selectedContract = row
    },
    confirmSelect() {
      if (!this.selectedContract) {
        this.$modal.msgWarning("请选择历史贷款合同")
        return
      }
      if (!this.selectedContract.loan_rate_history) {
        this.$modal.msgWarning("历史贷款利率不能为空")
        return
      }
      this.$emit('select', this.selectedContract)
      this.dialogVisible = false
    },
    handleClose() {
      this.selectedContract = null
    }
  }
}
```

- [ ] **Step 4: Run selector test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS for selector assertions.

- [ ] **Step 5: Commit**

```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue \
  ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "新增历史贷款合同选择弹窗"
```

## Task 3: Add Personal Create Dialog Behavior

**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`

- [ ] **Step 1: Extend failing personal dialog assertions**

Add:

```js
const personalCreate = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')

assert(
  personalCreate.includes('label="业务种类"') &&
    personalCreate.includes('v-model="form.businessType"') &&
    personalCreate.includes('新客') &&
    personalCreate.includes('存量新增') &&
    personalCreate.includes('存量转贷'),
  '个人新增弹窗缺少业务种类选择'
)

assert(
  personalCreate.includes('label="历史贷款利率"') &&
    personalCreate.includes('v-model="form.loanRateHistory"') &&
    personalCreate.includes(':readonly="true"'),
  '个人新增弹窗缺少只读历史贷款利率'
)

assert(
  personalCreate.includes('queryHistoryContracts') &&
    personalCreate.includes('HistoryContractSelector') &&
    personalCreate.includes('handleBusinessTypeChange') &&
    personalCreate.includes('handleHistoryContractSelect'),
  '个人新增弹窗缺少历史合同查询和选择逻辑'
)

assert(
  personalCreate.includes('请选择历史贷款合同') &&
    personalCreate.includes('未查询到历史贷款合同') &&
    personalCreate.includes('delete data.loanRateHistory'),
  '个人新增弹窗缺少存量转贷拦截、空列表提示或非存量转贷清理'
)
```

- [ ] **Step 2: Run test to verify it fails**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: FAIL.

- [ ] **Step 3: Implement personal form fields**

In the loan information area, add:

```vue
<el-row>
  <el-col :span="12">
    <el-form-item label="业务种类" prop="businessType">
      <el-select v-model="form.businessType" placeholder="请选择业务种类" style="width: 100%" @change="handleBusinessTypeChange">
        <el-option label="新客" value="新客"/>
        <el-option label="存量新增" value="存量新增"/>
        <el-option label="存量转贷" value="存量转贷"/>
      </el-select>
    </el-form-item>
  </el-col>
  <el-col :span="12" v-if="isStockTransfer">
    <el-form-item label="历史贷款利率" prop="loanRateHistory">
      <el-input v-model="form.loanRateHistory" placeholder="请选择历史贷款合同" :readonly="true"/>
    </el-form-item>
  </el-col>
</el-row>
```

Add selector under the form:

```vue
<history-contract-selector
  :visible.sync="showHistorySelector"
  :contracts="historyContracts"
  :loading="historyLoading"
  @select="handleHistoryContractSelect"
/>
```

- [ ] **Step 4: Implement personal script**

Import:

```js
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
import HistoryContractSelector from "./HistoryContractSelector"
```

Register component, then add state:

```js
businessTypeOptions: ['新客', '存量新增', '存量转贷'],
showHistorySelector: false,
historyLoading: false,
historyContracts: [],
selectedHistoryContract: null,
```

Add form fields in initial data and `reset()`:

```js
businessType: undefined,
loanRateHistory: undefined,
```

Add rules:

```js
businessType: [
  {required: true, message: "请选择业务种类", trigger: "change"}
],
loanRateHistory: [
  {required: true, message: "请选择历史贷款合同", trigger: "change"}
]
```

Add computed:

```js
isStockTransfer() {
  return this.form.businessType === '存量转贷'
}
```

Add methods:

```js
handleBusinessTypeChange(value) {
  this.clearHistoryContract()
  if (value === '存量转贷') {
    this.queryHistoryContracts()
  }
},
queryHistoryContracts() {
  if (!this.form.custIsn) {
    this.$modal.msgWarning("客户内码不能为空")
    return
  }
  this.historyLoading = true
  queryHistoryContracts(this.form.custIsn).then(response => {
    this.historyContracts = response.data || []
    if (this.historyContracts.length === 0) {
      this.$modal.msgWarning("未查询到历史贷款合同")
      return
    }
    this.showHistorySelector = true
  }).finally(() => {
    this.historyLoading = false
  })
},
handleHistoryContractSelect(row) {
  if (!row.loan_rate_history) {
    this.$modal.msgWarning("历史贷款利率不能为空")
    return
  }
  this.selectedHistoryContract = row
  this.form.loanRateHistory = row.loan_rate_history
},
clearHistoryContract() {
  this.selectedHistoryContract = null
  this.form.loanRateHistory = undefined
  this.historyContracts = []
}
```

In `submitForm`, before setting `submitting`:

```js
if (this.isStockTransfer && !this.form.loanRateHistory) {
  this.$modal.msgWarning("请选择历史贷款合同")
  return
}
```

When building `data`, remove historical rate for non-stock-transfer:

```js
if (!this.isStockTransfer) {
  delete data.loanRateHistory
}
```

- [ ] **Step 5: Run personal static test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS for personal assertions.

- [ ] **Step 6: Commit**

```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
  ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "个人新增支持业务种类和历史利率"
```

## Task 4: Add Corporate Create Dialog Behavior

**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`

- [ ] **Step 1: Extend failing corporate assertions**

Mirror the personal assertions for `CorporateCreateDialog.vue`, replacing assertion messages with “企业新增弹窗...”.

- [ ] **Step 2: Run test to verify it fails**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: FAIL.

- [ ] **Step 3: Implement corporate fields and selector**

Apply the same form, selector, state, rules, computed, and methods from Task 3, but keep existing corporate-specific fields and submission conversion intact.

- [ ] **Step 4: Keep collateral and green/trade behavior untouched**

When editing submit payload, keep:

```js
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
```

Do not reintroduce `repayMethod` UI.

- [ ] **Step 5: Run corporate static test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
  ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "企业新增支持业务种类和历史利率"
```

## Task 5: Add Detail Display

**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`

- [ ] **Step 1: Add failing detail assertions**

```js
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')

;[
  ['个人详情', personalDetail],
  ['企业详情', corporateDetail]
].forEach(([name, source]) => {
  assert(source.includes('label="业务种类"') && source.includes('detailData.businessType'), `${name} 缺少业务种类展示`)
  assert(source.includes('label="历史贷款利率"') && source.includes('detailData.loanRateHistory'), `${name} 缺少历史贷款利率展示`)
})
```

- [ ] **Step 2: Run test to verify it fails**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: FAIL.

- [ ] **Step 3: Add personal detail fields**

In personal “业务信息” descriptions, add:

```vue
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
```

- [ ] **Step 4: Add corporate detail fields**

Add the same two fields to corporate “业务信息” descriptions.

- [ ] **Step 5: Run detail test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue \
  ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue \
  ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "详情展示业务种类和历史利率"
```

## Task 6: Frontend Static Verification

**Files:**
- No new files unless tests expose a focused defect.

- [ ] **Step 1: Run new focused test**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
```

Expected: PASS.

- [ ] **Step 2: Run related existing frontend tests**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```

Expected: PASS.

- [ ] **Step 3: Build frontend**

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```

Expected: build succeeds.

- [ ] **Step 4: Commit only if fixes were required**

```bash
git status --short
```

Expected: no unexpected frontend changes beyond this plan.

## Task 7: Real Page Verification With browser-use

**Files:**
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`

- [ ] **Step 1: Start backend with latest code**

Start or restart the backend on port `63310` using the repo's existing scripts or Maven command. Confirm the backend has loaded the latest backend code before browser testing.

Example:

```bash
bin/restart_java_backend_test.sh
```

Expected: backend listens on `63310`.

- [ ] **Step 2: Start frontend with Node 14**

Use nvm-managed Node:

```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'
```

Expected: frontend dev server is available at `http://localhost:8080`.

- [ ] **Step 3: Initialize browser-use**

Use the Node REPL `js` tool and the in-app browser backend. First browser cell:

```js
if (!globalThis.agent) {
  const { setupAtlasRuntime } = await import("/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/scripts/browser-client.mjs");
  const backend = "iab";
  await setupAtlasRuntime({ globals: globalThis, backend });
}
await agent.browser.nameSession("🔎 利率定价历史利率测试");
if (typeof tab === "undefined") {
  globalThis.tab = await agent.browser.tabs.new();
}
await tab.goto("http://localhost:8080");
await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 });
console.log(await tab.title());
```

- [ ] **Step 4: Log in on the real page**

Use the real login page, not a prototype. If the default test login path is available, use the project-supported login route. Otherwise log in through the visible login form with the configured test account.

After login, verify that the actual workflow list is visible.

- [ ] **Step 5: Verify personal new customer path**

In the browser:

1. Open 利率定价流程 list.
2. Click 新增.
3. Select 个人客户.
4. Query a customer number and choose a customer internal code.
5. In the personal create dialog, choose 业务种类 = 新客.
6. Confirm no historical contract selector appears.
7. Fill required fields and submit.
8. Open detail and verify 业务种类 displays 新客 and 历史贷款利率 is empty/`-`.

- [ ] **Step 6: Verify personal existing-new path**

Repeat personal creation with 业务种类 = 存量新增. Confirm no historical contract selector appears, submit succeeds, and detail displays 存量新增.

- [ ] **Step 7: Verify personal stock-transfer path**

Repeat personal creation with 业务种类 = 存量转贷. Confirm:

1. Historical contract selector opens.
2. All 7 columns are visible.
3. A single radio selection is possible.
4. Selecting a row fills 历史贷款利率.
5. Submit succeeds.
6. Detail displays 业务种类 = 存量转贷 and the chosen 历史贷款利率.

- [ ] **Step 8: Verify corporate three paths**

Repeat Steps 5-7 for 企业客户: 新客、存量新增、存量转贷.

- [ ] **Step 9: Verify blocked stock-transfer submission**

Use a stock-transfer flow and attempt to submit without selecting a historical contract. Confirm the page blocks submission and shows “请选择历史贷款合同”.

- [ ] **Step 10: Verify empty history scenario**

Use the fixed customer-map mock customer number `HISTORY_EMPTY`. In the customer number selector, query `HISTORY_EMPTY`, select the returned row with `cust_isn=EMPTY_HISTORY`, then choose 业务种类 = 存量转贷. Confirm “未查询到历史贷款合同” appears and submission remains blocked.

- [ ] **Step 11: Verify empty historical-rate scenario**

Use the fixed customer-map mock customer number `HISTORY_EMPTY_RATE`. In the customer number selector, query `HISTORY_EMPTY_RATE`, select the returned row with `cust_isn=EMPTY_RATE`, then choose 业务种类 = 存量转贷. Confirm the history selector opens with a row whose `loan_rate_history` is empty; selecting it and clicking 确定 must show “历史贷款利率不能为空”, must not fill the create form's historical loan-rate field, and submission must remain blocked.

- [ ] **Step 12: Capture evidence**

Capture screenshots or DOM snapshots for:

- Personal stock-transfer selector with all 7 columns.
- Corporate stock-transfer selector with all 7 columns.
- Detail page showing 业务种类 and 历史贷款利率.
- Blocked submit warning.
- Empty-history warning from `HISTORY_EMPTY`.
- Empty-rate warning from `HISTORY_EMPTY_RATE`.

Use browser-use screenshots through `await display(await tab.playwright.screenshot({ fullPage: false }))` when visual confirmation matters.

- [ ] **Step 13: Stop test processes**

Stop only the backend/frontend processes started for this test. Do not kill unrelated user processes.

- [ ] **Step 14: Record verification**

Update:

```text
doc/implementation-report-2026-04-29-business-type-history-rate.md
```

Include:

- Static test commands and results.
- Backend/frontend server commands.
- browser-use URL and scenarios covered.
- Confirmation that test processes were stopped.

- [ ] **Step 15: Commit verification notes**

```bash
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
git commit -m "记录业务种类历史利率页面验证"
```
