# Revenue Bugfix Clear Scope Implementation Plan > **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:** Fix the first batch of clearly evidenced revenue bugs so retry, settlement, red-flush records, prepay deduction, and pending-approval UI states behave consistently. **Architecture:** Keep the current backend application services and frontend pages. Add narrow backend query/validation behavior where data is missing, and adjust frontend API wiring and status messages where the UI currently misrepresents backend state. Each task is independently testable and should be committed separately. **Tech Stack:** Java 17, Spring Boot, MyBatis Plus, JUnit 5, Mockito, Vue 3, TypeScript, Element Plus, node:test, Playwright. --- ## Source Spec Approved design: `docs/superpowers/specs/2026-06-08-revenue-bugfix-clear-scope-design.md` Implementation touches two sibling repositories: - Backend: `../water-backend` - Frontend: `../water-frontend` Start execution from isolated worktrees created from `develop`: ```bash cd /Volumes/Dpan/github/water-workspace git -C water-backend worktree add ../worktrees/backend-revenue-bugfix-clear-scope develop git -C water-frontend worktree add ../worktrees/frontend-revenue-bugfix-clear-scope develop ``` If either worktree already exists, reuse it after checking `git status --short`. ## File Structure Backend files: - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAO.java` - Replace lock refresh implementation with safe Redis/Redisson expiry extension. - Modify `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/pricetemplate/PriceTemplateServiceImplTest.java` - Add tests for failed update releasing lock and re-entry refresh behavior. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/paymentrecord/PaymentRecordMapper.java` - Include `DEPOSIT_TOPUP` in counter unsettled records. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java` - Support deposit-topup settlement rows without `chargeId`. - Use requested `cashierId` when provided. - Add red-flush record page query. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationService.java` - Add red-flush record page/export methods. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/ChargeController.java` - Add `/counter-settle/red-flush-record-page` endpoint and export endpoint. - Create `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordPageReqVO.java` - Query params for柜台红冲记录. - Create `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordRespVO.java` - Response row for柜台红冲记录. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterUnsettledPageRespVO.java` - Ensure null `chargeId/billMonth` is allowed in exported/list rows. - Modify `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java` - Validate `prepayDeductAmount` against account deposit. - Modify `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java` - Add settlement, cashier, and red-flush-record tests. - Add or modify `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImplCounterPrepayTest.java` - Test prepay deduction validation. Frontend files: - Modify `src/views/settings/price/priceTemplate/index.vue` - Retry `startAdjustment()` after failed submit. - Modify `src/views/accountProcess/unsoldAdjustment/components/SplitAdjustmentForm.vue` - Show pending-approval message from response. - Modify `src/views/accountProcess/unsoldAdjustment/components/BadDebtAdjustmentForm.vue` - Show pending-approval message from response and reduce immediate-effect wording. - Modify `src/views/accountProcess/unsoldAdjustment/components/PriceAdjustmentForm.vue` - Show pending-approval message from response. - Modify `src/views/accountProcess/unsoldAdjustment/components/PenaltyRemissionForm.vue` - Show pending-approval message from response. - Modify `src/views/operatingCharges/counterCheckout/components/CounterUnsettledPanel.vue` - Display deposit-topup rows with missing bill fields gracefully. - Modify `src/api/business/charge/counterSettle.ts` - Add red-flush record API types and methods. - Modify `src/api/operatingCharges/redReversalRecord/index.ts` - Point red-flush record page to counter-settle red-flush endpoints. - Modify `src/views/operatingCharges/redReversalRecord/index.vue` - Change query fields and columns to柜台红冲语义. - Modify `src/views/operatingCharges/counterCharging/index.vue` - Add prepay deduction zero/overflow guard before submit. - Add frontend contract tests under existing test style: - `tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs` Documentation: - After implementation and verification, append evidence to a new file: - `../water-docs/docs/evidence/revenue-bugfix-clear-scope-2026-06-08.md` --- ### Task 1: Backend Price Adjustment Lock Recovery **Files:** - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAO.java` - Modify: `../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/pricetemplate/PriceTemplateServiceImplTest.java` - [ ] **Step 1: Write failing test for re-entering price adjustment after failed update** Add this test to `PriceTemplateServiceImplTest.java`: ```java @Test void startPriceAdjustment_shouldRefreshCurrentUsersLockWhenReEnteringAfterFailure() { try (MockedStatic tenantContext = org.mockito.Mockito.mockStatic(TenantContextHolder.class); MockedStatic securityContext = org.mockito.Mockito.mockStatic(SecurityFrameworkUtils.class)) { tenantContext.when(TenantContextHolder::getTenantId).thenReturn(1L); securityContext.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1001L); when(priceTemplateAdjustmentLockRedisDAO.getLockUserInfo(1L)).thenReturn(1001L); when(priceTemplateAdjustmentLockRedisDAO.refreshLock( 1L, 1001L, PriceTemplateServiceImpl.PRICE_TEMPLATE_ADJUSTMENT_TIMEOUT_MILLIS )).thenReturn(true); priceTemplateService.startPriceAdjustment(); verify(priceTemplateAdjustmentLockRedisDAO).refreshLock( 1L, 1001L, PriceTemplateServiceImpl.PRICE_TEMPLATE_ADJUSTMENT_TIMEOUT_MILLIS ); } } ``` Add imports: ```java import cn.com.emsoft.sw.framework.security.core.util.SecurityFrameworkUtils; import cn.com.emsoft.sw.framework.tenant.core.context.TenantContextHolder; import org.mockito.MockedStatic; import static org.mockito.Mockito.verify; ``` - [ ] **Step 2: Run test to verify current baseline** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=PriceTemplateServiceImplTest test ``` Expected: PASS. This locks current expected frontend recovery entry point. - [ ] **Step 3: Add DAO unit test for refreshLock not calling forceUnlock** Create `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAOTest.java`: ```java package cn.com.emsoft.sw.business.dal.redis.pricetemplate; import org.junit.jupiter.api.Test; import java.nio.file.Files; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class PriceTemplateAdjustmentLockRedisDAOTest { @Test void refreshLock_shouldNotReleaseLockBeforeRefreshingTtl() throws Exception { String source = Files.readString(Path.of( "src/main/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAO.java")); int refreshStart = source.indexOf("public boolean refreshLock"); int nextMethod = source.indexOf("/**", refreshStart + 1); String refreshMethod = source.substring(refreshStart, nextMethod); assertFalse(refreshMethod.contains("forceUnlock()")); assertTrue(refreshMethod.contains("remainTimeToLive()")); assertTrue(refreshMethod.contains("expire(")); } } ``` - [ ] **Step 4: Run DAO test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope/sw-business/sw-business-server mvn -Dtest=PriceTemplateAdjustmentLockRedisDAOTest test ``` Expected: FAIL because `refreshLock()` still contains `forceUnlock()`. - [ ] **Step 5: Implement safe lock refresh** Replace `refreshLock()` in `PriceTemplateAdjustmentLockRedisDAO.java` with: ```java public boolean refreshLock(Long tenantId, Long userId, long timeoutMillis) { String lockKey = formatKey(tenantId); RLock lock = redissonClient.getLock(lockKey); if (!lock.isLocked()) { return false; } Long currentLockUserId = getLockUserInfo(tenantId); if (!userId.equals(currentLockUserId)) { log.warn("[调价锁] 刷新锁失败,当前用户ID: {}, 锁持有者ID: {}, 租户ID: {}", userId, currentLockUserId, tenantId); throw exception(PRICE_TEMPLATE_ADJUSTMENT_LOCK_NOT_OWNED_BY_USER, currentLockUserId); } long currentTtl = lock.remainTimeToLive(); if (currentTtl <= 0) { return false; } boolean refreshed = lock.expire(timeoutMillis, TimeUnit.MILLISECONDS); if (refreshed) { setLockUserInfo(tenantId, userId, timeoutMillis); log.info("[调价锁] 成功刷新锁过期时间,租户ID: {}, 用户ID: {}, 新超时: {}ms", tenantId, userId, timeoutMillis); } return refreshed; } ``` Replace `setLockUserInfo(Long tenantId, Long userId)` with: ```java private void setLockUserInfo(Long tenantId, Long userId) { setLockUserInfo(tenantId, userId, TimeUnit.MINUTES.toMillis(30)); } private void setLockUserInfo(Long tenantId, Long userId, long timeoutMillis) { String userKey = formatUserKey(tenantId); stringRedisTemplate.opsForValue().set(userKey, userId.toString(), timeoutMillis, TimeUnit.MILLISECONDS); } ``` - [ ] **Step 6: Run price lock tests** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=PriceTemplateServiceImplTest,PriceTemplateAdjustmentLockRedisDAOTest test ``` Expected: PASS. - [ ] **Step 7: Commit backend price-lock fix** ```bash git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAO.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/pricetemplate/PriceTemplateServiceImplTest.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/dal/redis/pricetemplate/PriceTemplateAdjustmentLockRedisDAOTest.java git commit -m "fix: make price adjustment lock refresh safe" ``` --- ### Task 2: Backend Counter Settlement Includes Deposit Topups and Respects Cashier Query **Files:** - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/paymentrecord/PaymentRecordMapper.java` - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java` - Modify: `../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java` - [ ] **Step 1: Rename outdated cashier test** In `CounterSettleApplicationServiceImplTest.java`, rename: ```java void confirm_shouldPreferAuthenticatedUserOverRequestCashierId() ``` to: ```java void confirm_shouldUseRequestCashierIdWhenProvided() ``` Change the mocked payment lookup and assertion to use request cashier: ```java when(paymentRecordService.getCounterUnsettledRecords("forged-cashier")).thenReturn(List.of(record)); ``` and: ```java verify(paymentRecordService).getCounterUnsettledRecords("forged-cashier"); assertEquals("forged-cashier", settleCaptor.getValue().getCashierId()); assertEquals("2002", detailCaptor.getValue().getProcPerson()); ``` Keep the static login user mock at `2002L`; this proves `cashierId` and operator are separated. - [ ] **Step 2: Add failing test for deposit topup settlement** Add this test to `CounterSettleApplicationServiceImplTest.java`: ```java @Test void confirm_shouldSettleDepositTopupWithoutChargeWriteBack() { LocalDateTime settleTime = LocalDateTime.of(2026, 6, 8, 10, 0); PaymentRecordDO topup = PaymentRecordDO.builder() .id(301L) .sourceRefId(9001L) .paymentNo("PAY-TOPUP-301") .custId(9001L) .custCode("CUST-TOPUP") .custName("预存客户") .paymentAmount(new BigDecimal("100.00")) .build(); when(paymentRecordService.getCounterUnsettledRecords("1001")).thenReturn(List.of(topup)); doAnswer(invocation -> { SettleRecordDO record = invocation.getArgument(0); record.setId(8001L); return 1; }).when(settleRecordMapper).insert(any(SettleRecordDO.class)); CounterSettleConfirmReqVO reqVO = new CounterSettleConfirmReqVO(); reqVO.setCashierId("1001"); reqVO.setSettleTime(settleTime); reqVO.setRemark("deposit topup settle"); var result = service.confirm(reqVO); assertEquals(new BigDecimal("100.00"), result.getTotalAmount()); assertEquals(1, result.getTotalCount()); verify(chargeMapper, never()).markCounterSettled(any(), any(), any()); ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(SettleRecordDetailDO.class); verify(settleRecordDetailMapper).insert(detailCaptor.capture()); assertNull(detailCaptor.getValue().getChargeId()); assertNull(detailCaptor.getValue().getBillMonth()); assertEquals(Long.valueOf(9001L), detailCaptor.getValue().getSourceCustId()); } ``` Add import if missing: ```java import static org.mockito.Mockito.never; ``` - [ ] **Step 3: Run counter settle tests to verify failures** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=CounterSettleApplicationServiceImplTest test ``` Expected: FAIL on cashier behavior and deposit topup charge writeback. - [ ] **Step 4: Include deposit topup records in unsettled query** In `PaymentRecordMapper.selectCounterUnsettledRecords`, replace: ```java .eq(PaymentRecordDO::getBizScene, "CHARGE_PAYMENT") ``` with: ```java .in(PaymentRecordDO::getBizScene, List.of("CHARGE_PAYMENT", "DEPOSIT_TOPUP")) ``` Add import if missing: ```java import java.util.List; ``` - [ ] **Step 5: Add helper to identify charge-payment rows** In `CounterSettleApplicationServiceImpl.java`, add: ```java private boolean isChargePaymentRecord(PaymentRecordDO record) { return record != null && Objects.equals("CHARGE_PAYMENT", record.getBizScene()); } ``` - [ ] **Step 6: Filter charge IDs to real charge-payment rows** In `confirm()`, replace the `chargeIds` calculation with: ```java List chargeIds = records.stream() .filter(this::isChargePaymentRecord) .map(PaymentRecordDO::getSourceRefId) .filter(Objects::nonNull) .distinct() .toList(); ``` Keep `totalAmount` calculated from all records. - [ ] **Step 7: Make detail insertion tolerate missing charge** In the `for (PaymentRecordDO record : records)` loop, keep: ```java ChargeDO charge = chargeMap.get(record.getSourceRefId()); ``` and ensure these assignments remain nullable: ```java .chargeId(isChargePaymentRecord(record) ? record.getSourceRefId() : null) .billMonth(charge != null ? charge.getBillMonth() : null) ``` - [ ] **Step 8: Skip charge writeback when no charge IDs exist** Replace: ```java validateChargeSettledCount(chargeIds, chargeMapper.markCounterSettled(chargeIds, settleRecord.getId(), reqVO.getSettleTime())); ``` with: ```java if (!chargeIds.isEmpty()) { validateChargeSettledCount(chargeIds, chargeMapper.markCounterSettled(chargeIds, settleRecord.getId(), reqVO.getSettleTime())); } ``` - [ ] **Step 9: Change cashier resolution rule** Replace `resolveCashierId` with: ```java private String resolveCashierId(String requestCashierId) { if (requestCashierId != null && !requestCashierId.isBlank()) { return requestCashierId; } return resolveAuthenticatedUserId(); } ``` Leave `resolveOperatorId` unchanged so the current login user remains the operator. - [ ] **Step 10: Run counter settle tests** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=CounterSettleApplicationServiceImplTest test ``` Expected: PASS. - [ ] **Step 11: Commit counter settlement fix** ```bash git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/mysql/paymentrecord/PaymentRecordMapper.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java git commit -m "fix: include counter topups in settlement" ``` --- ### Task 3: Backend Counter Red-Flush Record Query **Files:** - Create: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordPageReqVO.java` - Create: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordRespVO.java` - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationService.java` - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java` - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/ChargeController.java` - Modify: `../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java` - [ ] **Step 1: Create request VO** Create `CounterRedFlushRecordPageReqVO.java`: ```java package cn.com.emsoft.sw.business.controller.admin.charge.vo; import cn.com.emsoft.sw.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import static cn.com.emsoft.sw.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - 柜台红冲记录分页 Request VO") @Data @EqualsAndHashCode(callSuper = true) public class CounterRedFlushRecordPageReqVO extends PageParam { @Schema(description = "收费员", example = "1001") private String cashierId; @Schema(description = "客户编号", example = "CUST001") private String custCode; @Schema(description = "开始红冲时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime beginReversedTime; @Schema(description = "结束红冲时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime endReversedTime; } ``` - [ ] **Step 2: Create response VO** Create `CounterRedFlushRecordRespVO.java`: ```java package cn.com.emsoft.sw.business.controller.admin.charge.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Schema(description = "管理后台 - 柜台红冲记录 Response VO") @Data @Builder public class CounterRedFlushRecordRespVO { @Schema(description = "结清记录ID") private Long settleId; @Schema(description = "结清单号") private String settleNo; @Schema(description = "支付主单ID") private Long paymentRecordId; @Schema(description = "支付单号") private String paymentNo; @Schema(description = "客户编号") private String custCode; @Schema(description = "客户名称") private String custName; @Schema(description = "收费员") private String cashierId; @Schema(description = "红冲金额") private BigDecimal reversedAmount; @Schema(description = "红冲时间") private LocalDateTime reversedTime; @Schema(description = "红冲原因") private String reverseReason; } ``` - [ ] **Step 3: Add service interface methods** In `CounterSettleApplicationService.java`, add imports: ```java import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordPageReqVO; import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordRespVO; ``` Add methods: ```java PageResult getRedFlushRecordPage(CounterRedFlushRecordPageReqVO reqVO); List getRedFlushRecordExportRows(CounterRedFlushRecordPageReqVO reqVO); ``` - [ ] **Step 4: Add failing test for red-flush record query** Add this test to `CounterSettleApplicationServiceImplTest.java`: ```java @Test void getRedFlushRecordPage_shouldFilterByReverseTimeAndReturnReversedDetails() { LocalDateTime reverseTime = LocalDateTime.of(2026, 6, 8, 11, 30); CounterRedFlushRecordPageReqVO reqVO = new CounterRedFlushRecordPageReqVO(); reqVO.setPageNo(1); reqVO.setPageSize(10); reqVO.setCashierId("1001"); reqVO.setBeginReversedTime(reverseTime.minusHours(1)); reqVO.setEndReversedTime(reverseTime.plusHours(1)); SettleRecordDO settleRecord = SettleRecordDO.builder() .id(7001L) .settleNo("SETTLE-7001") .cashierId("1001") .status("PARTIAL_REVERSED") .build(); SettleRecordDetailDO detail = SettleRecordDetailDO.builder() .settleId(7001L) .paymentRecordId(101L) .paymentNo("PAY-101") .sourceCustCode("CUST-1") .sourceCustName("客户一") .reversedAmount(new BigDecimal("20.50")) .reverseTime(reverseTime) .procRemark("柜台红冲") .build(); when(settleRecordMapper.selectList(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))) .thenReturn(List.of(settleRecord)); when(settleRecordDetailMapper.selectBySettleId(7001L)).thenReturn(List.of(detail)); PageResult result = service.getRedFlushRecordPage(reqVO); assertEquals(1L, result.getTotal()); assertEquals("SETTLE-7001", result.getList().get(0).getSettleNo()); assertEquals(new BigDecimal("20.50"), result.getList().get(0).getReversedAmount()); assertEquals(reverseTime, result.getList().get(0).getReversedTime()); } ``` Add imports: ```java import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordPageReqVO; import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordRespVO; ``` - [ ] **Step 5: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=CounterSettleApplicationServiceImplTest#getRedFlushRecordPage_shouldFilterByReverseTimeAndReturnReversedDetails test ``` Expected: FAIL because the method does not exist. - [ ] **Step 6: Implement red-flush record query** Add this method to `CounterSettleApplicationServiceImpl.java`: ```java @Override public PageResult getRedFlushRecordPage(CounterRedFlushRecordPageReqVO reqVO) { List rows = queryRedFlushRecordRows(reqVO); return paginate(rows, reqVO.getPageNo(), reqVO.getPageSize()); } @Override public List getRedFlushRecordExportRows(CounterRedFlushRecordPageReqVO reqVO) { CounterRedFlushRecordPageReqVO exportReq = BeanUtils.toBean(reqVO, CounterRedFlushRecordPageReqVO.class); exportReq.setPageNo(1); exportReq.setPageSize(PageParam.PAGE_SIZE_NONE); return queryRedFlushRecordRows(exportReq); } private List queryRedFlushRecordRows(CounterRedFlushRecordPageReqVO reqVO) { CounterRedFlushRecordPageReqVO effectiveReq = BeanUtils.toBean(reqVO, CounterRedFlushRecordPageReqVO.class); effectiveReq.setCashierId(resolveCashierId(effectiveReq.getCashierId())); List settleRecords = settleRecordMapper.selectList(new LambdaQueryWrapperX() .eqIfPresent(SettleRecordDO::getCashierId, effectiveReq.getCashierId()) .in(SettleRecordDO::getStatus, List.of( SettleRecordStatusEnum.PARTIAL_REVERSED.getCode(), SettleRecordStatusEnum.FULL_REVERSED.getCode())) .geIfPresent(SettleRecordDO::getReversedTime, effectiveReq.getBeginReversedTime()) .leIfPresent(SettleRecordDO::getReversedTime, effectiveReq.getEndReversedTime()) .orderByDesc(SettleRecordDO::getReversedTime, SettleRecordDO::getId)); List rows = new ArrayList<>(); for (SettleRecordDO settleRecord : settleRecords) { for (SettleRecordDetailDO detail : settleRecordDetailMapper.selectBySettleId(settleRecord.getId())) { if (!Objects.equals(SettleRecordDetailStatusEnum.REVERSED.getCode(), detail.getDetailStatus())) { continue; } if (StrUtil.isNotBlank(effectiveReq.getCustCode()) && !Objects.equals(effectiveReq.getCustCode(), detail.getSourceCustCode())) { continue; } rows.add(CounterRedFlushRecordRespVO.builder() .settleId(settleRecord.getId()) .settleNo(settleRecord.getSettleNo()) .paymentRecordId(detail.getPaymentRecordId()) .paymentNo(detail.getPaymentNo()) .custCode(detail.getSourceCustCode()) .custName(detail.getSourceCustName()) .cashierId(settleRecord.getCashierId()) .reversedAmount(resolvePersistedReversedAmount(detail)) .reversedTime(detail.getReverseTime()) .reverseReason(detail.getProcRemark()) .build()); } } return rows; } ``` Add imports: ```java import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordPageReqVO; import cn.com.emsoft.sw.business.controller.admin.charge.vo.CounterRedFlushRecordRespVO; ``` - [ ] **Step 7: Add controller endpoints** In `ChargeController.java`, add: ```java @GetMapping("/counter-settle/red-flush-record-page") @Operation(summary = "柜台红冲记录分页") @PreAuthorize("@ss.hasPermission('business:charge:query')") public CommonResult> getCounterRedFlushRecordPage( @Valid CounterRedFlushRecordPageReqVO reqVO) { return success(counterSettleApplicationService.getRedFlushRecordPage(reqVO)); } @GetMapping("/counter-settle/red-flush-record-export") @Operation(summary = "导出柜台红冲记录 Excel") @PreAuthorize("@ss.hasPermission('business:charge:export')") @ApiAccessLog(operateType = EXPORT) public void exportCounterRedFlushRecordExcel(@Valid CounterRedFlushRecordPageReqVO reqVO, HttpServletResponse response) throws IOException { List list = counterSettleApplicationService.getRedFlushRecordExportRows(reqVO); String fileName = ExcelFileNameUtils.generateFileName("柜台红冲记录"); ExcelUtils.write(response, fileName, "数据", CounterRedFlushRecordRespVO.class, list); } ``` - [ ] **Step 8: Run red-flush query tests** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=CounterSettleApplicationServiceImplTest test ``` Expected: PASS. - [ ] **Step 9: Commit red-flush record API** ```bash git add sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordPageReqVO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/CounterRedFlushRecordRespVO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationService.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImpl.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/ChargeController.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/countersettle/CounterSettleApplicationServiceImplTest.java git commit -m "feat: add counter red flush record query" ``` --- ### Task 4: Backend Prepay Deduction Validation **Files:** - Modify: `../water-backend/sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java` - Create: `../water-backend/sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImplCounterPrepayTest.java` - [ ] **Step 1: Create failing tests** Create `ChargeServiceImplCounterPrepayTest.java`: ```java package cn.com.emsoft.sw.business.service.charge; import cn.com.emsoft.sw.business.controller.admin.charge.vo.ChargeSaveReqVO; import cn.com.emsoft.sw.business.dal.dataobject.account.AccountDO; import cn.com.emsoft.sw.business.dal.dataobject.charge.ChargeDO; import cn.com.emsoft.sw.business.dal.mysql.charge.ChargeMapper; import cn.com.emsoft.sw.business.enums.charge.ChargeMethodEnum; import cn.com.emsoft.sw.business.enums.charge.PayStateEnum; import cn.com.emsoft.sw.business.service.account.AccountService; import cn.com.emsoft.sw.business.service.paymentapp.PaymentCommandApplicationService; import cn.com.emsoft.sw.framework.common.exception.ServiceException; import cn.com.emsoft.sw.framework.test.core.ut.BaseMockitoUnitTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ChargeServiceImplCounterPrepayTest extends BaseMockitoUnitTest { @InjectMocks private ChargeServiceImpl chargeService; @Mock private ChargeMapper chargeMapper; @Mock private AccountService accountService; @Mock private PaymentCommandApplicationService paymentCommandApplicationService; @Test void updateCharge_shouldRejectPrepayDeductAmountGreaterThanAccountDeposit() { ChargeDO before = new ChargeDO(); before.setId(100L); before.setCustId(9001L); before.setPayState(PayStateEnum.UNPAID.getValue()); when(chargeMapper.selectById(100L)).thenReturn(before, paidCharge()); AccountDO account = new AccountDO(); account.setDeposit(new BigDecimal("5.00")); when(accountService.getAccountByCustId(9001L)).thenReturn(account); ChargeSaveReqVO reqVO = paidReq(new BigDecimal("8.00")); assertThrows(ServiceException.class, () -> chargeService.updateCharge(reqVO)); verify(accountService, never()).decreaseDeposit(9001L, new BigDecimal("8.00")); verify(paymentCommandApplicationService, never()).captureCounterChargePayment( org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any(), org.mockito.Mockito.any()); } private ChargeDO paidCharge() { ChargeDO charge = new ChargeDO(); charge.setId(100L); charge.setCustId(9001L); charge.setExtendedAmount(new BigDecimal("20.00")); charge.setPayState(PayStateEnum.PAID.getValue()); return charge; } private ChargeSaveReqVO paidReq(BigDecimal prepayDeductAmount) { ChargeSaveReqVO reqVO = new ChargeSaveReqVO(); reqVO.setId(100L); reqVO.setCustId(9001L); reqVO.setPayState(PayStateEnum.PAID.getValue()); reqVO.setChargeMethod(ChargeMethodEnum.COUNTER.getValue()); reqVO.setChargeWay(1); reqVO.setCashierId("1001"); reqVO.setPayDate(LocalDateTime.of(2026, 6, 8, 10, 0)); reqVO.setPrepayDeductAmount(prepayDeductAmount); return reqVO; } } ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=ChargeServiceImplCounterPrepayTest test ``` Expected: FAIL because `normalizeCounterPrepayDeductAmount` does not check account balance. - [ ] **Step 3: Implement account balance validation** In `ChargeServiceImpl.normalizeCounterPrepayDeductAmount`, after the total amount check, add: ```java if (prepayDeductAmount.compareTo(BigDecimal.ZERO) > 0) { AccountDO account = accountService.getAccountByCustId(latest.getCustId()); BigDecimal deposit = account == null || account.getDeposit() == null ? BigDecimal.ZERO : account.getDeposit(); if (prepayDeductAmount.compareTo(deposit) > 0) { throw invalidParamException("预存抵扣金额不能大于账户预存余额"); } } ``` Ensure `AccountDO` is imported: ```java import cn.com.emsoft.sw.business.dal.dataobject.account.AccountDO; ``` - [ ] **Step 4: Run prepay tests** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=ChargeServiceImplCounterPrepayTest test ``` Expected: PASS. - [ ] **Step 5: Commit prepay validation** ```bash 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/ChargeServiceImplCounterPrepayTest.java git commit -m "fix: validate counter prepay deduction balance" ``` --- ### Task 5: Frontend Shared Accounting Adjustment Status Messages **Files:** - Create: `../water-frontend/src/views/accountProcess/unsoldAdjustment/utils/statusMessage.ts` - Modify: `../water-frontend/src/views/accountProcess/unsoldAdjustment/components/SplitAdjustmentForm.vue` - Modify: `../water-frontend/src/views/accountProcess/unsoldAdjustment/components/BadDebtAdjustmentForm.vue` - Modify: `../water-frontend/src/views/accountProcess/unsoldAdjustment/components/PriceAdjustmentForm.vue` - Modify: `../water-frontend/src/views/accountProcess/unsoldAdjustment/components/PenaltyRemissionForm.vue` - Add: `../water-frontend/tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs` - [ ] **Step 1: Create failing frontend contract test** Create `tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs`: ```javascript import test from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import path from 'node:path' const root = process.cwd() test('unsold adjustment forms use runtime status message helper', async () => { const files = [ 'src/views/accountProcess/unsoldAdjustment/components/SplitAdjustmentForm.vue', 'src/views/accountProcess/unsoldAdjustment/components/BadDebtAdjustmentForm.vue', 'src/views/accountProcess/unsoldAdjustment/components/PriceAdjustmentForm.vue', 'src/views/accountProcess/unsoldAdjustment/components/PenaltyRemissionForm.vue' ] for (const file of files) { const source = await readFile(path.join(root, file), 'utf8') assert.match(source, /resolveAccountingAdjustSubmitMessage/) assert.doesNotMatch(source, /message\.success\('提交成功'\)/) } }) test('status message helper maps pending approval to user-facing pending text', async () => { const source = await readFile( path.join(root, 'src/views/accountProcess/unsoldAdjustment/utils/statusMessage.ts'), 'utf8' ) assert.match(source, /申请已提交,待审批/) assert.match(source, /处理完成/) assert.match(source, /待回写/) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: FAIL because helper does not exist and forms still use `提交成功`. - [ ] **Step 3: Create status message helper** Create `src/views/accountProcess/unsoldAdjustment/utils/statusMessage.ts`: ```typescript export interface AccountingAdjustRuntimeStatus { approvalRequired?: boolean resultStatus?: string approvalStatus?: string writeBackStatus?: string message?: string msg?: string } export const resolveAccountingAdjustSubmitMessage = (resp?: AccountingAdjustRuntimeStatus) => { if (!resp) { return '申请已提交' } if (resp.approvalRequired || resp.resultStatus === 'PENDING_APPROVAL' || resp.approvalStatus === 'PENDING_APPROVAL') { return '申请已提交,待审批' } if (resp.writeBackStatus === 'PENDING') { return '申请已提交,待回写' } if (resp.resultStatus === 'SUCCESS' && resp.writeBackStatus === 'UPDATED') { return '处理完成' } return resp.message || resp.msg || '申请已提交' } ``` - [ ] **Step 4: Update SplitAdjustmentForm** In `SplitAdjustmentForm.vue`, add import: ```typescript import { resolveAccountingAdjustSubmitMessage } from '../utils/statusMessage' ``` Replace: ```typescript await UnsoldAdjustmentApi.submitUnsoldSplit({ ``` with: ```typescript const resp = await UnsoldAdjustmentApi.submitUnsoldSplit({ ``` Replace: ```typescript message.success('提交成功') ``` with: ```typescript message.success(resolveAccountingAdjustSubmitMessage(resp)) ``` - [ ] **Step 5: Update BadDebtAdjustmentForm** In `BadDebtAdjustmentForm.vue`, add import: ```typescript import { resolveAccountingAdjustSubmitMessage } from '../utils/statusMessage' ``` Replace the red warning text: ```vue 设置完成后,账单将无法按照欠费查询和统计。 ``` with: ```vue 申请提交后进入审批,审批通过并回写后账单状态生效。 ``` Replace: ```typescript await UnsoldAdjustmentApi.submitBadDebtBatch({ ``` with: ```typescript const resp = await UnsoldAdjustmentApi.submitBadDebtBatch({ ``` Replace: ```typescript message.success('提交成功') ``` with: ```typescript message.success(resolveAccountingAdjustSubmitMessage(resp)) ``` - [ ] **Step 6: Update PriceAdjustmentForm and PenaltyRemissionForm** Apply the same import and response capture pattern in: - `src/views/accountProcess/unsoldAdjustment/components/PriceAdjustmentForm.vue` - `src/views/accountProcess/unsoldAdjustment/components/PenaltyRemissionForm.vue` For each file: ```typescript import { resolveAccountingAdjustSubmitMessage } from '../utils/statusMessage' ``` Use: ```typescript const resp = await UnsoldAdjustmentApi.submitPriceDiffBatch({ items }) message.success(resolveAccountingAdjustSubmitMessage(resp)) ``` and: ```typescript const resp = await UnsoldAdjustmentApi.submitLateFeeReduceBatch({ lateFeeType: adjustmentType.value === 'amount' ? '1' : '2', applicant: formData.value.applicant || undefined, contactMobile: formData.value.contactMobile || undefined, applyReason: formData.value.applyReason || undefined, remark: formData.value.remark || undefined, attachmentRefs: formData.value.files?.length ? formData.value.files : undefined, items }) message.success(resolveAccountingAdjustSubmitMessage(resp)) ``` - [ ] **Step 7: Run frontend contract test** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: PASS. - [ ] **Step 8: Commit frontend pending-status messages** ```bash git add src/views/accountProcess/unsoldAdjustment/utils/statusMessage.ts \ src/views/accountProcess/unsoldAdjustment/components/SplitAdjustmentForm.vue \ src/views/accountProcess/unsoldAdjustment/components/BadDebtAdjustmentForm.vue \ src/views/accountProcess/unsoldAdjustment/components/PriceAdjustmentForm.vue \ src/views/accountProcess/unsoldAdjustment/components/PenaltyRemissionForm.vue \ tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs git commit -m "fix: show pending status for accounting adjustments" ``` --- ### Task 6: Frontend Price Adjustment Retry Flow **Files:** - Modify: `../water-frontend/src/views/settings/price/priceTemplate/index.vue` - Modify: `../water-frontend/tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs` - [ ] **Step 1: Add failing contract assertion** Append this test to `tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs`: ```javascript test('price template submit failure attempts to reacquire adjustment lock', async () => { const source = await readFile( path.join(root, 'src/views/settings/price/priceTemplate/index.vue'), 'utf8' ) assert.match(source, /const reacquireAdjustmentLockAfterFailure = async/) assert.match(source, /await reacquireAdjustmentLockAfterFailure\(\)/) assert.doesNotMatch(source, /catch \(err: any\) \{\s*message\.error\(err\?\.message \|\| '调价失败,请稍后重试'\)\s*isAdjusting\.value = false\s*\}/) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: FAIL. - [ ] **Step 3: Add reacquire helper** In `src/views/settings/price/priceTemplate/index.vue`, near `startAdjustment`, add: ```typescript const reacquireAdjustmentLockAfterFailure = async () => { try { await PriceTemplateApi.startAdjustment() isAdjusting.value = true message.warning('调价失败,本次调价锁已重新获取,请修正数据后再次提交') } catch (lockErr: any) { isAdjusting.value = false message.error(lockErr?.message || '调价失败,重新获取调价锁失败,请稍后重试') } } ``` - [ ] **Step 4: Use helper in submit failure path** In `submitForm`, replace the catch block: ```typescript } catch (err: any) { message.error(err?.message || '调价失败,请稍后重试') isAdjusting.value = false } finally { ``` with: ```typescript } catch (err: any) { message.error(err?.message || '调价失败,请稍后重试') await reacquireAdjustmentLockAfterFailure() } finally { ``` - [ ] **Step 5: Run frontend contract test** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: PASS. - [ ] **Step 6: Commit price adjustment retry flow** ```bash git add src/views/settings/price/priceTemplate/index.vue tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs git commit -m "fix: reacquire price adjustment lock after failure" ``` --- ### Task 7: Frontend Counter Red-Flush Record Page Wiring **Files:** - Modify: `../water-frontend/src/api/business/charge/counterSettle.ts` - Modify: `../water-frontend/src/api/operatingCharges/redReversalRecord/index.ts` - Modify: `../water-frontend/src/views/operatingCharges/redReversalRecord/index.vue` - Modify: `../water-frontend/tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs` - [ ] **Step 1: Add failing contract assertion** Append this test: ```javascript test('red reversal record page uses counter red flush record endpoint and reverse time filters', async () => { const apiSource = await readFile( path.join(root, 'src/api/operatingCharges/redReversalRecord/index.ts'), 'utf8' ) const viewSource = await readFile( path.join(root, 'src/views/operatingCharges/redReversalRecord/index.vue'), 'utf8' ) assert.match(apiSource, /counter-settle\/red-flush-record-page/) assert.match(apiSource, /counter-settle\/red-flush-record-export/) assert.doesNotMatch(apiSource, /accounting-adjust\/log-page/) assert.match(viewSource, /红冲时间/) assert.match(viewSource, /beginReversedTime/) assert.match(viewSource, /endReversedTime/) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: FAIL. - [ ] **Step 3: Add counter red-flush API methods** In `src/api/business/charge/counterSettle.ts`, add: ```typescript export interface CounterRedFlushRecordPageReqVO extends PageParam { cashierId?: string custCode?: string beginReversedTime?: string endReversedTime?: string } export interface CounterRedFlushRecordRespVO { settleId: number settleNo?: string paymentRecordId?: number paymentNo?: string custCode?: string custName?: string cashierId?: string reversedAmount?: number reversedTime?: string reverseReason?: string } ``` Add methods inside `CounterSettleApi`: ```typescript getCounterRedFlushRecordPage: (params: CounterRedFlushRecordPageReqVO) => request.get>({ url: '/business/charge/counter-settle/red-flush-record-page', params }), exportCounterRedFlushRecordPage: (params: CounterRedFlushRecordPageReqVO) => request.download({ url: '/business/charge/counter-settle/red-flush-record-export', params }), ``` - [ ] **Step 4: Rewire redReversalRecord API wrapper** Replace `src/api/operatingCharges/redReversalRecord/index.ts` with: ```typescript import { CounterSettleApi, type CounterRedFlushRecordPageReqVO } from '@/api/business/charge/counterSettle' export const getRedReversalPage = async (params: CounterRedFlushRecordPageReqVO) => { return await CounterSettleApi.getCounterRedFlushRecordPage(params) } export const exportRedReversalRecord = (params: CounterRedFlushRecordPageReqVO) => { return CounterSettleApi.exportCounterRedFlushRecordPage(params) } ``` - [ ] **Step 5: Update red-flush record page query fields** In `src/views/operatingCharges/redReversalRecord/index.vue`, replace query params: ```typescript const queryParams = reactive({ pageNo: 1, pageSize: 10, cashierId: undefined, custCode: undefined, reversedTime: [] }) ``` Add helper: ```typescript const buildQueryParams = () => ({ pageNo: queryParams.pageNo, pageSize: queryParams.pageSize, cashierId: queryParams.cashierId, custCode: queryParams.custCode, beginReversedTime: queryParams.reversedTime?.[0], endReversedTime: queryParams.reversedTime?.[1] }) ``` In `getList`, use: ```typescript const data = await getRedReversalPage(buildQueryParams()) ``` In export, use: ```typescript const data = await exportRedReversalRecord(buildQueryParams()) ``` - [ ] **Step 6: Update red-flush record labels and columns** Change “处理人” input to: ```vue ``` Change date picker label to: ```vue ``` Replace `defaultColumns` with: ```typescript const defaultColumns = [ { field: 'settleNo', label: '结账单号', width: 180 }, { field: 'paymentNo', label: '收费单号', width: 180 }, { field: 'custCode', label: '客户编号', width: 160 }, { field: 'custName', label: '客户名称', width: 180 }, { field: 'cashierId', label: '收费员', width: 140 }, { field: 'reversedAmount', label: '红冲金额', width: 140 }, { field: 'reversedTime', label: '红冲时间', width: 180 }, { field: 'reverseReason', label: '红冲原因', width: 220 } ] ``` - [ ] **Step 7: Run frontend contract test** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: PASS. - [ ] **Step 8: Commit red-flush record frontend wiring** ```bash git add src/api/business/charge/counterSettle.ts \ src/api/operatingCharges/redReversalRecord/index.ts \ src/views/operatingCharges/redReversalRecord/index.vue \ tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs git commit -m "fix: wire red flush record page to counter records" ``` --- ### Task 8: Frontend Counter Settlement and Prepay Guards **Files:** - Modify: `../water-frontend/src/views/operatingCharges/counterCheckout/components/CounterUnsettledPanel.vue` - Modify: `../water-frontend/src/views/operatingCharges/counterCharging/index.vue` - Modify: `../water-frontend/tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs` - [ ] **Step 1: Add failing contract assertion** Append: ```javascript test('counter checkout and charging guard deposit-topup and zero prepay states', async () => { const checkoutSource = await readFile( path.join(root, 'src/views/operatingCharges/counterCheckout/components/CounterUnsettledPanel.vue'), 'utf8' ) const chargingSource = await readFile( path.join(root, 'src/views/operatingCharges/counterCharging/index.vue'), 'utf8' ) assert.match(checkoutSource, /formatNullableBillField/) assert.match(chargingSource, /confirmZeroPrepayDeduction/) assert.match(chargingSource, /预存抵扣金额为0/) }) ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: FAIL. - [ ] **Step 3: Add nullable bill formatter** In `CounterUnsettledPanel.vue`, add: ```typescript const formatNullableBillField = (value?: string | number | null) => { if (value === null || value === undefined || value === '') { return '--' } return String(value) } ``` Use it for bill fields: ```vue ``` and: ```vue ``` - [ ] **Step 4: Add zero prepay confirmation helper** In `counterCharging/index.vue`, add: ```typescript const confirmZeroPrepayDeduction = async () => { if (!preDeduct.value || preDeductAmount.value > 0) { return true } try { await message.confirm('预存抵扣金额为0,将按现金/其他方式全额收费,是否继续?') return true } catch { return false } } ``` - [ ] **Step 5: Invoke zero prepay confirmation before submit** At the start of the submit handler, after existing selected-row guard and before `submitting.value = true`, add: ```typescript if (!(await confirmZeroPrepayDeduction())) { return } ``` - [ ] **Step 6: Run frontend contract test** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: PASS. - [ ] **Step 7: Commit counter frontend guards** ```bash git add src/views/operatingCharges/counterCheckout/components/CounterUnsettledPanel.vue \ src/views/operatingCharges/counterCharging/index.vue \ tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs git commit -m "fix: guard counter topup and prepay display states" ``` --- ### Task 9: Cross-Repository Verification **Files:** - Backend verification only. - Frontend verification only. - Create evidence: `../water-docs/docs/evidence/revenue-bugfix-clear-scope-2026-06-08.md` - [ ] **Step 1: Run backend targeted tests** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -Dtest=PriceTemplateServiceImplTest,PriceTemplateAdjustmentLockRedisDAOTest,CounterSettleApplicationServiceImplTest,ChargeServiceImplCounterPrepayTest test ``` Expected: BUILD SUCCESS. - [ ] **Step 2: Run backend module compile** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope mvn -pl sw-business/sw-business-server -DskipTests compile ``` Expected: BUILD SUCCESS. - [ ] **Step 3: Run frontend contract test** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs ``` Expected: all tests PASS. - [ ] **Step 4: Run frontend type check** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope pnpm ts:check ``` Expected: no TypeScript errors. - [ ] **Step 5: Run focused frontend build if type check passes** Run: ```bash cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope pnpm build:dev ``` Expected: build completes successfully. - [ ] **Step 6: Write evidence document** Create `../water-docs/docs/evidence/revenue-bugfix-clear-scope-2026-06-08.md`: ```markdown # 营收明确缺陷第一批修复验证记录 日期:2026-06-08 ## 修复范围 - #78 水价调整失败后重试闭环 - #39 柜台预存缴费进入结账 - #50 柜台结账收费员筛选 - #53 预存抵扣校验 - #58/#59 柜台红冲记录查询 - #69/#76 待审批账务调整文案 ## 后端基线 - 仓库:water-backend - 分支或 worktree:backend-revenue-bugfix-clear-scope - 提交:记录 `cd /Volumes/Dpan/github/water-workspace/worktrees/backend-revenue-bugfix-clear-scope && git rev-parse HEAD` 的输出 ## 前端基线 - 仓库:water-frontend - 分支或 worktree:frontend-revenue-bugfix-clear-scope - 提交:记录 `cd /Volumes/Dpan/github/water-workspace/worktrees/frontend-revenue-bugfix-clear-scope && git rev-parse HEAD` 的输出 ## 验证命令 ```bash mvn -pl sw-business/sw-business-server -Dtest=PriceTemplateServiceImplTest,PriceTemplateAdjustmentLockRedisDAOTest,CounterSettleApplicationServiceImplTest,ChargeServiceImplCounterPrepayTest test mvn -pl sw-business/sw-business-server -DskipTests compile node --test tests/revenue-bugs/revenueBugfixClearScope.contract.test.mjs pnpm ts:check pnpm build:dev ``` ## 验证结果 - 后端 targeted tests:通过 - 后端 compile:通过 - 前端 contract test:通过 - 前端 type check:通过 - 前端 build:通过 ## 备注 本轮未处理 #70 和 #9。#70 需抓包确认提交字段;#9 需产品确认抄表状态规则。 ``` - [ ] **Step 7: Commit evidence** ```bash cd /Volumes/Dpan/github/water-workspace/water-docs git add docs/evidence/revenue-bugfix-clear-scope-2026-06-08.md git commit -m "docs: add revenue bugfix verification evidence" ``` --- ## Self-Review Spec coverage: - `#78` covered by Task 1 and Task 6. - `#39` covered by Task 2 and Task 8. - `#50` covered by Task 2. - `#53` covered by Task 4 and Task 8. - `#58/#59` covered by Task 3 and Task 7. - `#69/#76` covered by Task 5. - Verification and docs evidence covered by Task 9. Placeholder scan: - No `TBD`, `TODO`, or open-ended “handle later” steps are present. - Each task has exact files, code snippets, commands, and expected results. Type consistency: - Backend VO names: `CounterRedFlushRecordPageReqVO`, `CounterRedFlushRecordRespVO`. - Frontend API type names match backend VO names. - Frontend helper name `resolveAccountingAdjustSubmitMessage` is used consistently. - Red-flush date params use `beginReversedTime` and `endReversedTime` consistently.