fujian_water_biz_doc/docs/superpowers/plans/2026-06-19-rev004-unsold-preview-options.md

41 KiB
Raw Blame History

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:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=UnsoldTrialPreviewServiceImplTest,ChargeServiceAccountingAdjustTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

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:

# 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:

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:

@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<AccountingAdjustReqVO> 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:

import org.mockito.ArgumentCaptor;
  • Step 2: Run test to verify it fails

Run:

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:

@Schema(description = "是否更新累计量未传默认false", example = "true")
private Boolean updateAccumulated;

@Schema(description = "是否更新底码未传默认false", example = "true")
private Boolean updateBaseCode;

In AccountingAdjustSubmitReqVO, add near currentReading:

private Boolean updateAccumulated;

private Boolean updateBaseCode;

In AccountingAdjustReqVO, add after currentReading:

@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:

target.setUpdateAccumulated(reqVO.getUpdateAccumulated());
target.setUpdateBaseCode(reqVO.getUpdateBaseCode());

In AccountingAdjustProcessServiceImpl.buildLegacyAdjustReq(...), add:

coreReq.setUpdateAccumulated(reqVO.getUpdateAccumulated());
coreReq.setUpdateBaseCode(reqVO.getUpdateBaseCode());
  • Step 5: Run test to verify it passes

Run:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=AccountingAdjustProcessServiceImplTest#createUnsoldAdjust_shouldPassWaterOptionFlagsToUnifiedChargeService \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

BUILD SUCCESS
  • Step 6: Commit Task 1

Run:

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:

@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:

@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:

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:

if (Boolean.TRUE.equals(reqVO.getUpdateBaseCode()) && reqVO.getCurrentReading() == null) {
    throw new IllegalArgumentException("更新底码必须传入本次抄码");
}

In ChargeServiceImpl.handleUsageAdjust(...), preserve original water before recalculation:

BigDecimal beforeBillWater = state.getChargeDO().getBillWater();

After copyRecalculatedFees(recalculated, state.getChargeDO());, add:

applyUsageAdjustOptions(state.getChargeDO(), state.getReqVO(), beforeBillWater);

Add helper method near copyRecalculatedFees:

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:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=ChargeServiceAccountingAdjustTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

BUILD SUCCESS
  • Step 7: Commit Task 2

Run:

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:

@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<PriceDiffAdjustDetailDO> 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:

import org.mockito.ArgumentCaptor;
  • Step 2: Run test to verify it fails

Run:

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:

private Boolean calculateGarbageFee;
private Boolean updateAccumulatedVolume;

In PriceDiffAdjustDetailDO, add:

private Short calculateGarbageFee;
private Short updateAccumulatedVolume;
  • Step 4: Pass options into formalization VO

In AccountingAdjustProcessServiceImpl.createUnsoldPriceDiff(...), after dto.setIsLadder(reqVO.getIsLadder());, add:

dto.setCalculateGarbageFee(reqVO.getCalculateGarbageFee());
dto.setUpdateAccumulatedVolume(reqVO.getUpdateAccumulatedVolume());

In PriceDiffAdjustInternalApiImpl.create(...), after vo.setIsLadder(reqDTO.getIsLadder());, add:

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():

.isLadder(toShortDefaultTrue(reqVO.getIsLadder()))
.calculateGarbageFee(toShortDefaultTrue(reqVO.getCalculateGarbageFee()))
.updateAccumulatedVolume(toShortDefaultTrue(reqVO.getUpdateAccumulatedVolume()))

Add helper method:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PriceDiffFormalizationServiceTest#ensurePendingRecord_shouldPersistPriceDiffOptionFlags \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

BUILD SUCCESS
  • Step 7: Commit Task 3

Run:

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:

@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<ChargeDO> 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:

@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<ChargeDO> 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:

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:

Map<String, BigDecimal> newFeeMap = priceDiffPreviewService.recalculate(original, newTemplate, isLadder, calculateGarbageFee != null && calculateGarbageFee);

with:

Boolean effectiveIsLadder = Boolean.FALSE.equals(isLadder) ? false : true;
Boolean effectiveCalculateGarbageFee = Boolean.FALSE.equals(calculateGarbageFee) ? false : true;
Map<String, BigDecimal> 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:

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:

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:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=PriceDiffWriteBackServiceTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

BUILD SUCCESS
  • Step 8: Commit Task 4

Run:

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:

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:

mvn -pl sw-business/sw-business-server -am -DskipTests compile

Expected:

BUILD SUCCESS
  • Step 3: Commit Task 5

Run:

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:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=AccountingAdjustProcessServiceImplTest,ChargeServiceAccountingAdjustTest,PriceDiffFormalizationServiceTest,PriceDiffWriteBackServiceTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

BUILD SUCCESS
  • Step 2: Run existing trial preview tests

Run:

mvn -pl sw-business/sw-business-server -am \
  -Dtest=UnsoldTrialPreviewServiceImplTest \
  -Dsurefire.failIfNoSpecifiedTests=false test

Expected:

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:

# 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
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_feeupdate_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:

文档验证通过
  • Step 5: Commit evidence

Run:

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:

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:

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:

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.