# Bind Schedule Device Dispatch 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:** When the app binds a schedule to one or more devices, immediately publish that schedule's detail payload to each bound device.

**Architecture:** Keep the change inside the existing app module flow. `AppController.addScheduleDevice` remains the entry point, persists the relation as it does today, then reuses the existing `IDeviceCommandService.sendScheduleBindCommand(...)` path to publish a `bindSchedule` command with a small payload assembled from current schedule-detail data. Shared payload shaping should stay private to `AppController` unless implementation pressure proves otherwise.

**Tech Stack:** Java 17, Spring Boot 3, existing app controller/service layer, MQTT command publishing through `IDeviceCommandService`, Maven

## Global Constraints

- Only touch the schedule-device binding flow in `water-modules/water-app`
- Reuse existing `IDeviceCommandService.sendScheduleBindCommand(...)`
- Do not modify MQTT publisher, ACK handling, or topic routing
- Published payload must contain `deviceNo` and `details`
- `details` must follow the current device-facing schedule detail structure used by `buildScheduleBindPayload`
- Binding uses synchronous failure semantics: command publish failure must fail the API call
- Keep changes ASCII unless the target file already uses non-ASCII

---

### Task 1: Add a focused regression test for schedule-device bind dispatch

**Files:**
- Create: `water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java`
- Modify: `water-modules/water-app/pom.xml`

**Interfaces:**
- Consumes: `AppController.addScheduleDevice(String body)`
- Produces: a regression test proving that binding a device triggers `deviceCommandService.sendScheduleBindCommand(deviceNo, payload)`

- [ ] **Step 1: Add the test dependency baseline for the app module**

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
```

- [ ] **Step 2: Write the failing controller regression test**

```java
package org.dromara.app.controller;

import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.vo.AppScheduleDetailVo;
import org.dromara.app.domain.vo.AppScheduleVo;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppScheduleDetailService;
import org.dromara.app.service.IAppScheduleService;
import org.dromara.app.service.IAppSchedulingDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.common.core.domain.R;
import org.dromara.system.service.ISysUserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class AppControllerTest {

    @Mock private IAppDeviceService appDeviceService;
    @Mock private IAppScheduleService appScheduleService;
    @Mock private IAppScheduleDetailService appScheduleDetailService;
    @Mock private org.dromara.app.service.impl.AppScheduleServiceImpl appScheduleServiceimpl;
    @Mock private IAppSchedulingDeviceService appSchedulingDeviceService;
    @Mock private ISysUserService userService;
    @Mock private IAppWateringLogService appWateringLogService;
    @Mock private IDeviceCommandService deviceCommandService;

    @InjectMocks
    private AppController controller;

    @Test
    void addScheduleDevice_dispatchesSchedulePayloadAfterBinding() {
        AppScheduleVo schedule = new AppScheduleVo();
        schedule.setId(10L);
        schedule.setUserId(99L);
        schedule.setName("Morning");
        schedule.setStatus("1");

        AppDeviceVo device = new AppDeviceVo();
        device.setDeviceNo("D01");
        device.setUserId(99L);

        AppScheduleDetailVo detail = new AppScheduleDetailVo();
        detail.setId(101L);
        detail.setWeekday("1");
        detail.setTimeData("[{\"startTime\":\"08:00\",\"durationMin\":15}]");
        detail.setTriggerType("0");
        detail.setStatus("1");

        when(appScheduleService.queryById(10L)).thenReturn(schedule);
        when(appDeviceService.queryById("D01")).thenReturn(device);
        when(appScheduleDetailService.queryByScheduleIdByStatus(10L)).thenReturn(List.of(detail));
        when(appSchedulingDeviceService.insertByBo(any(AppSchedulingDeviceBo.class))).thenReturn(true);
        when(deviceCommandService.sendScheduleBindCommand(eq("D01"), anyMap())).thenReturn("cmd-1");

        R<Void> result = controller.addScheduleDevice("{\"scheduleId\":10,\"deviceNos\":[\"D01\"]}");

        assertThat(result.getCode()).isEqualTo(200);

        ArgumentCaptor<Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(Map.class);
        verify(deviceCommandService).sendScheduleBindCommand(eq("D01"), payloadCaptor.capture());
        Map<String, Object> payload = payloadCaptor.getValue();
        assertThat(payload.get("deviceNo")).isEqualTo("D01");
        assertThat(payload).containsKey("details").doesNotContainKeys("cmd", "schedule");
    }
}
```

- [ ] **Step 3: Run the test to verify it fails for the expected reason**

Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test`

