Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.patinanetwork.patchats.api.match;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.patinanetwork.patchats.api.match.dto.match.AdminMatchResponse;
import org.patinanetwork.patchats.api.match.dto.match.CreateMatchRequest;
import org.patinanetwork.patchats.common.dto.ApiResponder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin/matches")
@Tag(name = "Admin Matches")
@RequiredArgsConstructor
public class AdminMatchController {

private final MatchService matchService;

@Operation(summary = "Create a match between two members for a match cycle")
@PostMapping
public ResponseEntity<ApiResponder<AdminMatchResponse>> createMatch(
@Valid @RequestBody final CreateMatchRequest request) {
final AdminMatchResponse response = matchService.createMatch(request);
return ResponseEntity.ok(ApiResponder.success("Match created successfully", response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.patinanetwork.patchats.api.match;

import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.patinanetwork.patchats.api.match.db.models.Match;
import org.patinanetwork.patchats.api.match.db.models.MatchCycle;
import org.patinanetwork.patchats.api.match.db.repos.MatchCycleRepo;
import org.patinanetwork.patchats.api.match.db.repos.MatchRepo;
import org.patinanetwork.patchats.api.match.dto.match.AdminMatchResponse;
import org.patinanetwork.patchats.api.match.dto.match.CreateMatchRequest;
import org.patinanetwork.patchats.common.web.exception.MatchCycleNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MatchService {

private static final String DEFAULT_MATCH_STATUS = "PENDING";
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");

private final MatchRepo matchRepo;
private final MatchCycleRepo matchCycleRepo;

public AdminMatchResponse createMatch(CreateMatchRequest request) {
MatchCycle cycle = matchCycleRepo
.getMatchCycleById(request.matchCycleId())
.orElseThrow(() -> new MatchCycleNotFoundException(request.matchCycleId()));

Match match = Match.builder()
.id(UUID.randomUUID())
.memberAId(request.memberAId())
.memberBId(request.memberBId())
.matchCycleId(request.matchCycleId())
.matchScore(request.matchScore())
.status(request.status() == null ? DEFAULT_MATCH_STATUS : request.status())
.build();

Match createdMatch = matchRepo.createMatch(match);
return AdminMatchResponse.from(createdMatch, deriveMonth(cycle));
}

/** Derives the "YYYY-MM" month label for a match from its cycle's run time (UTC). */
private String deriveMonth(final MatchCycle cycle) {
return MONTH_FORMATTER.format(cycle.getRunAt().atZone(ZoneOffset.UTC));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.patinanetwork.patchats.api.match.dto.match;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.UUID;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.patinanetwork.patchats.api.match.db.models.Match;

@Getter
@Builder
@ToString
@EqualsAndHashCode
public class AdminMatchResponse {

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID matchId;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID memberAId;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final UUID memberBId;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final Integer matchCycleId;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final String month;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final String status;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED, nullable = true)
private final Double matchScore;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private final Instant createdAt;

public static AdminMatchResponse from(final Match match, final String month) {
return AdminMatchResponse.builder()
.matchId(match.getId())
.memberAId(match.getMemberAId())
.memberBId(match.getMemberBId())
.matchCycleId(match.getMatchCycleId())
.month(month)
.status(match.getStatus())
.matchScore(match.getMatchScore())
.createdAt(match.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.patinanetwork.patchats.api.match.dto.match;

import jakarta.validation.constraints.NotNull;
import java.util.UUID;

public record CreateMatchRequest(
@NotNull UUID memberAId,
@NotNull UUID memberBId,
@NotNull Integer matchCycleId,
Double matchScore,
String status) {}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.patinanetwork.patchats.common.web.exception.EmailNotFoundException;
import org.patinanetwork.patchats.common.web.exception.EmailNotResendableException;
import org.patinanetwork.patchats.common.web.exception.EmailTemplateNotFoundException;
import org.patinanetwork.patchats.common.web.exception.MatchCycleNotFoundException;
import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException;
import org.patinanetwork.patchats.common.web.exception.MemberNotFoundException;
import org.patinanetwork.patchats.common.web.exception.ValidationException;
Expand All @@ -24,73 +25,78 @@
@RestControllerAdvice
public class ApiExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponder<Void>> handleValidation(final MethodArgumentNotValidException ex) {
final String details = ex.getBindingResult().getFieldErrors().stream()
.map(this::formatError)
.collect(Collectors.joining("; "));
final String message = details.isBlank() ? "Validation failed" : details;
return ResponseEntity.badRequest().body(ApiResponder.failure(message));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResponder<Void>> handleParameterTypeMismatch(
final MethodArgumentTypeMismatchException ex) {
final String message = "Invalid value for query parameter '" + ex.getName() + "'";
return ResponseEntity.badRequest().body(ApiResponder.failure(message));
}

@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ApiResponder<Void>> handleParameterValidation(final HandlerMethodValidationException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure("Invalid query parameters"));
}

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleMemberNotFound(final MemberNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(MemberDuplicateException.class)
public ResponseEntity<ApiResponder<Void>> handleMemberDuplicate(final MemberDuplicateException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailTemplateNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailTemplateNotFound(final EmailTemplateNotFoundException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailNotFound(final EmailNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailNotResendableException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailNotResendable(final EmailNotResendableException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiResponder<Void>> handleValidation(ValidationException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(InvalidMagicLinkException.class)
public ResponseEntity<ApiResponder<Void>> handleInvalidMagicLink(final InvalidMagicLinkException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(UnregisteredEmailException.class)
public ResponseEntity<ApiResponder<Void>> handleUnregisteredEmail(final UnregisteredEmailException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(TooManyLinkRequestsException.class)
public ResponseEntity<ApiResponder<Void>> handleTooManyLinkRequests(final TooManyLinkRequestsException ex) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ApiResponder.failure(ex.getMessage()));
}

private String formatError(final FieldError error) {
return error.getField() + " " + error.getDefaultMessage();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponder<Void>> handleValidation(final MethodArgumentNotValidException ex) {
final String details = ex.getBindingResult().getFieldErrors().stream()
.map(this::formatError)
.collect(Collectors.joining("; "));
final String message = details.isBlank() ? "Validation failed" : details;
return ResponseEntity.badRequest().body(ApiResponder.failure(message));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResponder<Void>> handleParameterTypeMismatch(
final MethodArgumentTypeMismatchException ex) {
final String message = "Invalid value for query parameter '" + ex.getName() + "'";
return ResponseEntity.badRequest().body(ApiResponder.failure(message));
}

@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ApiResponder<Void>> handleParameterValidation(final HandlerMethodValidationException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure("Invalid query parameters"));
}

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleMemberNotFound(final MemberNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(MemberDuplicateException.class)
public ResponseEntity<ApiResponder<Void>> handleMemberDuplicate(final MemberDuplicateException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailTemplateNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailTemplateNotFound(final EmailTemplateNotFoundException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailNotFound(final EmailNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(EmailNotResendableException.class)
public ResponseEntity<ApiResponder<Void>> handleEmailNotResendable(final EmailNotResendableException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiResponder<Void>> handleValidation(ValidationException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(InvalidMagicLinkException.class)
public ResponseEntity<ApiResponder<Void>> handleInvalidMagicLink(final InvalidMagicLinkException ex) {
return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(UnregisteredEmailException.class)
public ResponseEntity<ApiResponder<Void>> handleUnregisteredEmail(final UnregisteredEmailException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(TooManyLinkRequestsException.class)
public ResponseEntity<ApiResponder<Void>> handleTooManyLinkRequests(final TooManyLinkRequestsException ex) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ApiResponder.failure(ex.getMessage()));
}

@ExceptionHandler(MatchCycleNotFoundException.class)
public ResponseEntity<ApiResponder<Void>> handleMatchCycleNotFound(final MatchCycleNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponder.failure(ex.getMessage()));
}

private String formatError(final FieldError error) {
return error.getField() + " " + error.getDefaultMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.patinanetwork.patchats.common.web.exception;

public class MatchCycleNotFoundException extends RuntimeException {
public MatchCycleNotFoundException(Integer id) {
super("Match Cycle with ID " + id + " not found");
}
}
Loading