diff --git a/docs/superpowers/plans/2026-06-19-rev004-unsold-preview-options.md b/docs/superpowers/plans/2026-06-19-rev004-unsold-preview-options.md new file mode 100644 index 0000000..86687bb --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-rev004-unsold-preview-options.md @@ -0,0 +1,1044 @@ +# 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.