Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/cd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ jobs:
echo "TOSS_PAYMENT_SECRET_KEY=${{ secrets.TOSS_PAYMENT_SECRET_KEY }}" >> .env
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env
echo "REDIS_URL=${{ secrets.REDIS_URL }}" >> .env


echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public class BossStoreFacade {
@Transactional
public StoreCreateResponse createStore(final Long userId, final StoreCreateRequest request) {
final UserEntity boss = userService.getUserById(userId);
storeService.validateBusinessNumber(request.businessNumber());
final StoreEntity saved = storeService.createStore(request, boss);
payrollSettingService.initPayrollSettingForStore(saved);
requiredDocumentService.initRequiredDocuments(saved);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class StaffStoreFacade {
public StaffJoinResponse joinStaff(final Long userId, final StaffJoinRequest request) {
final UserEntity user = userService.getUserById(userId);
final StoreEntity store = storeService.getStoreByInviteCode(request.inviteCode());
staffService.createStaff(user, store);
staffService.createStaff(user, store, store.getBoss());
return StaffJoinResponse.fromEntity(store);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.mangoboss.app.domain.service.billing.BillingService;
import com.mangoboss.app.domain.service.subscription.SubscriptionService;
import com.mangoboss.app.domain.service.user.UserService;
import com.mangoboss.app.dto.subscription.request.SubscriptionCreateRequest;
import com.mangoboss.app.dto.subscription.response.SubscriptionOrderResponse;
import com.mangoboss.app.dto.subscription.response.SubscriptionResponse;
import com.mangoboss.storage.user.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -16,10 +18,12 @@ public class SubscriptionFacade {

private final SubscriptionService subscriptionService;
private final BillingService billingService;
private final UserService userService;

public void createOrReplaceSubscription(Long bossId, SubscriptionCreateRequest request) {
billingService.validateBillingExists(bossId);
subscriptionService.createOrReplaceSubscription(bossId, request.planType());
UserEntity boss = userService.getUserById(bossId);
subscriptionService.createOrReplaceSubscription(boss, request.planType());
}

public SubscriptionResponse getSubscription(Long bossId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mangoboss.app.api.facade.workreport;

import com.mangoboss.app.common.util.S3PreSignedUrlManager;
import com.mangoboss.app.domain.service.notification.NotificationService;
import com.mangoboss.app.domain.service.staff.StaffService;
import com.mangoboss.app.domain.service.workreport.WorkReportService;
import com.mangoboss.app.dto.s3.response.UploadPreSignedUrlResponse;
Expand All @@ -9,6 +10,7 @@
import com.mangoboss.storage.metadata.S3FileType;
import com.mangoboss.storage.staff.StaffEntity;
import com.mangoboss.storage.workreport.WorkReportEntity;
import com.mangoboss.storage.workreport.WorkReportTargetType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -21,11 +23,18 @@ public class StaffWorkReportFacade {

private final WorkReportService workReportService;
private final StaffService staffService;
private final NotificationService notificationService;
private final S3PreSignedUrlManager s3PreSignedUrlManager;

public WorkReportResponse createWorkReport(final Long storeId, final Long userId, final WorkReportCreateRequest request) {
StaffEntity staff = staffService.getVerifiedStaff(userId, storeId);
final WorkReportEntity entity = workReportService.createWorkReport(storeId, staff.getId(), request.content(), request.reportImageUrl(), request.targetType());

if (request.targetType() == WorkReportTargetType.TO_BOSS) {
final Long bossUserId = staff.getStore().getBoss().getId();
notificationService.saveWorkReportNotificationToBoss(bossUserId, storeId, staff.getUser().getName());
}

return WorkReportResponse.fromEntity(entity, staff);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public enum CustomErrorInfo {
DOCUMENT_NOT_BELONG_TO_STAFF(403, "이 서류는 해당 알바생의 서류가 아닙니다.", 403006),
FORBIDDEN_TASK_LOG_DELETE(403, "해당 업무 완료 기록을 삭제할 권한이 없습니다.", 403007),
WORK_REPORT_ACCESS_DENIED(403, "보고사항 접근 권한이 없습니다.", 403008),
PLAN_LIMIT_EXCEEDED(403, "무료 요금제는 매장 2개, 알바생 5명 까지만 가능합니다.",403009),

// 422 Unprocessable Entity
CONTRACT_PDF_TAMPERED(422, "계약서 PDF의 무결성이 손상되었습니다.", 422001),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.Optional;

public interface SubscriptionRepository {
void save(SubscriptionEntity subscriptionEntity);
SubscriptionEntity save(SubscriptionEntity subscriptionEntity);
Optional<SubscriptionEntity> findByBossId(Long bossId);
void delete(SubscriptionEntity subscriptionEntity);
boolean existsByBossId(Long bossId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ private void saveNotification(final Long userId, final Long storeId, final Strin
final String content, final NotificationType type, final String path) {
final String clickUrl = frontendUrl + path;
final List<String> tokens = deviceTokenRepository.findActiveTokensByUserId(userId);
for (String token : tokens) {
final NotificationEntity notification = NotificationEntity.create(
userId, storeId, title, content, null, clickUrl, type, token
);

if (tokens.isEmpty()) {
final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, null);
notificationRepository.save(notification);
} else {
for (String token : tokens) {
final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, token);
notificationRepository.save(notification);
}
}
}

Expand Down Expand Up @@ -159,4 +163,17 @@ public void saveAttendanceEditRejectNotification(final AttendanceEditEntity atte
public List<NotificationEntity> getNotificationsByUserAndStore(final Long userId, final Long storeId) {
return notificationRepository.findByUserIdAndStoreIdOrderByCreatedAtDesc(userId, storeId);
}

@Transactional
public void saveWorkReportNotificationToBoss(final Long bossUserId, final Long storeId, final String staffName) {
String content = String.format("%s님이 보고사항을 작성했어요.", staffName);
saveNotification(
bossUserId,
storeId,
"보고사항 작성",
content,
NotificationType.WORK_REPORT,
"/boss/task?type=report"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,23 @@
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StaffService {
private static final int PLAN_LIMIT_STAFF_NUM = 5;
private final StaffRepository staffRepository;

@Transactional
public StaffEntity createStaff(final UserEntity user, final StoreEntity store) {
public StaffEntity createStaff(final UserEntity user, final StoreEntity store, final UserEntity boss) {
isAlreadyJoin(user, store);
if (boss.getSubscription() == null && getStaffsNum(store.getId()) >= PLAN_LIMIT_STAFF_NUM) {
throw new CustomException(CustomErrorInfo.PLAN_LIMIT_EXCEEDED);
}
final StaffEntity staff = StaffEntity.create(user, store);
return staffRepository.save(staff);
}

private Integer getStaffsNum(final Long storeId) {
return getStaffsForStore(storeId).size();
}

public StaffEntity validateStaffBelongsToStore(final Long storeId, final Long staffId) {
return staffRepository.getByIdAndStoreId(staffId, storeId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@Transactional
public class StoreService {
private static final String CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final int PLAN_LIMIT_STORE_NUM = 2;
private static final int INVITE_CODE_LENGTH = 6;
private static final int QR_CODE_LENGTH = 12;

Expand Down Expand Up @@ -58,13 +59,20 @@ public StoreEntity getStoreById(final Long storeId) {

@Transactional
public StoreEntity createStore(final StoreCreateRequest request, final UserEntity boss) {
if (boss.getSubscription() == null && getStoresNum(boss.getId()) >= PLAN_LIMIT_STORE_NUM){
throw new CustomException(CustomErrorInfo.PLAN_LIMIT_EXCEEDED);
}
validateBusinessNumber(request.businessNumber());
final String inviteCode = generateInviteCode();
final String attendanceQrCode = generateQrCode();
final StoreEntity store = request.toEntity(boss, inviteCode, attendanceQrCode);
return storeRepository.save(store);
}

private Integer getStoresNum(final Long bossId) {
return storeRepository.findAllByBossId(bossId).size();
}

private String generateInviteCode() {
String code;
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.mangoboss.storage.subscription.SubscriptionEntity;
import com.mangoboss.app.domain.repository.SubscriptionRepository;
import com.mangoboss.storage.subscription.SubscriptionOrderEntity;
import com.mangoboss.storage.user.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -23,11 +24,12 @@ public class SubscriptionService {
private final SubscriptionOrderRepository subscriptionOrderRepository;

@Transactional
public void createOrReplaceSubscription(Long bossId, PlanType planType) {
subscriptionRepository.findByBossId(bossId).ifPresent(subscriptionRepository::delete);
public void createOrReplaceSubscription(UserEntity boss, PlanType planType) {
subscriptionRepository.findByBossId(boss.getId()).ifPresent(subscriptionRepository::delete);

SubscriptionEntity subscription = SubscriptionEntity.create(bossId, planType);
SubscriptionEntity subscription = SubscriptionEntity.create(boss.getId(), planType);
subscriptionRepository.save(subscription);
boss.addSubscription(subscription);
}

public SubscriptionEntity getSubscription(Long bossId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public class SubscriptionRepositoryImpl implements SubscriptionRepository {
private final SubscriptionJpaRepository subscriptionJpaRepository;

@Override
public void save(SubscriptionEntity subscriptionEntity) {
subscriptionJpaRepository.save(subscriptionEntity);
public SubscriptionEntity save(SubscriptionEntity subscriptionEntity) {
return subscriptionJpaRepository.save(subscriptionEntity);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ class StaffServiceTest {
UserEntity user = mock(UserEntity.class);
StoreEntity store = mock(StoreEntity.class);
StaffEntity staff = mock(StaffEntity.class);
UserEntity boss = mock(UserEntity.class);
when(staffRepository.save(any(StaffEntity.class))).thenReturn(staff);

//when
StaffEntity result = staffService.createStaff(user,store);
StaffEntity result = staffService.createStaff(user,store,boss);

//then
assertThat(result).isEqualTo(staff);
Expand All @@ -47,11 +48,12 @@ class StaffServiceTest {
//given
UserEntity user = mock(UserEntity.class);
StoreEntity store = mock(StoreEntity.class);
UserEntity boss = mock(UserEntity.class);
when(staffRepository.existsByUserIdAndStoreId(any(Long.class), any(Long.class))).thenReturn(true);

//when
//then
Assertions.assertThatThrownBy(()-> staffService.createStaff(user,store))
Assertions.assertThatThrownBy(()-> staffService.createStaff(user,store,boss))
.isInstanceOf(CustomException.class)
.hasMessage(CustomErrorInfo.ALREADY_JOIN_STAFF.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import com.mangoboss.storage.attendance.ClockInStatus;
import com.mangoboss.storage.attendance.ClockOutStatus;
import com.mangoboss.storage.schedule.ScheduleEntity;
import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Clock;
import java.time.LocalDateTime;
import java.util.List;

Expand All @@ -20,12 +20,13 @@
public class AutoClockOutService {
private final AttendanceRepository attendanceRepository;
private final ScheduleRepository scheduleRepository;
private final Clock clock;
private final NotificationAutoClockOutService notificationAutoClockOutService;

@Transactional
public void autoClockOut() {
List<ScheduleEntity> schedules = scheduleRepository.findAllSchedulesWithoutClockOut();
List<AttendanceEntity> attendances = schedules.stream().map(schedule -> {
List<ScheduleForNotificationProjection> schedules = scheduleRepository.findAllSchedulesWithoutClockOut();
List<AttendanceEntity> attendances = schedules.stream().map(projection -> {
ScheduleEntity schedule = projection.getSchedule();
if (schedule.getAttendance() == null) {
return recordAbsentAttendance(schedule);
}
Expand All @@ -41,4 +42,8 @@ private AttendanceEntity recordAbsentAttendance(final ScheduleEntity schedule) {
private AttendanceEntity recordNormalClockOutAttendance(final AttendanceEntity attendance, final LocalDateTime clockOutTime) {
return attendance.recordClockOut(clockOutTime, ClockOutStatus.NORMAL);
}

private void notifyAbsentClockOut(final List<ScheduleForNotificationProjection> projections) {
notificationAutoClockOutService.saveNotifications(projections);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mangoboss.batch.auto_clock_out.domain.service;

import com.mangoboss.batch.common.repository.DeviceTokenRepository;
import com.mangoboss.batch.common.repository.NotificationRepository;
import com.mangoboss.storage.notification.NotificationEntity;
import com.mangoboss.storage.notification.NotificationType;
import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class NotificationAutoClockOutService {
private final NotificationRepository notificationRepository;
private final DeviceTokenRepository deviceTokenRepository;

@Value("${frontend-url}")
private String frontendUrl;


private List<NotificationEntity> generateNotification(final Long userId, final Long storeId, final String title,
final String content, final NotificationType type, final String path) {
String clickUrl = frontendUrl + path;
List<String> tokens = deviceTokenRepository.findActiveTokensByUserId(userId);
if (tokens.isEmpty()) {
final NotificationEntity notification = NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, null);
return List.of(notification);
}
return tokens.stream()
.map(token -> NotificationEntity.create(userId, storeId, title, content, null, clickUrl, type, token))
.toList();
}

private List<NotificationEntity> generateAbsentClockInNotification(final Long userId, final Long storeId, final String staffName) {
String content = String.format("%s님이 결근처리 되었어요.", staffName);
return generateNotification(
userId,
storeId,
"결근 알림",
content,
NotificationType.SCHEDULE,
"/boss/schedule"
);
}

@Transactional
public void saveNotifications(final List<ScheduleForNotificationProjection> projections) {
List<NotificationEntity> notifications = projections.stream()
.flatMap(schedule -> generateAbsentClockInNotification(
schedule.getBossId(),
schedule.getStoreId(),
schedule.getStaffName()
).stream())
.toList();
notificationRepository.saveAll(notifications);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.mangoboss.batch.common.persistence;

import com.mangoboss.batch.common.repository.ScheduleRepository;
import com.mangoboss.storage.schedule.ScheduleEntity;
import com.mangoboss.storage.schedule.ScheduleJpaRepository;
import com.mangoboss.storage.schedule.projection.ScheduleForLateClockInProjection;
import com.mangoboss.storage.schedule.projection.ScheduleForNotificationProjection;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

Expand All @@ -18,13 +17,13 @@ public class ScheduleRepositoryImpl implements ScheduleRepository {
private final Clock clock;

@Override
public List<ScheduleEntity> findAllSchedulesWithoutClockOut() {
public List<ScheduleForNotificationProjection> findAllSchedulesWithoutClockOut() {
LocalDateTime oneHourAgo = LocalDateTime.now(clock).minusHours(1);
return scheduleJpaRepository.findAllSchedulesWithoutClockOut(oneHourAgo);
}

@Override
public List<ScheduleForLateClockInProjection> findAllSchedulesWithoutClockIn() {
public List<ScheduleForNotificationProjection> findAllSchedulesWithoutClockIn() {
LocalDateTime temMinuteAgo = LocalDateTime.now(clock).minusMinutes(10);
return scheduleJpaRepository.findLateSchedulesWithoutAlarm(temMinuteAgo);
}
Expand Down
Loading