2026-05-26 05:52:12 +07:00

310 lines
14 KiB
Java

package com.walkguide.service;
import com.walkguide.dto.response.PairingStatusResponse;
import com.walkguide.dto.response.PairingCodeResponse;
import com.walkguide.entity.*;
import com.walkguide.enums.*;
import com.walkguide.exception.PairingException;
import com.walkguide.exception.ResourceNotFoundException;
import com.walkguide.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class PairingService {
private final PairingRelationRepository pairingRelationRepository;
private final UserRepository userRepository;
private final VoiceCommandConfigRepository voiceCommandConfigRepository;
private final HardwareShortcutRepository hardwareShortcutRepository;
private final AiConfigRepository aiConfigRepository;
private final ActivityLogService activityLogService;
private final FcmService fcmService;
private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
private static final int PAIRING_CODE_LENGTH = 8;
private static final int PAIRING_CODE_TTL_MINUTES = 15;
private static final SecureRandom RANDOM = new SecureRandom();
@Transactional
public PairingCodeResponse getOrCreatePairingCode(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan"));
if (!"ROLE_USER".equals(user.getRole())) {
throw new PairingException("Hanya akun User yang bisa membuat pairing code.");
}
LocalDateTime now = LocalDateTime.now();
if (user.getPairingCode() == null
|| user.getPairingCodeExpiresAt() == null
|| !user.getPairingCodeExpiresAt().isAfter(now)) {
assignNewPairingCode(user, now);
userRepository.save(user);
}
return buildPairingCodeResponse(user, now);
}
@Transactional
public PairingCodeResponse regeneratePairingCode(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan"));
if (!"ROLE_USER".equals(user.getRole())) {
throw new PairingException("Hanya akun User yang bisa membuat pairing code.");
}
LocalDateTime now = LocalDateTime.now();
assignNewPairingCode(user, now);
userRepository.save(user);
activityLogService.createLog(user, ActivityLogType.PAIRING_INVITE_SENT,
"User membuat pairing code baru", null);
return buildPairingCodeResponse(user, now);
}
@Transactional
public PairingStatusResponse inviteUser(Long guardianId, String submittedCode) {
// Guardian tidak boleh punya pairing ACTIVE atau PENDING
if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)) {
throw new PairingException("Kamu sudah memiliki user yang dipair. Unpair dulu sebelum invite user baru.");
}
if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.PENDING)) {
throw new PairingException("Kamu sudah punya invite yang menunggu. Tunggu user menerima atau tolak dulu.");
}
User guardian = userRepository.findById(guardianId)
.orElseThrow(() -> new ResourceNotFoundException("Guardian tidak ditemukan"));
User user = resolveUserByPairingCode(submittedCode);
if (!"ROLE_USER".equals(user.getRole())) {
throw new PairingException("ID tersebut bukan milik User. Pastikan kamu memasukkan ID yang benar.");
}
if (pairingRelationRepository.existsByUser_IdAndStatus(user.getId(), PairingStatus.ACTIVE)) {
throw new PairingException("User ini sudah dipair dengan Guardian lain.");
}
PairingRelation pairing = PairingRelation.builder()
.guardian(guardian)
.user(user)
.status(PairingStatus.PENDING)
.build();
pairing = pairingRelationRepository.save(pairing);
user.setPairingCode(null);
user.setPairingCodeExpiresAt(null);
userRepository.save(user);
// Kirim FCM ke user
fcmService.sendToToken(user.getFcmToken(),
"Pairing Request",
"Guardian " + guardian.getDisplayName() + " mengundang kamu untuk terhubung",
Map.of("type", "PAIRING_INVITE", "guardianName", guardian.getDisplayName()));
activityLogService.createLog(guardian, ActivityLogType.PAIRING_INVITE_SENT,
"Guardian mengirim invite ke " + user.getDisplayName(), null);
return buildStatus(pairing, guardian, user, "GUARDIAN");
}
@Transactional
public PairingStatusResponse respondToPairing(Long userId, Long pairingId, boolean accept) {
PairingRelation pairing = pairingRelationRepository.findById(pairingId)
.orElseThrow(() -> new ResourceNotFoundException("Pairing tidak ditemukan"));
if (!pairing.getUser().getId().equals(userId)) {
throw new PairingException("Kamu tidak berhak merespons pairing ini");
}
if (pairing.getStatus() != PairingStatus.PENDING) {
throw new PairingException("Pairing ini sudah direspons sebelumnya");
}
User user = pairing.getUser();
User guardian = pairing.getGuardian();
pairing.setStatus(accept ? PairingStatus.ACTIVE : PairingStatus.REJECTED);
pairing.setRespondedAt(LocalDateTime.now());
pairingRelationRepository.save(pairing);
if (accept) {
seedDefaults(guardian.getId(), user.getId());
activityLogService.createLog(user, ActivityLogType.PAIRING_ACCEPTED,
"User menerima pairing dengan Guardian " + guardian.getDisplayName(), null);
fcmService.sendToToken(guardian.getFcmToken(),
"Pairing Berhasil!",
user.getDisplayName() + " menerima undangan pairing kamu",
Map.of("type", "PAIRING_RESPONSE", "accepted", "true"));
} else {
activityLogService.createLog(user, ActivityLogType.PAIRING_REJECTED,
"User menolak pairing dengan Guardian " + guardian.getDisplayName(), null);
fcmService.sendToToken(guardian.getFcmToken(),
"Pairing Ditolak",
user.getDisplayName() + " menolak undangan pairing kamu",
Map.of("type", "PAIRING_RESPONSE", "accepted", "false"));
}
return buildStatus(pairing, guardian, user, "USER");
}
@Transactional
public void unpair(Long requesterId) {
var pairing = pairingRelationRepository.findByGuardian_Id(requesterId)
.or(() -> pairingRelationRepository.findByUser_Id(requesterId))
.orElseThrow(() -> new ResourceNotFoundException("Tidak ada pairing yang aktif"));
User guardian = pairing.getGuardian();
User user = pairing.getUser();
// Hapus semua konfigurasi yang terkait dengan pasangan ini
voiceCommandConfigRepository.deleteByUserId(user.getId());
hardwareShortcutRepository.deleteByUserId(user.getId());
aiConfigRepository.findByUserId(user.getId()).ifPresent(aiConfigRepository::delete);
pairingRelationRepository.delete(pairing);
// Beritahu pihak lain
boolean requesterIsGuardian = guardian.getId().equals(requesterId);
String otherFcmToken = requesterIsGuardian ? user.getFcmToken() : guardian.getFcmToken();
String requesterName = requesterIsGuardian ? guardian.getDisplayName() : user.getDisplayName();
fcmService.sendToToken(otherFcmToken,
"Pairing Diakhiri",
requesterName + " mengakhiri koneksi pairing",
Map.of("type", "PAIRING_DISSOLVED"));
activityLogService.createLog(guardian, ActivityLogType.PAIRING_DISSOLVED,
"Pairing antara " + guardian.getDisplayName() + " dan " + user.getDisplayName() + " diakhiri", null);
}
public PairingStatusResponse getStatus(Long userId, String role) {
if ("ROLE_GUARDIAN".equals(role)) {
return pairingRelationRepository.findByGuardian_Id(userId)
.map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "GUARDIAN"))
.orElse(PairingStatusResponse.builder().status("NONE").build());
} else {
return pairingRelationRepository.findByUser_Id(userId)
.map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "USER"))
.orElse(PairingStatusResponse.builder().status("NONE").build());
}
}
// ========== PRIVATE ==========
private void seedDefaults(Long guardianId, Long userId) {
// Voice commands default
List<VoiceCommandConfig> defaults = List.of(
vc(guardianId, userId, VoiceCommandKey.OPEN_WALKGUIDE, "Open Walkguide"),
vc(guardianId, userId, VoiceCommandKey.START_WALKGUIDE, "Start Walkguide"),
vc(guardianId, userId, VoiceCommandKey.STOP_WALKGUIDE, "Stop Walkguide"),
vc(guardianId, userId, VoiceCommandKey.CALL_GUARDIAN, "Call Guardian"),
vc(guardianId, userId, VoiceCommandKey.OPEN_NOTIFICATION, "Open Notifications"),
vc(guardianId, userId, VoiceCommandKey.READ_ALL_NOTIF, "Read All My Notifications"),
vc(guardianId, userId, VoiceCommandKey.OPEN_SOS, "Open SOS"),
vc(guardianId, userId, VoiceCommandKey.SEND_SOS, "Send SOS"),
vc(guardianId, userId, VoiceCommandKey.WHERE_AM_I, "Where Am I"),
vc(guardianId, userId, VoiceCommandKey.OPEN_ACTIVITY, "Open Activity Log"),
vc(guardianId, userId, VoiceCommandKey.OPEN_NAVIGATION, "Open Navigation"),
vc(guardianId, userId, VoiceCommandKey.OPEN_SETTINGS, "Open Settings"),
vc(guardianId, userId, VoiceCommandKey.REPEAT_LAST, "Repeat"),
vc(guardianId, userId, VoiceCommandKey.STOP_TTS, "Stop")
);
voiceCommandConfigRepository.saveAll(defaults);
// Hardware shortcuts default (Vol Up = Call Guardian, Vol Down = Start WalkGuide)
List<HardwareShortcut> shortcuts = List.of(
hs(guardianId, userId, HardwareShortcutKey.CALL_GUARDIAN, "Volume Up", 24),
hs(guardianId, userId, HardwareShortcutKey.START_WALKGUIDE, "Volume Down", 25),
hs(guardianId, userId, HardwareShortcutKey.SEND_SOS, null, null),
hs(guardianId, userId, HardwareShortcutKey.STOP_WALKGUIDE, null, null),
hs(guardianId, userId, HardwareShortcutKey.OPEN_NOTIFICATION, null, null)
);
hardwareShortcutRepository.saveAll(shortcuts);
// AI config default
aiConfigRepository.save(AiConfig.builder()
.guardianId(guardianId)
.userId(userId)
.build());
}
private VoiceCommandConfig vc(Long gId, Long uId, VoiceCommandKey key, String phrase) {
return VoiceCommandConfig.builder()
.guardianId(gId).userId(uId).commandKey(key).triggerPhrase(phrase).build();
}
private HardwareShortcut hs(Long gId, Long uId, HardwareShortcutKey key, String name, Integer code) {
return HardwareShortcut.builder()
.guardianId(gId).userId(uId).shortcutKey(key)
.buttonName(name).buttonCode(code)
.enabled(name != null).build();
}
private User resolveUserByPairingCode(String submittedCode) {
if (submittedCode == null || submittedCode.isBlank()) {
throw new PairingException("Pairing code tidak boleh kosong.");
}
String code = submittedCode.trim().toUpperCase();
User user = userRepository.findByPairingCode(code)
.orElseThrow(() -> new ResourceNotFoundException(
"Pairing code '" + code + "' tidak ditemukan. Minta User generate kode baru."));
if (user.getPairingCodeExpiresAt() == null
|| !user.getPairingCodeExpiresAt().isAfter(LocalDateTime.now())) {
user.setPairingCode(null);
user.setPairingCodeExpiresAt(null);
userRepository.save(user);
throw new PairingException("Pairing code sudah kadaluarsa. Minta User generate kode baru.");
}
return user;
}
private void assignNewPairingCode(User user, LocalDateTime now) {
String candidate;
do {
candidate = randomCode();
} while (userRepository.findByPairingCode(candidate).isPresent());
user.setPairingCode(candidate);
user.setPairingCodeExpiresAt(now.plusMinutes(PAIRING_CODE_TTL_MINUTES));
}
private String randomCode() {
StringBuilder sb = new StringBuilder(PAIRING_CODE_LENGTH);
for (int i = 0; i < PAIRING_CODE_LENGTH; i++) {
sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length())));
}
return sb.toString();
}
private PairingCodeResponse buildPairingCodeResponse(User user, LocalDateTime now) {
long seconds = Math.max(0,
ChronoUnit.SECONDS.between(now, user.getPairingCodeExpiresAt()));
return PairingCodeResponse.builder()
.pairingCode(user.getPairingCode())
.expiresAt(user.getPairingCodeExpiresAt())
.expiresInSeconds(seconds)
.build();
}
private PairingStatusResponse buildStatus(PairingRelation p, User guardian, User user, String viewerRole) {
String pairedWithName = "GUARDIAN".equals(viewerRole)
? user.getDisplayName() : guardian.getDisplayName();
String pairedWithEmail = "GUARDIAN".equals(viewerRole)
? user.getEmail() : guardian.getEmail();
return PairingStatusResponse.builder()
.pairingId(p.getId())
.status(p.getStatus().name())
.pairedWithId("GUARDIAN".equals(viewerRole) ? user.getId() : guardian.getId())
.pairedWithName(pairedWithName)
.pairedWithEmail(pairedWithEmail)
.uniqueUserId(user.getUniqueUserId())
.pairingCode(user.getPairingCode())
.pairingCodeExpiresAt(user.getPairingCodeExpiresAt())
.invitedAt(p.getInvitedAt())
.respondedAt(p.getRespondedAt())
.build();
}
}