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

1045 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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:
```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<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:
```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<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:
```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<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:
```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<String, BigDecimal> 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<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:
```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.