Expected: FAIL because `sendScheduleBindCommand(...)` is never invoked and payload assertions cannot be satisfied yet

- [ ] **Step 4: Commit the failing test baseline**

```bash
git add water-modules/water-app/pom.xml water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
git commit -m "test: cover schedule bind device dispatch"
```

### Task 2: Implement schedule payload assembly and dispatch in AppController

**Files:**
- Modify: `water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java`
- Test: `water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java`

**Interfaces:**
- Consumes: `IAppScheduleService.queryById(Long)`, `IAppScheduleDetailService.queryByScheduleIdByStatus(Long)`, `IDeviceCommandService.sendScheduleBindCommand(String, Map<String, Object>)`
- Produces: `addScheduleDevice(...)` publishes a `bindSchedule` command after each successful relation insert

- [ ] **Step 1: Implement the minimum controller change to make the test pass**

```java
for (String deviceNo : deviceNos) {
    if (StringUtils.isBlank(deviceNo)) {
        continue;
    }
    assertDeviceOwned(deviceNo);
    AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
    schedulingDeviceBo.setDeviceNo(deviceNo);
    schedulingDeviceBo.setScheduleId(scheduleId);
    appSchedulingDeviceService.insertByBo(schedulingDeviceBo);
    deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo));
}
```

```java
private Map<String, Object> buildScheduleBindPayload(Long scheduleId, String deviceNo) {
    List<AppScheduleDetailVo> details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId);

    List<Map<String, Object>> detailPayload = new ArrayList<>();
    for (AppScheduleDetailVo detail : details) {
        List<Object> timeSlots = new ArrayList<>();
        JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData());
        for (Object timeSlot : timeArray) {
            timeSlots.add(timeSlot);
        }

        Map<String, Object> item = new HashMap<>();
        item.put("weekday", detail.getWeekday());
        item.put("timeData", timeSlots);
        item.put("triggerType", detail.getTriggerType());
        item.put("status", detail.getStatus());
        detailPayload.add(item);
    }

    Map<String, Object> payload = new HashMap<>();
    payload.put("deviceNo", deviceNo);
    payload.put("details", detailPayload);
    return payload;
}
```

- [ ] **Step 2: Run the focused regression test to verify it passes**

Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test`

Expected: PASS with `AppControllerTest` green

- [ ] **Step 3: Refactor only if needed to remove duplication inside schedule bind payload shaping**

```java
private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) {
    List<Map<String, Object>> detailPayload = new ArrayList<>();
    for (AppScheduleDetailVo detail : details) {
        List<Object> timeSlots = new ArrayList<>();
        JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData());
        for (Object timeSlot : timeArray) {
            timeSlots.add(timeSlot);
        }
        Map<String, Object> item = new HashMap<>();
        item.put("weekday", detail.getWeekday());
        item.put("timeData", timeSlots);
        item.put("triggerType", detail.getTriggerType());
        item.put("status", detail.getStatus());
        detailPayload.add(item);
    }
    return detailPayload;
}
```

- [ ] **Step 4: Re-run the focused regression test after refactor**

Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test`

Expected: PASS

- [ ] **Step 5: Commit the implementation**

```bash
git add water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java
git commit -m "feat: dispatch schedule payload when binding device"
```

### Task 3: Run module-level verification and review edge behavior

**Files:**
- Modify: none required unless verification reveals defects

**Interfaces:**
- Consumes: completed controller change and regression test
- Produces: fresh verification evidence for the implementation claim

- [ ] **Step 1: Run the focused controller regression test**

Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test`

Expected: PASS

- [ ] **Step 2: Run a compile pass for the app module with tests skipped**

Run: `mvn -pl water-modules/water-app -DskipTests compile`

Expected: BUILD SUCCESS

- [ ] **Step 3: Inspect the diff to confirm scope stayed local**

Run: `git diff --stat`

Expected: only the app controller, app module test baseline, the new controller test, and planning/spec docs changed for this task

- [ ] **Step 4: Commit any verification-driven follow-up if needed**

```bash
git add -A
git commit -m "chore: verify schedule bind dispatch change"
```
