# REV-004 Unsold Preview Options 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:** Make unsold-adjust preview checkbox options flow into formal submission, audit records, and final write-back calculation consistently. **Architecture:** Keep the existing REV-004 unsold-adjust endpoints and services. Extend the current VO/DTO/formalization/write-back chain so preview options are explicit formal adjustment parameters instead of transient preview-only flags. **Tech Stack:** Java 17, Spring Boot, MyBatis Plus, JUnit 5, Mockito, Maven, Markdown evidence in `../water-docs`. --- ## File Structure Backend files: - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustUnsoldAdjustSubmitReqVO.java` - Add formal water-adjust options `updateAccumulated` and `updateBaseCode`. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustSubmitReqVO.java` - Carry water-adjust options through accountProcess services. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/AccountingAdjustActionController.java` - Map water and price-diff option fields from endpoint-specific VO to unified submit VO. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/AccountingAdjustReqVO.java` - Carry water-adjust options into `ChargeServiceImpl`. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImpl.java` - Pass water-adjust options into legacy/core adjustment request. - Pass price-diff options into formal create DTO. - `sw-business/sw-business-api/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/dto/PriceDiffAdjustCreateReqDTO.java` - Add `calculateGarbageFee` and `updateAccumulatedVolume`. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/PriceDiffAdjustInternalApiImpl.java` - Move price-diff options from API DTO to formalization VO. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/pricediffadjustdetail/PriceDiffAdjustDetailDO.java` - Add `calculateGarbageFee` and `updateAccumulatedVolume`. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationService.java` - Persist all price-diff option values in formal detail rows. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/AccountingAdjustActionServiceImpl.java` - Read price-diff options from formal/log detail and send them into write-back. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackService.java` - Align default option semantics with preview and apply `updateAccumulatedVolume`. - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java` - Apply formal water-adjust options and record option values in operation log details. Backend test files: - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/AccountingAdjustActionControllerTest.java` - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImplTest.java` - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceAccountingAdjustTest.java` - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationServiceTest.java` - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackServiceTest.java` Docs files: - `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md` ## Task 0: Preserve Current Usage-Adjustment Fix **Files:** - Modify: `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-usage-adjust-zero-and-recovery-2026-06-19.md` - Existing backend changes: - `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/trial/UnsoldTrialPreviewServiceImpl.java` - `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/accountingadjust/trial/UnsoldTrialPreviewServiceImplTest.java` - `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceAccountingAdjustTest.java` - [ ] **Step 1: Confirm the current backend diff only contains the prior usage-adjustment fix** Run: ```bash git status --short git diff -- sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/trial/UnsoldTrialPreviewServiceImpl.java \ 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/accountingadjust/trial/UnsoldTrialPreviewServiceImplTest.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceAccountingAdjustTest.java ``` Expected: - `UnsoldTrialPreviewServiceImpl` accepts zero target water and rejects negative target water. - `ChargeServiceImpl` allows target water above read/check water only for supplementary/recovery reasons. - Tests cover zero-water preview and supplementary reading recovery. - [ ] **Step 2: Re-run the already-passing validation** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=UnsoldTrialPreviewServiceImplTest,ChargeServiceAccountingAdjustTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text Tests run: 36, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` - [ ] **Step 3: Add evidence for the prior usage-adjustment fix** Create `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-usage-adjust-zero-and-recovery-2026-06-19.md`: ```markdown # REV-004 未销水量调整零水量与补抄追收验证记录 ## 变更范围 - 按水量调整预算允许 `targetBillWater=0`,仍拒绝负数水量。 - 正式水量调整默认保留“调整后水量不能超过抄见水量”限制。 - 当原因编码或原因说明属于补抄、追收、漏抄、少抄、估抄偏低、补征等场景时,允许目标水量超过原抄见水量。 ## 验证命令 ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=UnsoldTrialPreviewServiceImplTest,ChargeServiceAccountingAdjustTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` ## 验证结果 - `UnsoldTrialPreviewServiceImplTest`: 12 tests, 0 failures, 0 errors. - `ChargeServiceAccountingAdjustTest`: 24 tests, 0 failures, 0 errors. - Maven reactor: `BUILD SUCCESS`. ## 风险说明 - 本次未放开已收费、已开票、已结账、缺少抄表依据、缺少附件等既有校验。 - 补抄追收白名单为后端兼容策略,后续可在正式原因字典稳定后收敛为字典值校验。 ``` - [ ] **Step 4: Commit the prior usage-adjustment fix** Run: ```bash git add \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/trial/UnsoldTrialPreviewServiceImpl.java \ 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/accountingadjust/trial/UnsoldTrialPreviewServiceImplTest.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceAccountingAdjustTest.java git commit -m "fix(accounting): relax unsold usage adjust water validation" ``` Then in docs repo: ```bash git -C ../water-docs add docs/evidence/rev004-accounting/rev004-unsold-usage-adjust-zero-and-recovery-2026-06-19.md git -C ../water-docs commit -m "docs(accounting): record unsold usage adjust validation evidence" ``` Expected: - Backend and docs commits are separate. - `git status --short` in backend is clean before Task 1 starts. ## Task 1: Add Water-Adjust Option Fields to Request Chain **Files:** - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustUnsoldAdjustSubmitReqVO.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustSubmitReqVO.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/AccountingAdjustReqVO.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/AccountingAdjustActionController.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImpl.java` - Test: `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImplTest.java` - [ ] **Step 1: Write failing service mapping test** Add this test to `AccountingAdjustProcessServiceImplTest` near the existing `createUnsoldAdjust_shouldDelegateUsageAdjustToUnifiedChargeService` test: ```java @Test void createUnsoldAdjust_shouldPassWaterOptionFlagsToUnifiedChargeService() { AccountingAdjustSubmitReqVO reqVO = new AccountingAdjustSubmitReqVO(); reqVO.setChargeId(15801L); reqVO.setTargetBillWater(new BigDecimal("0.000")); reqVO.setReasonCode("REV004_USAGE_FIX"); reqVO.setReason("零水量更正"); reqVO.setAttachmentRefs(List.of("proof-001")); reqVO.setUpdateAccumulated(true); reqVO.setUpdateBaseCode(true); reqVO.setCurrentReading(new BigDecimal("1234.000")); AccountingAdjustRespVO expected = AccountingAdjustRespVO.of( "REV004-15801-20260619110000", null, "SUCCESS", false, "NOT_REQUIRED", "UPDATED", "REV004-15801-20260619110000", "水量调整成功" ); when(chargeService.adjustAccounting(any(AccountingAdjustReqVO.class))).thenReturn(expected); AccountingAdjustRespVO actual = service.createUnsoldAdjust(reqVO); assertEquals("SUCCESS", actual.getResultStatus()); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountingAdjustReqVO.class); verify(chargeService).adjustAccounting(captor.capture()); AccountingAdjustReqVO coreReq = captor.getValue(); assertEquals(new BigDecimal("0.000"), coreReq.getTargetBillWater()); assertEquals(Boolean.TRUE, coreReq.getUpdateAccumulated()); assertEquals(Boolean.TRUE, coreReq.getUpdateBaseCode()); assertEquals(new BigDecimal("1234.000"), coreReq.getCurrentReading()); } ``` Ensure imports exist: ```java import org.mockito.ArgumentCaptor; ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=AccountingAdjustProcessServiceImplTest#createUnsoldAdjust_shouldPassWaterOptionFlagsToUnifiedChargeService \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: compile failure or assertion failure because `getUpdateAccumulated` and `getUpdateBaseCode` are not present or not mapped. - [ ] **Step 3: Add fields to request VOs** In `AccountingAdjustUnsoldAdjustSubmitReqVO`, add after `currentReading`: ```java @Schema(description = "是否更新累计量;未传默认false", example = "true") private Boolean updateAccumulated; @Schema(description = "是否更新底码;未传默认false", example = "true") private Boolean updateBaseCode; ``` In `AccountingAdjustSubmitReqVO`, add near `currentReading`: ```java private Boolean updateAccumulated; private Boolean updateBaseCode; ``` In `AccountingAdjustReqVO`, add after `currentReading`: ```java @Schema(description = "是否更新累计量,仅 legacy-only 水量调整使用;未传默认 false", example = "true") private Boolean updateAccumulated; @Schema(description = "是否更新底码,仅 legacy-only 水量调整使用;未传默认 false", example = "true") private Boolean updateBaseCode; ``` - [ ] **Step 4: Map fields through Controller and process service** In `AccountingAdjustActionController.toUnsoldSubmitReq(AccountingAdjustUnsoldAdjustSubmitReqVO reqVO)`, add: ```java target.setUpdateAccumulated(reqVO.getUpdateAccumulated()); target.setUpdateBaseCode(reqVO.getUpdateBaseCode()); ``` In `AccountingAdjustProcessServiceImpl.buildLegacyAdjustReq(...)`, add: ```java coreReq.setUpdateAccumulated(reqVO.getUpdateAccumulated()); coreReq.setUpdateBaseCode(reqVO.getUpdateBaseCode()); ``` - [ ] **Step 5: Run test to verify it passes** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=AccountingAdjustProcessServiceImplTest#createUnsoldAdjust_shouldPassWaterOptionFlagsToUnifiedChargeService \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 6: Commit Task 1** Run: ```bash git add \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustUnsoldAdjustSubmitReqVO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/vo/AccountingAdjustSubmitReqVO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/charge/vo/AccountingAdjustReqVO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/controller/admin/accountingadjust/accountProcess/AccountingAdjustActionController.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImpl.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImplTest.java git commit -m "feat(accounting): carry water preview options into unsold submit" ``` ## Task 2: Apply Water-Adjust Options in ChargeService **Files:** - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java` - Test: `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceAccountingAdjustTest.java` - [ ] **Step 1: Write failing test for updateBaseCode requiring currentReading** Add to `ChargeServiceAccountingAdjustTest`: ```java @Test void testAdjustAccounting_usageUpdateBaseCodeRequiresCurrentReading() { ChargeDO charge = buildCharge(123L, PayStateEnum.UNPAID.getValue(), new BigDecimal("18.50")); charge.setRecordId(923L); charge.setReadWater(new BigDecimal("30.000")); when(chargeMapper.selectById(123L)).thenReturn(charge); ReadingDataDO readingData = new ReadingDataDO(); readingData.setId(923L); readingData.setReadWater(new BigDecimal("30.000")); readingData.setCheckWater(new BigDecimal("30.000")); when(readingDataService.getReadingDataByIds(Collections.singletonList(923L))) .thenReturn(Collections.singletonList(readingData)); when(chargeDetailService.getChargeDetailsByFeeIds(Collections.singletonList(123L))) .thenReturn(Collections.emptyList()); AccountingAdjustReqVO reqVO = new AccountingAdjustReqVO(); reqVO.setChargeId(123L); reqVO.setAdjustType("USAGE"); reqVO.setTargetBillWater(new BigDecimal("20.000")); reqVO.setReasonCode("REV004_USAGE_FIX"); reqVO.setReason("底码更正"); reqVO.setAttachmentRefs(List.of("proof-001")); reqVO.setUpdateBaseCode(true); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> chargeService.adjustAccounting(reqVO)); assertEquals("更新底码必须传入本次抄码", ex.getMessage()); } ``` - [ ] **Step 2: Write failing test for updateBaseCode and updateAccumulated effects** Add to `ChargeServiceAccountingAdjustTest`: ```java @Test void testAdjustAccounting_usageOptionsUpdateReadingAndAccumulatedWater() { ChargeDO charge = buildCharge(124L, PayStateEnum.UNPAID.getValue(), new BigDecimal("18.50")); charge.setRecordId(924L); charge.setReadWater(new BigDecimal("30.000")); charge.setReading(new BigDecimal("1000.000")); charge.setTotalWater(new BigDecimal("500.000")); when(chargeMapper.selectById(124L)).thenReturn(charge); ReadingDataDO readingData = new ReadingDataDO(); readingData.setId(924L); readingData.setReadWater(new BigDecimal("30.000")); readingData.setCheckWater(new BigDecimal("30.000")); when(readingDataService.getReadingDataByIds(Collections.singletonList(924L))) .thenReturn(Collections.singletonList(readingData)); when(chargeDetailService.getChargeDetailsByFeeIds(Collections.singletonList(124L))) .thenReturn(Collections.emptyList()); CustDO cust = new CustDO(); cust.setId(1L); cust.setPriceTemplateCode("PRICE-001"); when(custService.getCust(1L)).thenReturn(cust); when(custWaterUseSchemeService.getCustWaterUseSchemeByCustId(1L)).thenReturn(null); PriceTemplateDO priceTemplate = new PriceTemplateDO(); priceTemplate.setId(11L); priceTemplate.setCode("PRICE-001"); priceTemplate.setAdjustmentSnapCode("SNAP-001"); when(priceTemplateService.getPriceTemplatesByCodes(Collections.singletonList("PRICE-001"))) .thenReturn(Collections.singletonList(priceTemplate)); PriceCostAdjustmentDO costAdjustment = new PriceCostAdjustmentDO(); costAdjustment.setId(21L); costAdjustment.setCostComponentCode(ChargeCalculateHelper.COST_CODE_WATER); when(priceCostAdjustmentService.getPriceCostAdjustmentsByTemplateIds(Collections.singletonList(11L))) .thenReturn(Collections.singletonList(costAdjustment)); PriceTierAdjustmentDO tierAdjustment = new PriceTierAdjustmentDO(); tierAdjustment.setId(31L); tierAdjustment.setCostAdjustmentId(21L); tierAdjustment.setTierLevel(1); tierAdjustment.setStartVolume(BigDecimal.ZERO); tierAdjustment.setEndVolume(new BigDecimal("999999")); tierAdjustment.setPrice(new BigDecimal("1.0000")); when(priceTierAdjustmentService.getPriceTierAdjustmentsByCostAdjustmentIds(Collections.singletonList(21L))) .thenReturn(Collections.singletonList(tierAdjustment)); AccountingAdjustReqVO reqVO = new AccountingAdjustReqVO(); reqVO.setChargeId(124L); reqVO.setAdjustType("USAGE"); reqVO.setTargetBillWater(new BigDecimal("20.000")); reqVO.setReasonCode("REV004_USAGE_FIX"); reqVO.setReason("同步累计量和底码"); reqVO.setAttachmentRefs(List.of("proof-001")); reqVO.setUpdateBaseCode(true); reqVO.setUpdateAccumulated(true); reqVO.setCurrentReading(new BigDecimal("1010.000")); AccountingAdjustRespVO respVO = chargeService.adjustAccounting(reqVO); assertEquals("SUCCESS", respVO.getResultStatus()); assertEquals(new BigDecimal("1010.000"), charge.getReading()); assertEquals(new BigDecimal("510.000"), charge.getTotalWater()); verify(chargeMapper).updateById(charge); } ``` - [ ] **Step 3: Run tests to verify they fail** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=ChargeServiceAccountingAdjustTest#testAdjustAccounting_usageUpdateBaseCodeRequiresCurrentReading+testAdjustAccounting_usageOptionsUpdateReadingAndAccumulatedWater \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: at least one failure because `updateBaseCode` and `updateAccumulated` are not implemented. - [ ] **Step 4: Implement validation and write-back** In `ChargeServiceImpl.validateUsageAdjust(...)`, after reason/attachment checks are available and before recalculation, add: ```java if (Boolean.TRUE.equals(reqVO.getUpdateBaseCode()) && reqVO.getCurrentReading() == null) { throw new IllegalArgumentException("更新底码必须传入本次抄码"); } ``` In `ChargeServiceImpl.handleUsageAdjust(...)`, preserve original water before recalculation: ```java BigDecimal beforeBillWater = state.getChargeDO().getBillWater(); ``` After `copyRecalculatedFees(recalculated, state.getChargeDO());`, add: ```java applyUsageAdjustOptions(state.getChargeDO(), state.getReqVO(), beforeBillWater); ``` Add helper method near `copyRecalculatedFees`: ```java private void applyUsageAdjustOptions(ChargeDO chargeDO, AccountingAdjustReqVO reqVO, BigDecimal beforeBillWater) { if (Boolean.TRUE.equals(reqVO.getUpdateBaseCode())) { chargeDO.setReading(reqVO.getCurrentReading()); } if (Boolean.TRUE.equals(reqVO.getUpdateAccumulated())) { BigDecimal beforeTotalWater = chargeDO.getTotalWater(); if (beforeTotalWater != null) { BigDecimal oldWater = beforeBillWater == null ? BigDecimal.ZERO : beforeBillWater; BigDecimal newWater = reqVO.getTargetBillWater() == null ? BigDecimal.ZERO : reqVO.getTargetBillWater(); chargeDO.setTotalWater(beforeTotalWater.add(newWater.subtract(oldWater))); } } } ``` - [ ] **Step 5: Include option values in operation log details** In the operation-log detail builder for accounting adjustments in `ChargeServiceImpl`, add details for: ```java addOperatLogDetail(dto, "updateAccumulated", "是否更新累计量", null, reqVO.getUpdateAccumulated(), EnumColumnType.NORMAL_TYPE); addOperatLogDetail(dto, "updateBaseCode", "是否更新底码", null, reqVO.getUpdateBaseCode(), EnumColumnType.NORMAL_TYPE); ``` - [ ] **Step 6: Run Task 2 tests** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=ChargeServiceAccountingAdjustTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 7: Commit Task 2** Run: ```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/ChargeServiceAccountingAdjustTest.java git commit -m "feat(accounting): apply water adjust preview options" ``` ## Task 3: Add Price-Diff Options to Formal Object Chain **Files:** - Modify: `sw-business/sw-business-api/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/dto/PriceDiffAdjustCreateReqDTO.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/PriceDiffAdjustInternalApiImpl.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImpl.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/pricediffadjustdetail/PriceDiffAdjustDetailDO.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationService.java` - Test: `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationServiceTest.java` - [ ] **Step 1: Write failing formalization test** In `PriceDiffFormalizationServiceTest`, add: ```java @Test void ensurePendingRecord_shouldPersistPriceDiffOptionFlags() { ChargeDO sourceCharge = new ChargeDO(); sourceCharge.setId(15801L); sourceCharge.setCustCode("C001"); sourceCharge.setBillMonth(202606); sourceCharge.setBillAmount(new BigDecimal("100.00")); sourceCharge.setExtendedAmount(new BigDecimal("100.00")); sourceCharge.setPriceTemplateCode("OLD"); sourceCharge.setAdjustmentSnapCode("OLD-SNAP"); sourceCharge.setPayState(PayStateEnum.UNPAID.getValue()); AccountingAdjustSubmitReqVO reqVO = new AccountingAdjustSubmitReqVO(); reqVO.setChargeId(15801L); reqVO.setPriceDiffAmount(new BigDecimal("15.50")); reqVO.setReasonCode("REV004_PRICE_DIFF"); reqVO.setReason("价差调整"); reqVO.setNewPriceTemplateCode("NEW"); reqVO.setAdjustmentSnapCode("NEW-SNAP"); reqVO.setIsLadder(false); reqVO.setCalculateGarbageFee(false); reqVO.setUpdateAccumulatedVolume(false); service.ensurePendingRecord("REV004-PD-C001-15801-20260619110000", sourceCharge, reqVO); ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(PriceDiffAdjustDetailDO.class); verify(priceDiffAdjustDetailMapper).insert(detailCaptor.capture()); PriceDiffAdjustDetailDO detail = detailCaptor.getValue(); assertEquals(Short.valueOf((short) 0), detail.getIsLadder()); assertEquals(Short.valueOf((short) 0), detail.getCalculateGarbageFee()); assertEquals(Short.valueOf((short) 0), detail.getUpdateAccumulatedVolume()); } ``` Ensure imports exist: ```java import org.mockito.ArgumentCaptor; ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PriceDiffFormalizationServiceTest#ensurePendingRecord_shouldPersistPriceDiffOptionFlags \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: compile failure because `getCalculateGarbageFee` and `getUpdateAccumulatedVolume` are not defined on `PriceDiffAdjustDetailDO`. - [ ] **Step 3: Extend DTO and DO** In `PriceDiffAdjustCreateReqDTO`, add: ```java private Boolean calculateGarbageFee; private Boolean updateAccumulatedVolume; ``` In `PriceDiffAdjustDetailDO`, add: ```java private Short calculateGarbageFee; private Short updateAccumulatedVolume; ``` - [ ] **Step 4: Pass options into formalization VO** In `AccountingAdjustProcessServiceImpl.createUnsoldPriceDiff(...)`, after `dto.setIsLadder(reqVO.getIsLadder());`, add: ```java dto.setCalculateGarbageFee(reqVO.getCalculateGarbageFee()); dto.setUpdateAccumulatedVolume(reqVO.getUpdateAccumulatedVolume()); ``` In `PriceDiffAdjustInternalApiImpl.create(...)`, after `vo.setIsLadder(reqDTO.getIsLadder());`, add: ```java vo.setCalculateGarbageFee(reqDTO.getCalculateGarbageFee()); vo.setUpdateAccumulatedVolume(reqDTO.getUpdateAccumulatedVolume()); ``` - [ ] **Step 5: Persist options in formal detail** In `PriceDiffFormalizationService.ensurePendingRecord(...)`, set the new fields in `PriceDiffAdjustDetailDO.builder()`: ```java .isLadder(toShortDefaultTrue(reqVO.getIsLadder())) .calculateGarbageFee(toShortDefaultTrue(reqVO.getCalculateGarbageFee())) .updateAccumulatedVolume(toShortDefaultTrue(reqVO.getUpdateAccumulatedVolume())) ``` Add helper method: ```java private Short toShortDefaultTrue(Boolean value) { return Boolean.FALSE.equals(value) ? (short) 0 : (short) 1; } ``` Use this helper for `isLadder` as well, replacing the current `Boolean.TRUE.equals(...) ? 1 : 0` default so missing values default to true. - [ ] **Step 6: Run formalization test** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PriceDiffFormalizationServiceTest#ensurePendingRecord_shouldPersistPriceDiffOptionFlags \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 7: Commit Task 3** Run: ```bash git add \ sw-business/sw-business-api/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/dto/PriceDiffAdjustCreateReqDTO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/api/accountingadjust/PriceDiffAdjustInternalApiImpl.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/accountProcess/AccountingAdjustProcessServiceImpl.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/pricediffadjustdetail/PriceDiffAdjustDetailDO.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationService.java \ sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffFormalizationServiceTest.java git commit -m "feat(accounting): persist price diff preview options" ``` ## Task 4: Apply Price-Diff Options During Write-Back **Files:** - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/AccountingAdjustActionServiceImpl.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackService.java` - Modify: `sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/charge/ChargeServiceImpl.java` - Test: `sw-business/sw-business-server/src/test/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackServiceTest.java` - [ ] **Step 1: Write failing test for calculateGarbageFee default true** In `PriceDiffWriteBackServiceTest`, add or adapt a test: ```java @Test void cancelAndCreate_shouldDefaultCalculateGarbageFeeToTrueWhenOptionMissing() { ChargeDO original = new ChargeDO(); original.setId(15801L); original.setPayState(PayStateEnum.UNPAID.getValue()); original.setBillWater(new BigDecimal("10.000")); original.setTotalWater(new BigDecimal("100.000")); when(chargeMapper.selectById(15801L)).thenReturn(original); PriceTemplateDO template = new PriceTemplateDO(); template.setCode("NEW"); template.setAdjustmentSnapCode("SNAP-NEW"); when(priceTemplateService.getPriceTemplateByCode("NEW")).thenReturn(template); when(priceDiffPreviewService.recalculate(eq(original), eq(template), eq(true), eq(true))) .thenReturn(Map.of( "101", new BigDecimal("20.00"), "103", new BigDecimal("3.00") )); Long newChargeId = service.cancelAndCreate(15801L, "NEW", "SNAP-NEW", null, null, null, "价差调整"); assertNull(newChargeId); ArgumentCaptor insertCaptor = ArgumentCaptor.forClass(ChargeDO.class); verify(chargeMapper).insert(insertCaptor.capture()); assertEquals(new BigDecimal("3.00"), insertCaptor.getValue().getGarbageFee()); verify(priceDiffPreviewService).recalculate(original, template, true, true); } ``` The unit test mock does not assign an ID during `chargeMapper.insert(newCharge)`, so `newChargeId` remains `null` in this test. - [ ] **Step 2: Write failing test for calculateGarbageFee false** Add: ```java @Test void cancelAndCreate_shouldExcludeGarbageFeeWhenOptionFalse() { ChargeDO original = new ChargeDO(); original.setId(15802L); original.setPayState(PayStateEnum.UNPAID.getValue()); original.setBillWater(new BigDecimal("10.000")); when(chargeMapper.selectById(15802L)).thenReturn(original); PriceTemplateDO template = new PriceTemplateDO(); template.setCode("NEW"); template.setAdjustmentSnapCode("SNAP-NEW"); when(priceTemplateService.getPriceTemplateByCode("NEW")).thenReturn(template); when(priceDiffPreviewService.recalculate(eq(original), eq(template), eq(false), eq(false))) .thenReturn(Map.of("101", new BigDecimal("20.00"))); service.cancelAndCreate(15802L, "NEW", "SNAP-NEW", false, false, false, "价差调整"); ArgumentCaptor insertCaptor = ArgumentCaptor.forClass(ChargeDO.class); verify(chargeMapper).insert(insertCaptor.capture()); assertEquals(BigDecimal.ZERO, insertCaptor.getValue().getGarbageFee()); verify(priceDiffPreviewService).recalculate(original, template, false, false); } ``` - [ ] **Step 3: Run tests to verify failure** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PriceDiffWriteBackServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: failure because `calculateGarbageFee` currently defaults to false in write-back when null. - [ ] **Step 4: Fix write-back default semantics** In `PriceDiffWriteBackService.cancelAndCreate(...)`, replace: ```java Map newFeeMap = priceDiffPreviewService.recalculate(original, newTemplate, isLadder, calculateGarbageFee != null && calculateGarbageFee); ``` with: ```java Boolean effectiveIsLadder = Boolean.FALSE.equals(isLadder) ? false : true; Boolean effectiveCalculateGarbageFee = Boolean.FALSE.equals(calculateGarbageFee) ? false : true; Map newFeeMap = priceDiffPreviewService.recalculate( original, newTemplate, effectiveIsLadder, effectiveCalculateGarbageFee); ``` - [ ] **Step 5: Apply updateAccumulatedVolume behavior** In `PriceDiffWriteBackService.cancelAndCreate(...)`, after `newCharge.setTotalWater(source.getTotalWater())` is copied by `cloneChargeMetadata`, set: ```java if (Boolean.FALSE.equals(updateAccumulatedVolume)) { newCharge.setTotalWater(original.getTotalWater()); } ``` Do not alter customer-level accumulated volume in this task because this service currently only clones and inserts `ChargeDO`. - [ ] **Step 6: Write price-diff option values to operation log details** In `ChargeServiceImpl.recordAccountingAdjustLog(...)`, add these log details near `priceDiffAmount`: ```java addOperatLogDetail(dto, "newPriceTemplateCode", "新水价模板代码", null, reqVO.getNewPriceTemplateCode(), EnumColumnType.NORMAL_TYPE); addOperatLogDetail(dto, "adjustmentSnapCode", "调价号", null, reqVO.getAdjustmentSnapCode(), EnumColumnType.NORMAL_TYPE); addOperatLogDetail(dto, "isLadder", "是否阶梯计费", null, reqVO.getIsLadder(), EnumColumnType.NORMAL_TYPE); addOperatLogDetail(dto, "calculateGarbageFee", "是否计算垃圾费", null, reqVO.getCalculateGarbageFee(), EnumColumnType.NORMAL_TYPE); addOperatLogDetail(dto, "updateAccumulatedVolume", "是否更新累计量", null, reqVO.getUpdateAccumulatedVolume(), EnumColumnType.NORMAL_TYPE); ``` `AccountingAdjustActionServiceImpl.applyPriceDiffWriteBack(...)` already reads these exact column names: ```java Boolean isLadder = parseBoolean(detailColumnValue(detail, "isLadder")); Boolean calculateGarbageFee = parseBoolean(detailColumnValue(detail, "calculateGarbageFee")); Boolean updateAccumulatedVolume = parseBoolean(detailColumnValue(detail, "updateAccumulatedVolume")); ``` - [ ] **Step 7: Run write-back tests** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=PriceDiffWriteBackServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 8: Commit Task 4** Run: ```bash git add \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/AccountingAdjustActionServiceImpl.java \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/service/accountingadjust/pricediff/PriceDiffWriteBackService.java \ 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/accountingadjust/pricediff/PriceDiffWriteBackServiceTest.java git commit -m "fix(accounting): align price diff option writeback" ``` ## Task 5: Add Database Migration for Price-Diff Detail Options **Files:** - Create: `sql/rev004/REV004_price_diff_option_fields_deploy.sql` - Modify: `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md` - [ ] **Step 1: Add deploy SQL** Create `sql/rev004/REV004_price_diff_option_fields_deploy.sql`: ```sql ALTER TABLE biz_price_diff_adjust_detail ADD calculate_garbage_fee SMALLINT DEFAULT 1 NULL; COMMENT ON COLUMN biz_price_diff_adjust_detail.calculate_garbage_fee IS '是否计算垃圾费:0-否,1-是'; ALTER TABLE biz_price_diff_adjust_detail ADD update_accumulated_volume SMALLINT DEFAULT 1 NULL; COMMENT ON COLUMN biz_price_diff_adjust_detail.update_accumulated_volume IS '是否更新累计量:0-否,1-是'; ``` - [ ] **Step 2: Run compile to validate entity changes** Run: ```bash mvn -pl sw-business/sw-business-server -am -DskipTests compile ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 3: Commit Task 5** Run: ```bash git add \ sql/rev004/REV004_price_diff_option_fields_deploy.sql \ sw-business/sw-business-server/src/main/java/cn/com/emsoft/sw/business/dal/dataobject/pricediffadjustdetail/PriceDiffAdjustDetailDO.java git commit -m "chore(accounting): add price diff option fields ddl" ``` ## Task 6: End-to-End Regression and Evidence **Files:** - Modify: `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md` - [ ] **Step 1: Run focused unit tests** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=AccountingAdjustProcessServiceImplTest,ChargeServiceAccountingAdjustTest,PriceDiffFormalizationServiceTest,PriceDiffWriteBackServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 2: Run existing trial preview tests** Run: ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=UnsoldTrialPreviewServiceImplTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` Expected: ```text BUILD SUCCESS ``` - [ ] **Step 3: Add evidence document** Create or update `../water-docs/docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md`: ```markdown # REV-004 未销调整预算参数正式化验证记录 ## 变更范围 - 水量调整正式提交承接 `updateAccumulated`、`updateBaseCode`。 - 价差调整正式对象承接 `isLadder`、`calculateGarbageFee`、`updateAccumulatedVolume`。 - 价差正式回写与预算使用一致默认值:未传 `isLadder` 和 `calculateGarbageFee` 时按 `true` 处理。 ## 验证命令 ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=AccountingAdjustProcessServiceImplTest,ChargeServiceAccountingAdjustTest,PriceDiffFormalizationServiceTest,PriceDiffWriteBackServiceTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` ```bash mvn -pl sw-business/sw-business-server -am \ -Dtest=UnsoldTrialPreviewServiceImplTest \ -Dsurefire.failIfNoSpecifiedTests=false test ``` ## 验证结果 - 聚焦账务调整测试:通过。 - 未销试算测试:通过。 ## 数据库变更 - 新增 `sql/rev004/REV004_price_diff_option_fields_deploy.sql`。 - `biz_price_diff_adjust_detail` 新增 `calculate_garbage_fee` 与 `update_accumulated_volume`。 ## 风险说明 - 水量调整仍使用 legacy-only 即时回写,checkbox 审计通过操作日志明细承接。 - 价差正式对象已承接 checkbox;部署前需在目标库执行新增字段 SQL。 ``` - [ ] **Step 4: Validate evidence document** Run: ```bash make validate-file FILE=docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md ``` from `../water-docs`. Expected: ```text 文档验证通过 ``` - [ ] **Step 5: Commit evidence** Run: ```bash git -C ../water-docs add docs/evidence/rev004-accounting/rev004-unsold-preview-options-formalization-2026-06-19.md git -C ../water-docs commit -m "docs(accounting): record unsold preview option evidence" ``` ## Task 7: Final Status Check **Files:** - No file edits. - [ ] **Step 1: Check backend status** Run: ```bash git status --short git log --oneline -5 ``` Expected: - No unstaged backend files. - Recent commits include usage validation, water option chain, price-diff option persistence, price-diff write-back, and DDL. - [ ] **Step 2: Check docs status** Run: ```bash git -C ../water-docs status --short git -C ../water-docs log --oneline -5 ``` Expected: - No unstaged docs files. - Recent commits include design, plan, and evidence. - [ ] **Step 3: Summarize implementation** Report: ```text Implemented REV-004 unsold preview option formalization. Backend commits: - Include the short SHA and subject for each backend commit from `git log --oneline -6`. Docs commits: - Include the short SHA and subject for each docs commit from `git -C ../water-docs log --oneline -3`. Validation: - Focused Maven command: BUILD SUCCESS - Preview Maven command: BUILD SUCCESS ``` ## Self-Review Spec coverage: - Water preview options entering formal submit: covered by Tasks 1 and 2. - Price-diff preview options entering formal object and write-back: covered by Tasks 3 and 4. - Data persistence for price-diff formal detail: covered by Task 5. - Evidence under `docs/evidence/rev004-accounting/`: covered by Tasks 0 and 6. - Validation and final status: covered by Tasks 6 and 7. Plan specificity scan: - The plan contains concrete file paths, commands, expected outputs, and code snippets for each implementation task. - No implementation step depends on unnamed additional work. Type consistency: - Water option field names are `updateAccumulated` and `updateBaseCode` across endpoint VO, unified submit VO, core charge VO, and service logic. - Price-diff option field names are `isLadder`, `calculateGarbageFee`, and `updateAccumulatedVolume` across submit VO, DTO, formal detail, and write-back.