24 KiB
柜台预存持久回显与缴费历史修复实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 修复柜台预存只在成功后临时可见的问题,使重新查询能够从支付记录恢复最近有效预存,并让客户缴费历史完整分页且为新预存展示可靠的期初、期末余额。
Architecture: 后端以 biz_payment_record 作为历史主数据源,取消“仅最新一条”截断,增加 bizScene 精确过滤;新柜台预存先创建支付主单,再把支付主单 ID 写到账户流水 payDetailId,查询层批量映射余额快照。前端在无待缴账单时调用持久化支付记录接口恢复最近预存,成功后的即时展示也使用真实支付主单 ID。
Tech Stack: Java 17、Spring Boot、MyBatis-Plus、JUnit 5、Mockito、Vue 3、TypeScript、Element Plus、Node.js node:test、pnpm
文件结构
后端修改:
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java:增加可选业务场景过滤字段。sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java:返回完整有效历史、应用场景过滤、批量读取账户流水并映射余额。sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java:调整柜台预存事务内顺序,并把支付主单 ID 写入账户流水。sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java:覆盖完整历史、红冲过滤、场景过滤、余额映射和导出。sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java:覆盖支付主单先创建及账户流水关联。
前端修改:
src/api/operatingCharges/counterCharging/index.ts:增加最近有效预存查询类型与 API。src/views/operatingCharges/counterCharging/index.vue:从持久化记录恢复预存展示行,使用真实支付主单 ID,并确保历史行不进入收费选择。tests/operatingCharges/counterChargingPersistentTopup.test.mjs:验证接口参数和页面恢复契约。
验证证据:
docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md:记录命令、退出码、通过数量和未覆盖边界。
Task 1: 后端缴费历史返回全部有效记录
Files:
-
Modify:
water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java -
Modify:
water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java -
Modify:
water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java -
Step 1: 修改现有测试,要求返回全部有效记录
将 getPaymentRecordPageNew_shouldOnlyExposeLatestVisiblePaymentRecord 改为:
@Test
void getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords() {
// 保留 latestReversed、latestVisible、olderVisible 三条测试数据和既有依赖桩。
when(paymentRecordMapper.selectList(any()))
.thenReturn(List.of(latestReversed, latestVisible, olderVisible));
when(paymentRecordDetailMapper.selectByPaymentRecordIds(List.of(91L, 92L)))
.thenReturn(List.of(detail));
PaymentRecordPageNewReqVO reqVO = new PaymentRecordPageNewReqVO();
reqVO.setCustId(703L);
reqVO.setSkipCount(0);
reqVO.setMaxResultCount(10);
var result = paymentQueryService.getPaymentRecordPageNew(reqVO);
assertEquals(2L, result.getPaySubtotalPageListDto().getTotalCount());
assertEquals(List.of(91L, 92L), result.getPaySubtotalPageListDto().getItems().stream()
.map(PaymentRecordPageNewRespVO.PaySubtotalItem::getId)
.toList());
assertEquals(new BigDecimal("47.60"), result.getTotalAmount());
}
- Step 2: 运行单测并确认按旧行为失败
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldExposeAllVisiblePaymentRecords \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: FAIL,totalCount 实际为 1 而期望为 2。
- Step 3: 实现完整候选记录返回
在请求 VO 增加:
@Schema(description = "业务场景", example = "DEPOSIT_TOPUP")
private String bizScene;
将查询方法统一改名为 selectVisiblePaymentRecordCandidates,并实现:
private List<PaymentRecordDO> selectVisiblePaymentRecordCandidates(PaymentRecordPageNewReqVO pageReqVO) {
return paymentRecordMapper.selectList(new LambdaQueryWrapperX<PaymentRecordDO>()
.eqIfPresent(PaymentRecordDO::getCustId, pageReqVO.getCustId())
.eqIfPresent(PaymentRecordDO::getBizScene, pageReqVO.getBizScene())
.geIfPresent(PaymentRecordDO::getPayTime, pageReqVO.getSTime())
.leIfPresent(PaymentRecordDO::getPayTime, pageReqVO.getETime())
.orderByDesc(PaymentRecordDO::getPayTime, PaymentRecordDO::getId))
.stream()
.filter(this::isVisiblePaymentRecord)
.filter(record -> pageReqVO.getBizScene() == null
|| Objects.equals(pageReqVO.getBizScene(), record.getBizScene()))
.toList();
}
getPaymentRecordPageNew() 和 getPaymentRecordExportList() 均调用新方法,不再执行 findFirst()。
- Step 4: 运行测试确认变绿
Run: 与 Step 2 相同。
Expected: PASS。
- Step 5: 增加场景过滤和导出回归测试
新增测试:
@Test
void getPaymentRecordPageNew_shouldFilterByBizScene() {
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
PaymentRecordDO charge = buildVisibleRecord(102L, "CHARGE_PAYMENT", new BigDecimal("20.00"));
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup, charge));
PaymentRecordPageNewReqVO reqVO = new PaymentRecordPageNewReqVO();
reqVO.setCustId(703L);
reqVO.setBizScene("DEPOSIT_TOPUP");
var result = paymentQueryService.getPaymentRecordPageNew(reqVO);
assertEquals(1L, result.getPaySubtotalPageListDto().getTotalCount());
assertEquals(101L, result.getPaySubtotalPageListDto().getItems().get(0).getId());
assertEquals(1, result.getTopUpCount());
assertEquals(new BigDecimal("5.00"), result.getTopUpTotalMoney());
}
@Test
void getPaymentRecordExportList_shouldExportAllVisibleRecords() {
PaymentRecordDO first = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
PaymentRecordDO second = buildVisibleRecord(102L, "CHARGE_PAYMENT", new BigDecimal("20.00"));
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(first, second));
var result = paymentQueryService.getPaymentRecordExportList(new PaymentRecordPageNewReqVO());
assertEquals(List.of(new BigDecimal("5.00"), new BigDecimal("20.00")), result.stream()
.map(PaymentRecordExportRespVO::getActualMoney)
.toList());
}
private PaymentRecordDO buildVisibleRecord(Long id, String bizScene, BigDecimal amount) {
return PaymentRecordDO.builder()
.id(id)
.custId(703L)
.custCode("C001")
.custName("张三")
.paymentAmount(amount)
.billAmount(amount)
.lateFeeAmount(BigDecimal.ZERO)
.chargeMethod(1)
.chargeWay(1)
.cashierId("1001")
.payTime(LocalDateTime.of(2026, 5, 13, 10, 0).minusMinutes(id))
.bizScene(bizScene)
.payInOut("IN")
.settleStatus(PaymentSettleStatusEnum.UNSETTLED.getCode())
.build();
}
- Step 6: 运行完整查询服务单测
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=PaymentQueryServiceTest \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: 所有 PaymentQueryServiceTest 测试通过。
- Step 7: 提交完整历史查询修复
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/PaymentRecordPageNewReqVO.java \
sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java
git commit -m "fix: return complete payment history"
Task 2: 新预存支付主单与账户流水建立可靠关联
Files:
-
Modify:
water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java -
Modify:
water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java -
Step 1: 先把柜台预存测试改为验证顺序和来源 ID
在 counterTopup_shouldIncreaseDepositAndCapturePaymentRecord 中增加:
InOrder inOrder = inOrder(paymentCommandApplicationService, accountService);
inOrder.verify(paymentCommandApplicationService).captureCounterTopup(
cust, new BigDecimal("100.00"), reqVO.getPayTime(), 1, "1001", "柜台现金预存");
ArgumentCaptor<AccountLogContext> contextCaptor = ArgumentCaptor.forClass(AccountLogContext.class);
inOrder.verify(accountService).increaseDeposit(
eq(910001L), eq(new BigDecimal("100.00")), contextCaptor.capture());
assertEquals(7001L, contextCaptor.getValue().getPayDetailId());
- Step 2: 运行测试并确认旧顺序失败
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=ChargeServiceCounterPaymentTest#counterTopup_shouldIncreaseDepositAndCapturePaymentRecord \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: FAIL,旧实现先调用 increaseDeposit(),且 payDetailId 为空。
- Step 3: 调整事务内执行顺序
counterTopup() 改为:
PaymentRecordDO paymentRecord = paymentCommandApplicationService.captureCounterTopup(
cust,
reqVO.getAmount(),
reqVO.getPayTime(),
reqVO.getChargeWay(),
reqVO.getCashierId(),
reqVO.getRemark()
);
AccountDO account = accountService.increaseDeposit(mainCustId, reqVO.getAmount(),
AccountLogContext.builder()
.accLogType(accLogType)
.accInOut(1)
.payDetailId(paymentRecord.getId())
.relatedCustId(isTransfer ? reqVO.getCustId() : null)
.remark(logRemark)
.build());
保持方法级 @Transactional(rollbackFor = Exception.class),返回结构不变。
- Step 4: 运行测试确认变绿
Run: 与 Step 2 相同。
Expected: PASS。
- Step 5: 提交预存账户流水关联修复
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceCounterPaymentTest.java
git commit -m "fix: link counter topup to account log"
Task 3: 缴费历史映射预存期初和期末余额
Files:
-
Modify:
water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java -
Modify:
water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java -
Step 1: 新增余额映射失败测试
给测试类增加 @Mock AccountLogMapper accountLogMapper,新增:
@Test
void getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog() {
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
AccountLogDO accountLog = AccountLogDO.builder()
.id(201L)
.payDetailId(101L)
.accLogType(2)
.accInOut(1)
.balanceBefore(new BigDecimal("10.00"))
.balanceAfter(new BigDecimal("15.00"))
.build();
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup));
when(accountLogMapper.selectList(any())).thenReturn(List.of(accountLog));
var result = paymentQueryService.getPaymentRecordPageNew(new PaymentRecordPageNewReqVO());
var item = result.getPaySubtotalPageListDto().getItems().get(0);
assertEquals(new BigDecimal("10.00"), item.getLastDeposit());
assertEquals(new BigDecimal("15.00"), item.getDeposit());
}
@Test
void getPaymentRecordPageNew_shouldLeaveBalanceEmptyWithoutReliableAccountLog() {
PaymentRecordDO topup = buildVisibleRecord(101L, "DEPOSIT_TOPUP", new BigDecimal("5.00"));
when(paymentRecordMapper.selectList(any())).thenReturn(List.of(topup));
when(accountLogMapper.selectList(any())).thenReturn(List.of());
var result = paymentQueryService.getPaymentRecordPageNew(new PaymentRecordPageNewReqVO());
var item = result.getPaySubtotalPageListDto().getItems().get(0);
assertNull(item.getLastDeposit());
assertNull(item.getDeposit());
}
- Step 2: 运行测试并确认余额尚未赋值
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=PaymentQueryServiceTest#getPaymentRecordPageNew_shouldMapTopupBalanceFromAccountLog \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: FAIL,lastDeposit 和 deposit 为 null。
- Step 3: 批量加载账户流水并写入查询上下文
在 PaymentQueryServiceImpl 注入 AccountLogMapper,增加:
private Map<Long, AccountLogDO> buildTopupAccountLogMap(List<PaymentRecordDO> records) {
List<Long> paymentRecordIds = records.stream()
.filter(record -> "DEPOSIT_TOPUP".equals(record.getBizScene()))
.map(PaymentRecordDO::getId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (paymentRecordIds.isEmpty()) {
return Collections.emptyMap();
}
return accountLogMapper.selectList(new LambdaQueryWrapperX<AccountLogDO>()
.in(AccountLogDO::getPayDetailId, paymentRecordIds)
.in(AccountLogDO::getAccLogType, List.of(2, 3))
.eq(AccountLogDO::getAccInOut, 1)
.orderByDesc(AccountLogDO::getId))
.stream()
.filter(log -> log.getPayDetailId() != null)
.collect(Collectors.toMap(
AccountLogDO::getPayDetailId,
Function.identity(),
(newer, older) -> newer,
LinkedHashMap::new));
}
将 PaymentQueryContext 增加 Map<Long, AccountLogDO> topupAccountLogMap,在 buildQueryContext() 中填充。toPaySubtotalItem() 和 toPaymentRecordExportResp() 增加 AccountLogDO accountLog 参数并设置:
item.setLastDeposit(accountLog != null ? accountLog.getBalanceBefore() : null);
item.setDeposit(accountLog != null ? accountLog.getBalanceAfter() : null);
导出 VO 同样映射 lastDeposit 和 deposit,不再用 unallocatedAmount 充当期末余额。
- Step 4: 运行余额测试和完整查询测试
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=PaymentQueryServiceTest \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: 所有查询测试通过,旧数据无账户流水关联时余额保持空值。
- Step 5: 提交余额快照查询修复
git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceImpl.java \
sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/paymentquery/PaymentQueryServiceTest.java
git commit -m "fix: expose topup balance snapshots"
Task 4: 前端从持久化支付记录恢复最近预存
Files:
-
Create:
water-frontend/tests/operatingCharges/counterChargingPersistentTopup.test.mjs -
Modify:
water-frontend/src/api/operatingCharges/counterCharging/index.ts -
Modify:
water-frontend/src/views/operatingCharges/counterCharging/index.vue -
Step 1: 新增前端失败契约测试
创建测试:
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const apiSource = readFileSync(resolve(process.cwd(), 'src/api/operatingCharges/counterCharging/index.ts'), 'utf8')
const pageSource = readFileSync(resolve(process.cwd(), 'src/views/operatingCharges/counterCharging/index.vue'), 'utf8')
test('counter charging queries the latest persisted deposit topup', () => {
assert.match(apiSource, /export const getLatestCounterTopup\s*=/)
assert.match(apiSource, /bizScene:\s*'DEPOSIT_TOPUP'/)
assert.match(apiSource, /skipCount:\s*0/)
assert.match(apiSource, /maxResultCount:\s*1/)
})
test('counter charging restores persisted topup only when no unpaid charges exist', () => {
assert.match(pageSource, /const loadLatestCounterTopupDisplay\s*=\s*async/)
assert.match(pageSource, /if\s*\(chargeList\.value\.length\s*===\s*0\)[\s\S]*?loadLatestCounterTopupDisplay\(custId\)/)
assert.match(pageSource, /paymentRecordId|record\.id/)
assert.doesNotMatch(pageSource, /localStorage|sessionStorage/)
})
- Step 2: 运行测试并确认接口和恢复逻辑缺失
Run:
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
Expected: FAIL,找不到 getLatestCounterTopup。
- Step 3: 增加最近预存 API
在 API 文件增加:
export interface CounterTopupPaymentRecordVO {
id: number
creationTime?: string
payDate?: string
actualMoney?: number
chargeWay?: number
payInOut?: number
lastDeposit?: number
deposit?: number
}
export const getLatestCounterTopup = async (custId: number) => {
return await request.get<{
paySubtotalPageListDto?: {
totalCount?: number
items?: CounterTopupPaymentRecordVO[]
}
}>({
url: '/business/charge/payment-record/page-new',
params: {
custId,
bizScene: 'DEPOSIT_TOPUP',
skipCount: 0,
maxResultCount: 1
}
})
}
- Step 4: 增加持久回显转换和加载逻辑
把 setSettledTopupDisplayRow 改为接受持久记录:
const setSettledTopupDisplayRow = (record: {
id: number
amount: number
payTime: string
chargeWay: number
}) => {
const current = currentCustomer.value
settledChargeDisplayRows.value = [{
id: record.id,
meterId: 0,
recordId: 0,
billMonth: '预存款',
custId: current?.id || 0,
custCode: current?.custCode,
custName: current?.custName,
billAmount: record.amount,
lateFee: 0,
extendedAmount: record.amount,
payState: 1,
payStateName: '收讫',
meterCode: '预存',
recordNo: '预存',
payTime: record.payTime,
chargeWay: record.chargeWay,
displayChargeState: 'settled',
displayChargeType: 'topup'
} as ChargeDisplayRowVO]
}
const loadLatestCounterTopupDisplay = async (custId: number) => {
try {
const result = await getLatestCounterTopup(custId)
const record = result?.paySubtotalPageListDto?.items?.[0]
if (!record?.id) return
setSettledTopupDisplayRow({
id: record.id,
amount: Number(record.actualMoney ?? 0),
payTime: record.payDate || record.creationTime || dayjs().format('YYYY-MM-DD HH:mm:ss'),
chargeWay: Number(record.chargeWay ?? 1)
})
} catch (error: any) {
ElMessage.warning(error?.message || '最近预存记录加载失败')
}
}
在 loadCustomerChargeData() 完成 chargeList 赋值后增加:
if (chargeList.value.length === 0) {
await loadLatestCounterTopupDisplay(custId)
}
预存提交时保存 submitCounterTopup() 返回值,成功后以真实 paymentRecordId 设置即时行;若刷新查询已经恢复同一记录,赋相同 ID 不产生重复行。
- Step 5: 保证历史回显不参与收费选择
保持收费计算和选中逻辑只使用 chargeList.value,展示使用 settledChargeDisplayRows。预存成功或重新查询后,不把历史行写入 chargeList;selectedChargeRows 在无欠费预存模式下保持空数组。
- Step 6: 运行前端契约测试
Run:
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
Expected: 2 个测试全部通过。
- Step 7: 提交前端持久回显修复
git add src/api/operatingCharges/counterCharging/index.ts \
src/views/operatingCharges/counterCharging/index.vue \
tests/operatingCharges/counterChargingPersistentTopup.test.mjs
git commit -m "fix: restore persisted counter topup display"
Task 5: 回归验证和证据回写
Files:
-
Create:
water-docs/docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md -
Step 1: 运行后端相关测试集合
Run:
mvn -pl sw-business/sw-business-server -am \
-Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest \
-Dsurefire.failIfNoSpecifiedTests=false test
Expected: 退出码 0,无失败和错误。
- Step 2: 运行后端编译
Run:
mvn -pl sw-business/sw-business-server -am -DskipTests compile
Expected: BUILD SUCCESS。
- Step 3: 运行前端定向测试和既有柜台收费契约
Run:
node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs
pnpm test:counter-charging
node --test tests/operatingCharges/counterChargingRouteRestore.test.mjs
Expected: 新增测试和 test:counter-charging 通过;若既有 route restore 测试仍失败,记录其与本次修改是否相关,不隐藏结果。
- Step 4: 运行前端构建
Run:
pnpm build
Expected: 退出码 0;若存在既有警告,记录但不将警告表述为错误。
- Step 5: 写验证证据
证据文档必须记录:
# 柜台预存持久回显修复验证证据
- 验证日期:2026-07-14
- 后端基线:`06ed57830fe0ccd50f0a74e3e5cf02114b0ace45`
- 前端基线:`a0239b384f7e072c11c67ae6d405980260f4af14`
- 设计:`docs/superpowers/specs/2026-07-13-counter-topup-persistent-display-design.md`
## 验证结果
| 范围 | 命令 | 结果 | 说明 |
|---|---|---|---|
| 后端单测 | `mvn -pl sw-business/sw-business-server -am -Dtest=PaymentQueryServiceTest,ChargeServiceCounterPaymentTest,PaymentRecordServiceImplTest,CounterSettleApplicationServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test` | PASS/FAIL | 测试数、失败数 |
| 后端编译 | `mvn -pl sw-business/sw-business-server -am -DskipTests compile` | PASS/FAIL | `BUILD SUCCESS` 或错误摘要 |
| 前端契约 | `node --test tests/operatingCharges/counterChargingPersistentTopup.test.mjs`、`pnpm test:counter-charging` | PASS/FAIL | 测试数、失败数 |
| 前端构建 | `pnpm build` | PASS/FAIL | 构建结果 |
## 已知边界
- 旧预存支付记录没有可靠 `payDetailId` 时,期初、期末余额显示 `-`。
- 主副卡付款户解析和有效余额读取不在本次修复范围。
- Step 6: 最终差异检查
Run:
git -C water-backend diff --check
git -C water-frontend diff --check
git -C water-docs diff --check
git -C water-backend status --short
git -C water-frontend status --short
git -C water-docs status --short
Expected: 无空白错误;状态中仅包含本计划范围内的文件。
- Step 7: 提交验证证据
git add docs/evidence/bugfix/2026-07-14-counter-topup-persistent-display.md
git commit -m "docs: record counter topup display verification"