feat(sprint-3): Phase 2 — club settings controller

This commit is contained in:
Patrick Plate
2026-06-11 16:56:44 +02:00
parent 55d8434f35
commit 36deb72cf0
15 changed files with 700 additions and 0 deletions
@@ -0,0 +1,94 @@
package de.cannamanage.api.controller;
import de.cannamanage.api.dto.club.ClubResponse;
import de.cannamanage.api.dto.club.ClubStatsResponse;
import de.cannamanage.api.dto.club.UpdateClubRequest;
import de.cannamanage.domain.entity.Club;
import de.cannamanage.domain.entity.TenantContext;
import de.cannamanage.service.ClubService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/clubs")
@RequiredArgsConstructor
@Tag(name = "Club Settings", description = "Club configuration and statistics")
public class ClubController {
private final ClubService clubService;
@GetMapping("/me")
@Operation(summary = "Get current club", description = "Returns the club for the current tenant")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ClubResponse> getMyClub() {
UUID tenantId = TenantContext.getCurrentTenant();
Club club = clubService.getClubByTenantId(tenantId);
return ResponseEntity.ok(toResponse(club));
}
@PutMapping("/me")
@Operation(summary = "Update club settings", description = "Updates the club configuration for the current tenant")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ClubResponse> updateMyClub(@Valid @RequestBody UpdateClubRequest request) {
UUID tenantId = TenantContext.getCurrentTenant();
Club updated = clubService.updateClub(
tenantId,
request.name(),
request.registrationNumber(),
request.contactEmail(),
request.contactPhone(),
request.addressStreet(),
request.addressCity(),
request.addressPostalCode(),
request.addressState(),
request.foundedDate(),
request.maxPreventionOfficers(),
request.allowedEmailPattern()
);
return ResponseEntity.ok(toResponse(updated));
}
@GetMapping("/me/stats")
@Operation(summary = "Get club statistics", description = "Returns aggregated club statistics")
@PreAuthorize("hasRole('ADMIN') or @staffPermissions.has(authentication, T(de.cannamanage.domain.enums.StaffPermission).VIEW_COMPLIANCE_REPORT)")
public ResponseEntity<ClubStatsResponse> getMyClubStats() {
UUID tenantId = TenantContext.getCurrentTenant();
ClubService.ClubStats stats = clubService.getClubStats(tenantId);
return ResponseEntity.ok(new ClubStatsResponse(
stats.totalMembers(),
stats.activeMembers(),
stats.totalStaff(),
stats.activeStaff(),
stats.totalDistributionsThisMonth(),
stats.totalGramsDistributedThisMonth(),
stats.activeBatches(),
stats.preventionOfficerCount()
));
}
private ClubResponse toResponse(Club club) {
return new ClubResponse(
club.getId(),
club.getName(),
club.getRegistrationNumber(),
club.getContactEmail(),
club.getContactPhone(),
club.getAddressStreet(),
club.getAddressCity(),
club.getAddressPostalCode(),
club.getAddressState(),
club.getFoundedDate(),
club.getMaxPreventionOfficers(),
club.getAllowedEmailPattern(),
club.getStatus(),
club.getCreatedAt()
);
}
}
@@ -0,0 +1,24 @@
package de.cannamanage.api.dto.club;
import de.cannamanage.domain.enums.ClubStatus;
import java.time.Instant;
import java.time.LocalDate;
import java.util.UUID;
public record ClubResponse(
UUID id,
String name,
String registrationNumber,
String contactEmail,
String contactPhone,
String addressStreet,
String addressCity,
String addressPostalCode,
String addressState,
LocalDate foundedDate,
Integer maxPreventionOfficers,
String allowedEmailPattern,
ClubStatus status,
Instant createdAt
) {}
@@ -0,0 +1,14 @@
package de.cannamanage.api.dto.club;
import java.math.BigDecimal;
public record ClubStatsResponse(
long totalMembers,
long activeMembers,
long totalStaff,
long activeStaff,
long totalDistributionsThisMonth,
BigDecimal totalGramsDistributedThisMonth,
long activeBatches,
long preventionOfficerCount
) {}
@@ -0,0 +1,34 @@
package de.cannamanage.api.dto.club;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDate;
public record UpdateClubRequest(
@NotBlank(message = "Club name is required")
String name,
String registrationNumber,
@Email(message = "Must be a valid email address")
String contactEmail,
String contactPhone,
String addressStreet,
String addressCity,
String addressPostalCode,
String addressState,
LocalDate foundedDate,
@Min(value = 1, message = "Must have at least 1 prevention officer slot")
Integer maxPreventionOfficers,
String allowedEmailPattern
) {}
@@ -0,0 +1,12 @@
-- Sprint 3 Phase 2: Club settings extended columns
-- Additional address fields, contact info, and allowed email pattern for clubs
ALTER TABLE clubs ADD COLUMN registration_number VARCHAR(100);
ALTER TABLE clubs ADD COLUMN contact_email VARCHAR(255);
ALTER TABLE clubs ADD COLUMN contact_phone VARCHAR(50);
ALTER TABLE clubs ADD COLUMN address_street VARCHAR(255);
ALTER TABLE clubs ADD COLUMN address_city VARCHAR(100);
ALTER TABLE clubs ADD COLUMN address_postal_code VARCHAR(20);
ALTER TABLE clubs ADD COLUMN address_state VARCHAR(100);
ALTER TABLE clubs ADD COLUMN founded_date DATE;
ALTER TABLE clubs ADD COLUMN allowed_email_pattern VARCHAR(255);
@@ -0,0 +1,113 @@
package de.cannamanage.api.controller;
import de.cannamanage.api.dto.club.UpdateClubRequest;
import de.cannamanage.domain.entity.Club;
import de.cannamanage.domain.entity.TenantContext;
import de.cannamanage.domain.enums.ClubStatus;
import de.cannamanage.service.ClubService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ClubControllerTest {
@Mock
private ClubService clubService;
@InjectMocks
private ClubController clubController;
private UUID tenantId;
private Club club;
@BeforeEach
void setUp() {
tenantId = UUID.randomUUID();
TenantContext.setCurrentTenant(tenantId);
club = new Club();
club.setId(UUID.randomUUID());
club.setTenantId(tenantId);
club.setName("Green Garden Club");
club.setRegistrationNumber("REG-2024-001");
club.setContactEmail("info@greengardenclub.de");
club.setContactPhone("+49 30 12345678");
club.setAddressStreet("Hanfweg 42");
club.setAddressCity("Berlin");
club.setAddressPostalCode("10115");
club.setAddressState("Berlin");
club.setFoundedDate(LocalDate.of(2024, 7, 1));
club.setMaxPreventionOfficers(2);
club.setAllowedEmailPattern(".*@greengardenclub\\.de");
club.setStatus(ClubStatus.ACTIVE);
club.setCreatedAt(Instant.now());
club.setLicenseNumber("LIC-001");
}
@AfterEach
void tearDown() {
TenantContext.clear();
}
@Test
void getMyClub_returnsClubResponse() {
when(clubService.getClubByTenantId(tenantId)).thenReturn(club);
ResponseEntity<?> response = clubController.getMyClub();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
verify(clubService).getClubByTenantId(tenantId);
}
@Test
void updateMyClub_updatesAndReturns() {
UpdateClubRequest request = new UpdateClubRequest(
"Updated Club", "REG-NEW", "new@club.de", "+49111",
"Newstreet 1", "Hamburg", "20095", "Hamburg",
LocalDate.of(2024, 1, 1), 3, ".*@club\\.de"
);
when(clubService.updateClub(
eq(tenantId), eq("Updated Club"), eq("REG-NEW"),
eq("new@club.de"), eq("+49111"),
eq("Newstreet 1"), eq("Hamburg"), eq("20095"), eq("Hamburg"),
eq(LocalDate.of(2024, 1, 1)), eq(3), eq(".*@club\\.de")
)).thenReturn(club);
ResponseEntity<?> response = clubController.updateMyClub(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
@Test
void getMyClubStats_returnsStats() {
ClubService.ClubStats stats = new ClubService.ClubStats(
50, 42, 5, 4, 120, new BigDecimal("1500.50"), 8, 2
);
when(clubService.getClubStats(tenantId)).thenReturn(stats);
ResponseEntity<?> response = clubController.getMyClubStats();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
verify(clubService).getClubStats(tenantId);
}
}