33import com .dbaagent .model .DigestDeliveryMethod ;
44import com .dbaagent .model .PersonaTag ;
55import com .dbaagent .model .UserDigestPreference ;
6+ import com .dbaagent .service .DigestPreferenceSeedService ;
67import com .dbaagent .service .security .AccessControlService ;
78import com .dbaagent .service .UserDigestPreferenceService ;
9+ import com .dbaagent .service .SlackDailyDigestService ;
810import lombok .RequiredArgsConstructor ;
911import lombok .extern .slf4j .Slf4j ;
1012import org .springframework .http .ResponseEntity ;
13+ import org .springframework .security .access .prepost .PreAuthorize ;
1114import org .springframework .web .bind .annotation .*;
1215
1316import java .util .List ;
@@ -27,6 +30,8 @@ public class DigestPreferenceController {
2730
2831 private final UserDigestPreferenceService preferenceService ;
2932 private final AccessControlService accessControlService ;
33+ private final DigestPreferenceSeedService seedService ;
34+ private final SlackDailyDigestService digestService ;
3035
3136 /**
3237 * Get the current user's digest preferences.
@@ -44,9 +49,23 @@ public ResponseEntity<List<UserDigestPreference>> getMyPreferences() {
4449 public ResponseEntity <UserDigestPreference > createPreference (@ RequestBody CreatePreferenceRequest request ) {
4550 String username = accessControlService .requireCurrentUsername ();
4651
47- DigestDeliveryMethod method = request .deliveryMethod != null
48- ? DigestDeliveryMethod .fromString (request .deliveryMethod )
49- : DigestDeliveryMethod .SLACK_DM ;
52+ DigestDeliveryMethod method ;
53+ if (request .deliveryMethod == null || request .deliveryMethod .isBlank ()) {
54+ method = DigestDeliveryMethod .SLACK_DM ;
55+ } else {
56+ method = DigestDeliveryMethod .fromString (request .deliveryMethod );
57+ if (method == null ) {
58+ throw new IllegalArgumentException ("Unknown delivery method: " + request .deliveryMethod );
59+ }
60+ }
61+
62+ if (method == DigestDeliveryMethod .EMAIL ) {
63+ throw new IllegalArgumentException ("Email digest delivery is not available yet" );
64+ }
65+
66+ if (request .connectionId != null && !request .connectionId .isBlank ()) {
67+ accessControlService .assertCanReadConnectionContent (request .connectionId );
68+ }
5069
5170 PersonaTag persona = request .personaTag != null
5271 ? PersonaTag .fromString (request .personaTag )
@@ -159,10 +178,10 @@ public ResponseEntity<List<Map<String, String>>> getPersonaTags() {
159178 */
160179 @ GetMapping ("/delivery-methods" )
161180 public ResponseEntity <List <Map <String , String >>> getDeliveryMethods () {
181+ // Only advertise methods this PR actually delivers. EMAIL/WhatsApp are PR4.
162182 List <Map <String , String >> methods = List .of (
163183 Map .of ("value" , "SLACK_DM" , "label" , DigestDeliveryMethod .SLACK_DM .getDisplayName (), "description" , DigestDeliveryMethod .SLACK_DM .getDescription ()),
164- Map .of ("value" , "SLACK_CHANNEL" , "label" , DigestDeliveryMethod .SLACK_CHANNEL .getDisplayName (), "description" , DigestDeliveryMethod .SLACK_CHANNEL .getDescription ()),
165- Map .of ("value" , "EMAIL" , "label" , DigestDeliveryMethod .EMAIL .getDisplayName (), "description" , DigestDeliveryMethod .EMAIL .getDescription ())
184+ Map .of ("value" , "SLACK_CHANNEL" , "label" , DigestDeliveryMethod .SLACK_CHANNEL .getDisplayName (), "description" , DigestDeliveryMethod .SLACK_CHANNEL .getDescription ())
166185 );
167186 return ResponseEntity .ok (methods );
168187 }
@@ -181,4 +200,90 @@ public record UpdatePreferenceRequest(
181200 String cronExpression ,
182201 String timezone
183202 ) {}
203+
204+ // ─────────────────────────────────────────────────────────────────────────
205+ // Admin: Seed & Status endpoints
206+ // ─────────────────────────────────────────────────────────────────────────
207+
208+ /**
209+ * Preview what preferences would be seeded from singleton config.
210+ * Admin only.
211+ */
212+ @ GetMapping ("/admin/seed/preview" )
213+ @ PreAuthorize ("hasRole('ADMIN')" )
214+ public ResponseEntity <SeedPreviewResponse > previewSeed () {
215+ DigestPreferenceSeedService .SeedResult result = seedService .previewSeed ();
216+ return ResponseEntity .ok (new SeedPreviewResponse (
217+ result .usersProcessed (),
218+ result .preferencesCreated (),
219+ result .skipped (),
220+ result .preferences ().stream ()
221+ .map (p -> new PreferencePreview (p .getUsername (), p .getConnectionId (),
222+ p .getPersonaTag () != null ? p .getPersonaTag ().name () : null ))
223+ .toList ()
224+ ));
225+ }
226+
227+ /**
228+ * Seed preferences for all Slack-linked users from singleton config.
229+ * Admin only. Idempotent: skips users with existing preferences.
230+ */
231+ @ PostMapping ("/admin/seed" )
232+ @ PreAuthorize ("hasRole('ADMIN')" )
233+ public ResponseEntity <SeedResultResponse > executeSeed () {
234+ DigestPreferenceSeedService .SeedResult result = seedService .executeSeed ();
235+ log .info ("Admin seeded {} digest preferences for {} users" ,
236+ result .preferencesCreated (), result .usersProcessed ());
237+ return ResponseEntity .ok (new SeedResultResponse (
238+ result .usersProcessed (),
239+ result .preferencesCreated (),
240+ result .skipped ()
241+ ));
242+ }
243+
244+ /**
245+ * Seed preferences for the current user.
246+ * Available to any authenticated user.
247+ */
248+ @ PostMapping ("/seed/me" )
249+ public ResponseEntity <SeedResultResponse > seedForCurrentUser () {
250+ String username = accessControlService .requireCurrentUsername ();
251+ DigestPreferenceSeedService .SeedResult result = seedService .seedPreferencesForUser (username , false );
252+ return ResponseEntity .ok (new SeedResultResponse (
253+ 1 ,
254+ result .preferencesCreated (),
255+ result .skipped ()
256+ ));
257+ }
258+
259+ /**
260+ * Get current digest mode info.
261+ */
262+ @ GetMapping ("/status" )
263+ public ResponseEntity <DigestStatusResponse > getStatus () {
264+ SlackDailyDigestService .DigestModeInfo modeInfo = digestService .getDigestModeInfo ();
265+ return ResponseEntity .ok (new DigestStatusResponse (
266+ modeInfo .perUserMode (),
267+ modeInfo .enabledPreferences (),
268+ modeInfo .distinctUsers ()
269+ ));
270+ }
271+
272+ public record SeedPreviewResponse (
273+ int usersProcessed ,
274+ int wouldCreate ,
275+ List <String > wouldSkip ,
276+ List <PreferencePreview > preferences
277+ ) {}
278+
279+ public record PreferencePreview (String username , String connectionId , String personaTag ) {}
280+
281+ public record SeedResultResponse (int usersProcessed , int preferencesCreated , List <String > skipped ) {}
282+
283+ public record DigestStatusResponse (boolean perUserMode , long enabledPreferences , int distinctUsers ) {}
284+
285+ @ ExceptionHandler (IllegalArgumentException .class )
286+ public ResponseEntity <Map <String , String >> handleBadRequest (IllegalArgumentException e ) {
287+ return ResponseEntity .badRequest ().body (Map .of ("message" , e .getMessage () != null ? e .getMessage () : "Bad request" ));
288+ }
184289}
0 commit comments