merge(iteration-1): adopt attendance lombok conventions

This commit is contained in:
sechmachine
2026-08-15 11:51:46 +07:00
14 changed files with 543 additions and 78 deletions
@@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationServ
import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService;
import java.security.Principal;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
@@ -22,17 +24,12 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/attendance")
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class AttendanceController {
private final AttendanceApplicationService attendance;
private final AttendanceCurrentUserService currentUsers;
AttendanceController(
AttendanceApplicationService attendance, AttendanceCurrentUserService currentUsers) {
this.attendance = attendance;
this.currentUsers = currentUsers;
}
/**
* Renders the authenticated Intern's inclusive attendance history, defaulting to the current month.
*
@@ -7,6 +7,8 @@ import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserServ
import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService;
import java.security.Principal;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
@@ -23,21 +25,13 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/attendance/calendar")
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class CalendarController {
private final CalendarApplicationService calendar;
private final AttendanceApplicationService attendance;
private final AttendanceCurrentUserService currentUsers;
CalendarController(
CalendarApplicationService calendar,
AttendanceApplicationService attendance,
AttendanceCurrentUserService currentUsers) {
this.calendar = calendar;
this.attendance = attendance;
this.currentUsers = currentUsers;
}
/**
* Renders the next year of locally stored calendar events for an authenticated Admin.
*
@@ -19,12 +19,15 @@ import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA mapping of an immutable-on-effective attendance policy version and its configured workdays.
*/
@Entity
@Table(name = "attendance_policy_versions")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AttendancePolicyEntity {
@Id
@@ -65,11 +68,6 @@ public class AttendancePolicyEntity {
@Version
private long version;
/**
* Required by JPA; application code resolves existing effective-dated versions instead of constructing them here.
*/
protected AttendancePolicyEntity() {}
/**
* Converts the persisted version to the immutable policy used for historical boundary calculations.
*
@@ -13,12 +13,15 @@ import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.Instant;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA persistence model for one Intern/work-date punch row with its permanently attached policy version.
*/
@Entity
@Table(name = "attendance_records")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AttendanceRecordEntity {
@Id
@@ -44,11 +47,6 @@ public class AttendanceRecordEntity {
@Version
private long version;
/**
* Required by JPA.
*/
protected AttendanceRecordEntity() {}
/**
* Creates a new persistence row from server-authoritative raw punch values.
*
@@ -10,12 +10,15 @@ import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA model for the locally authoritative global calendar decision.
*/
@Entity
@Table(name = "global_calendar_events")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GlobalCalendarEventEntity {
@Id
@@ -43,11 +46,6 @@ public class GlobalCalendarEventEntity {
@Version
private long version;
/**
* Required by JPA.
*/
protected GlobalCalendarEventEntity() {}
/**
* Creates a custom calendar event attributed to the Admin actor.
*
@@ -9,12 +9,15 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* JPA mapping of an immutable leave-day allocation whose exact date, policy, and quota snapshot remain historical.
*/
@Entity
@Table(name = "leave_request_days")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LeaveRequestDayEntity {
@EmbeddedId
@@ -35,11 +38,6 @@ public class LeaveRequestDayEntity {
@Column(name = "monthly_quota_snapshot", nullable = false)
private int monthlyQuotaSnapshot;
/**
* Required by JPA.
*/
protected LeaveRequestDayEntity() {}
LeaveRequestDayEntity(
LeaveRequestEntity request,
LocalDate leaveDate,
@@ -5,11 +5,14 @@ import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Composite identifier of one frozen quota-consuming date within a leave request.
*/
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LeaveRequestDayId implements Serializable {
/** Parent request identity used by the composite primary key. */
@@ -20,11 +23,6 @@ public class LeaveRequestDayId implements Serializable {
@Column(name = "leave_date", nullable = false)
private LocalDate leaveDate;
/**
* Required by JPA.
*/
protected LeaveRequestDayId() {}
/**
* Creates the identity for an already-persisted request and its exact allocated date.
*
@@ -9,12 +9,15 @@ import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.Instant;
import java.time.LocalDate;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Minimal Attendance-owned JPA mapping of leave request state used when evaluating frozen leave-day allocations.
*/
@Entity
@Table(name = "leave_requests")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LeaveRequestEntity {
@Id
@@ -51,11 +54,6 @@ public class LeaveRequestEntity {
@Version
private long version;
/**
* Required by JPA.
*/
protected LeaveRequestEntity() {}
LeaveRequestEntity(
long internUserId,
LocalDate startDate,
@@ -20,9 +20,11 @@ import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.springframework.security.access.AccessDeniedException;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
* Account eligibility is obtained only through {@link AccountService}; raw rows retain their attached policy.
*/
@Service
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class AttendanceApplicationService {
private final Clock clock;
@@ -41,23 +44,6 @@ public class AttendanceApplicationService {
private final CalendarApplicationService calendar;
private final AttendanceService attendance;
AttendanceApplicationService(
Clock clock,
AttendancePolicyRepository policyEntities,
AttendanceRecordRepository recordEntities,
AttendanceQueryRepository queries,
AccountService accounts,
CalendarApplicationService calendar,
AttendanceService attendance) {
this.clock = clock;
this.policyEntities = policyEntities;
this.recordEntities = recordEntities;
this.queries = queries;
this.accounts = accounts;
this.calendar = calendar;
this.attendance = attendance;
}
/**
* Records the sole server-time check-in for the effective policy-local date.
* Eligibility, workday, calendar, and exact frozen leave allocation are evaluated in the transaction;
@@ -6,6 +6,8 @@ import com.lab.labtimesheet.feature.account.service.AccountService;
import com.lab.labtimesheet.feature.attendance.model.AttendanceActor;
import com.lab.labtimesheet.feature.attendance.model.AttendanceRole;
import java.security.Principal;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
@@ -13,14 +15,11 @@ import org.springframework.stereotype.Service;
* Converts Spring Security principals into active Attendance authorization contexts through AccountService DTOs.
*/
@Service
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class AttendanceCurrentUserService {
private final AccountService accounts;
AttendanceCurrentUserService(AccountService accounts) {
this.accounts = accounts;
}
/**
* Resolves the authenticated email through the Account feature and rejects missing or inactive identities.
*
@@ -8,16 +8,17 @@ import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Pure attendance punch rules over immutable policy, date context, and raw record values.
*/
@Service
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public final class AttendanceService {
AttendanceService() {}
/**
* Creates the sole raw check-in for an eligible Intern/date using the supplied server instant.
* Equality at the grace boundary is accepted; violation classification remains attached-policy based.
@@ -12,6 +12,8 @@ import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRep
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -20,21 +22,13 @@ import org.springframework.transaction.annotation.Transactional;
* Transactional boundary for the locally authoritative global calendar and its cross-feature day-off decision.
*/
@Service
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class CalendarApplicationService {
private final Clock clock;
private final AttendancePolicyRepository policies;
private final GlobalCalendarEventRepository events;
CalendarApplicationService(
Clock clock,
AttendancePolicyRepository policies,
GlobalCalendarEventRepository events) {
this.clock = clock;
this.policies = policies;
this.events = events;
}
/**
* Creates an Admin-authored custom event on a non-past policy-local date.
*
@@ -0,0 +1,398 @@
package com.lab.labtimesheet.feature.attendance;
import static org.assertj.core.api.Assertions.assertThat;
import com.lab.labtimesheet.feature.account.service.AccountService;
import com.lab.labtimesheet.feature.attendance.controller.AttendanceController;
import com.lab.labtimesheet.feature.attendance.controller.CalendarController;
import com.lab.labtimesheet.feature.attendance.model.AttendanceActor;
import com.lab.labtimesheet.feature.attendance.model.AttendanceDayContext;
import com.lab.labtimesheet.feature.attendance.model.AttendancePolicy;
import com.lab.labtimesheet.feature.attendance.model.AttendanceRecord;
import com.lab.labtimesheet.feature.attendance.model.AttendanceRole;
import com.lab.labtimesheet.feature.attendance.model.AttendanceViolations;
import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceCurrentState;
import com.lab.labtimesheet.feature.attendance.model.dto.AttendanceHistoryItem;
import com.lab.labtimesheet.feature.attendance.model.dto.GlobalCalendarEvent;
import com.lab.labtimesheet.feature.attendance.model.entity.AttendancePolicyEntity;
import com.lab.labtimesheet.feature.attendance.model.entity.AttendanceRecordEntity;
import com.lab.labtimesheet.feature.attendance.model.entity.GlobalCalendarEventEntity;
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayEntity;
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestDayId;
import com.lab.labtimesheet.feature.attendance.model.entity.LeaveRequestEntity;
import com.lab.labtimesheet.feature.attendance.repository.AttendancePolicyRepository;
import com.lab.labtimesheet.feature.attendance.repository.AttendanceQueryRepository;
import com.lab.labtimesheet.feature.attendance.repository.AttendanceRecordRepository;
import com.lab.labtimesheet.feature.attendance.repository.GlobalCalendarEventRepository;
import com.lab.labtimesheet.feature.attendance.service.AttendanceApplicationService;
import com.lab.labtimesheet.feature.attendance.service.AttendanceCurrentUserService;
import com.lab.labtimesheet.feature.attendance.service.AttendanceService;
import com.lab.labtimesheet.feature.attendance.service.CalendarApplicationService;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.security.Principal;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.ui.Model;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
class AttendanceLombokBoilerplateTest {
private static final int PACKAGE_PRIVATE = 0;
@Test
void generatedConstructorsPreserveParameterListsAndVisibility() {
assertConstructors(
AttendanceController.class,
constructor(
PACKAGE_PRIVATE,
AttendanceApplicationService.class,
AttendanceCurrentUserService.class));
assertConstructors(
CalendarController.class,
constructor(
PACKAGE_PRIVATE,
CalendarApplicationService.class,
AttendanceApplicationService.class,
AttendanceCurrentUserService.class));
assertConstructors(
AttendanceApplicationService.class,
constructor(
PACKAGE_PRIVATE,
Clock.class,
AttendancePolicyRepository.class,
AttendanceRecordRepository.class,
AttendanceQueryRepository.class,
AccountService.class,
CalendarApplicationService.class,
AttendanceService.class));
assertConstructors(
AttendanceCurrentUserService.class,
constructor(PACKAGE_PRIVATE, AccountService.class));
assertConstructors(
CalendarApplicationService.class,
constructor(
PACKAGE_PRIVATE,
Clock.class,
AttendancePolicyRepository.class,
GlobalCalendarEventRepository.class));
assertConstructors(AttendanceService.class, constructor(PACKAGE_PRIVATE));
assertConstructors(AttendancePolicyEntity.class, constructor(Modifier.PROTECTED));
assertConstructors(
AttendanceRecordEntity.class,
constructor(Modifier.PROTECTED),
constructor(
Modifier.PUBLIC,
long.class,
LocalDate.class,
AttendancePolicyEntity.class,
Instant.class,
Instant.class));
assertConstructors(
GlobalCalendarEventEntity.class,
constructor(Modifier.PROTECTED),
constructor(
Modifier.PUBLIC,
LocalDate.class,
String.class,
boolean.class,
long.class));
assertConstructors(
LeaveRequestDayEntity.class,
constructor(Modifier.PROTECTED),
constructor(
PACKAGE_PRIVATE,
LeaveRequestEntity.class,
LocalDate.class,
AttendancePolicyEntity.class,
int.class));
assertConstructors(
LeaveRequestDayId.class,
constructor(Modifier.PROTECTED),
constructor(Modifier.PUBLIC, long.class, LocalDate.class));
assertConstructors(
LeaveRequestEntity.class,
constructor(Modifier.PROTECTED),
constructor(
PACKAGE_PRIVATE,
long.class,
LocalDate.class,
LocalDate.class,
String.class,
Instant.class,
Instant.class,
long.class,
Instant.class));
}
@Test
void immutableModelsRemainRecordsWithTheirComponentContracts() {
assertRecordComponents(
AttendanceActor.class,
component("userId", long.class),
component("role", AttendanceRole.class));
assertRecordComponents(
AttendanceDayContext.class,
component("activeIntern", boolean.class),
component("globalDayOff", boolean.class),
component("approvedLeave", boolean.class));
assertRecordComponents(
AttendancePolicy.class,
component("id", long.class),
component("effectiveFrom", LocalDate.class),
component("zoneId", ZoneId.class),
component("scheduledStart", LocalTime.class),
component("scheduledEnd", LocalTime.class),
component("checkInGraceMinutes", int.class),
component("checkoutGraceMinutes", int.class),
component("monthlyLeaveQuota", int.class),
component("violationPenalty", BigDecimal.class),
component("workdays", Set.class));
assertRecordComponents(
AttendanceRecord.class,
component("internId", long.class),
component("workDate", LocalDate.class),
component("policy", AttendancePolicy.class),
component("checkInAt", Instant.class),
component("checkOutAt", Instant.class));
assertRecordComponents(
AttendanceViolations.class,
component("late", boolean.class),
component("earlyDeparture", boolean.class),
component("missingCheckout", boolean.class));
assertRecordComponents(
AttendanceHistoryItem.class,
component("workDate", LocalDate.class),
component("checkInAt", Instant.class),
component("checkOutAt", Instant.class),
component("policy", AttendancePolicy.class),
component("violations", AttendanceViolations.class));
assertRecordComponents(
GlobalCalendarEvent.class,
component("id", long.class),
component("date", LocalDate.class),
component("name", String.class),
component("dayOff", boolean.class),
component("version", long.class));
}
@Test
void entitiesExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() {
assertMethodSurface(
AttendancePolicyEntity.class,
method(Modifier.PUBLIC, "toDomain", AttendancePolicy.class));
assertMethodSurface(
AttendanceRecordEntity.class,
method(Modifier.PUBLIC, "toDomain", AttendanceRecord.class),
method(Modifier.PUBLIC, "setCheckOutAt", void.class, Instant.class),
method(Modifier.PUBLIC, "workDate", LocalDate.class));
assertMethodSurface(
GlobalCalendarEventEntity.class,
method(
Modifier.PUBLIC,
"update",
void.class,
LocalDate.class,
String.class,
boolean.class,
long.class),
method(Modifier.PUBLIC, "toDomain", GlobalCalendarEvent.class),
method(Modifier.PUBLIC, "calendarDate", LocalDate.class),
method(Modifier.PUBLIC, "version", long.class));
assertMethodSurface(LeaveRequestDayEntity.class);
assertMethodSurface(
LeaveRequestDayId.class,
method(Modifier.PUBLIC, "equals", boolean.class, Object.class),
method(Modifier.PUBLIC, "hashCode", int.class));
assertMethodSurface(LeaveRequestEntity.class);
}
@Test
void componentsExposeOnlyIntentionalPublicAndProtectedDeclaredMethods() {
assertMethodSurface(
AttendanceController.class,
method(
Modifier.PUBLIC,
"ownHistory",
String.class,
Principal.class,
LocalDate.class,
LocalDate.class,
Model.class),
method(
Modifier.PUBLIC,
"inspectHistory",
String.class,
Principal.class,
long.class,
LocalDate.class,
LocalDate.class,
Model.class),
method(
Modifier.PUBLIC,
"checkIn",
String.class,
Principal.class,
RedirectAttributes.class),
method(
Modifier.PUBLIC,
"checkOut",
String.class,
Principal.class,
RedirectAttributes.class));
assertMethodSurface(
CalendarController.class,
method(Modifier.PUBLIC, "calendar", String.class, Principal.class, Model.class),
method(
Modifier.PUBLIC,
"create",
String.class,
Principal.class,
LocalDate.class,
String.class,
boolean.class,
RedirectAttributes.class),
method(
Modifier.PUBLIC,
"update",
String.class,
Principal.class,
long.class,
long.class,
LocalDate.class,
String.class,
boolean.class,
RedirectAttributes.class));
assertMethodSurface(
AttendanceApplicationService.class,
method(Modifier.PUBLIC, "checkIn", AttendanceRecord.class, long.class),
method(Modifier.PUBLIC, "checkOut", AttendanceRecord.class, long.class),
method(Modifier.PUBLIC, "currentState", AttendanceCurrentState.class, long.class),
method(
Modifier.PUBLIC,
"history",
List.class,
AttendanceActor.class,
long.class,
LocalDate.class,
LocalDate.class),
method(Modifier.PUBLIC, "currentBusinessDate", LocalDate.class));
assertMethodSurface(
AttendanceCurrentUserService.class,
method(Modifier.PUBLIC, "actor", AttendanceActor.class, Principal.class));
assertMethodSurface(
AttendanceService.class,
method(
Modifier.PUBLIC,
"checkIn",
AttendanceRecord.class,
long.class,
Instant.class,
AttendancePolicy.class,
AttendanceDayContext.class,
Optional.class),
method(
Modifier.PUBLIC,
"checkOut",
AttendanceRecord.class,
Optional.class,
Instant.class));
assertMethodSurface(
CalendarApplicationService.class,
method(
Modifier.PUBLIC,
"createManual",
GlobalCalendarEvent.class,
AttendanceActor.class,
LocalDate.class,
String.class,
boolean.class),
method(
Modifier.PUBLIC,
"updateManual",
GlobalCalendarEvent.class,
AttendanceActor.class,
long.class,
long.class,
LocalDate.class,
String.class,
boolean.class),
method(Modifier.PUBLIC, "list", List.class, LocalDate.class, LocalDate.class),
method(Modifier.PUBLIC, "isGlobalDayOff", boolean.class, LocalDate.class));
}
private static void assertConstructors(
Class<?> type, ConstructorContract... expectedConstructors) {
List<ConstructorContract> actual = Arrays.stream(type.getDeclaredConstructors())
.map(ConstructorContract::from)
.toList();
assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedConstructors);
}
private static void assertRecordComponents(
Class<?> type, RecordComponentContract... expectedComponents) {
assertThat(type.isRecord()).as(type.getName()).isTrue();
assertThat(Arrays.stream(type.getRecordComponents())
.map(component -> new RecordComponentContract(component.getName(), component.getType()))
.toList())
.as(type.getName())
.containsExactly(expectedComponents);
}
private static void assertMethodSurface(Class<?> type, MethodContract... expectedMethods) {
List<MethodContract> actual = Arrays.stream(type.getDeclaredMethods())
.filter(method -> Modifier.isPublic(method.getModifiers())
|| Modifier.isProtected(method.getModifiers()))
.map(MethodContract::from)
.toList();
assertThat(actual).as(type.getName()).containsExactlyInAnyOrder(expectedMethods);
}
private static ConstructorContract constructor(int modifiers, Class<?>... parameterTypes) {
return new ConstructorContract(modifiers, List.of(parameterTypes));
}
private static RecordComponentContract component(String name, Class<?> type) {
return new RecordComponentContract(name, type);
}
private static MethodContract method(
int modifiers, String name, Class<?> returnType, Class<?>... parameterTypes) {
return new MethodContract(modifiers, name, returnType, List.of(parameterTypes));
}
private record ConstructorContract(int modifiers, List<Class<?>> parameterTypes) {
private static ConstructorContract from(Constructor<?> constructor) {
return new ConstructorContract(
constructor.getModifiers(), List.of(constructor.getParameterTypes()));
}
}
private record RecordComponentContract(String name, Class<?> type) {}
private record MethodContract(
int modifiers, String name, Class<?> returnType, List<Class<?>> parameterTypes) {
private static MethodContract from(Method method) {
return new MethodContract(
method.getModifiers(),
method.getName(),
method.getReturnType(),
List.of(method.getParameterTypes()));
}
}
}