Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.service;import com.spice.profitmandi.common.model.ProfitMandiConstants;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.cs.Position;import com.spice.profitmandi.dao.entity.cs.Region;import com.spice.profitmandi.dao.entity.user.Lead;import com.spice.profitmandi.dao.enumuration.cs.EscalationType;import com.spice.profitmandi.dao.enumuration.dtr.LeadStage;import com.spice.profitmandi.dao.repository.auth.AuthRepository;import com.spice.profitmandi.dao.repository.cs.PositionRepository;import com.spice.profitmandi.dao.repository.cs.RegionRepository;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.time.LocalDateTime;import java.util.Arrays;import java.util.List;/*** LMS auto-assignment engine (SOP §10) + LMS-id generation + SLA helpers.** Uses the EXISTING mapping: a region is picked from {@code cs.region} and the owning BM/RSM is* resolved live from {@code cs.position} (category = SALES, escalation = L4, region_id). Unmapped* regions land in the HOLD queue so nothing breaks while the position mapping is completed.** Reused by lead create + the Create-form preview endpoint.*/@Servicepublic class LmsAssignmentService {private static final Logger LOGGER = LogManager.getLogger(LmsAssignmentService.class);/** First-contact SLA window (SOP §11): reminder at T+3h, escalation at T+5h. Due = created + 5h. */public static final int SLA_HOURS = 5;/** DROPPED after this many NOT_REACHABLE attempts (SOP §8, "config"). */public static final int MAX_UNREACHABLE = 3;private static final int SALES = ProfitMandiConstants.TICKET_CATEGORY_SALES;/** BM/RSM tiers to try, highest-relevant first. L4 ≈ BM; fall back up/down if a region lacks L4. */private static final List<EscalationType> BM_TIERS =Arrays.asList(EscalationType.L4, EscalationType.L5, EscalationType.L3);@Autowiredprivate RegionRepository regionRepository;@Autowiredprivate PositionRepository positionRepository;@Autowiredprivate AuthRepository authRepository;// ---- Region resolution ----------------------------------------------------------------/** Result of resolving a lead to a region + its owning BM. */public static class Assignment {public Integer regionId; // cs.region id (null if not picked / not found)public String regionCode; // cs.region.region_code (fallback: derived from name) — LMS id prefixpublic AuthUser bm; // owning BM/RSM (null → HOLD)public String assignmentStatus; // ASSIGNED | HOLD}/** Resolve a picked {@code cs.region} id → its code + owning BM/RSM (via cs.position). */public Assignment resolve(Integer regionId) {Assignment a = new Assignment();if (regionId != null && regionId > 0) {Region region = regionRepository.selectById(regionId);if (region != null) {a.regionId = region.getId();a.regionCode = codeOf(region);a.bm = resolveBm(region.getId());}}a.assignmentStatus = a.bm != null ? "ASSIGNED" : "HOLD";return a;}/** region_code if set, otherwise a short slug of the region name (for the LMS id prefix). */private String codeOf(Region region) {if (region.getRegionCode() != null && !region.getRegionCode().trim().isEmpty()) {return region.getRegionCode().trim().toUpperCase();}String name = region.getName() == null ? "" : region.getName().replaceAll("[^A-Za-z]", "");if (name.isEmpty()) {return "GEN";}return name.substring(0, Math.min(3, name.length())).toUpperCase();}/** First active BM/RSM (SALES position) for a region, trying L4 → L5 → L3. Null → HOLD. */public AuthUser resolveBm(Integer regionId) {if (regionId == null) {return null;}for (EscalationType tier : BM_TIERS) {List<Position> positions = positionRepository.selectPositionbyCategoryIdAndEscalationType(SALES, tier, regionId);if (positions == null) {continue;}for (Position p : positions) {AuthUser u = authRepository.selectById(p.getAuthUserId());if (u != null && Boolean.TRUE.equals(u.isActive())) {return u;}}}return null;}// ---- Apply assignment to a lead -------------------------------------------------------/*** Stamp region + owner + stage + SLA onto a freshly-built lead (SOP §9), using the lead's* picked {@code regionId}. Path A owner = BM (BM maps an ASM later); Path B owner = creator* (assignTo already set). A manual assignTo override (assignTo > 0) is preserved. Legacy* status is synced to the stage. Does NOT persist — caller persists (then {@link #generateLmsCode}).*/public Assignment assign(Lead lead, String path) {String creationPath = (path != null && path.trim().equalsIgnoreCase("B")) ? "B" : "A";lead.setCreationPath(creationPath);Assignment a = resolve(lead.getRegionId());lead.setRegionId(a.regionId);lead.setRegionCode(a.regionCode);lead.setAssignmentStatus(a.assignmentStatus);if (a.bm != null) {lead.setOwnerBmId(a.bm.getId());if ("A".equals(creationPath) && lead.getAssignTo() <= 0) {lead.setAssignTo(a.bm.getId());}}lead.setStage(LeadStage.ASSIGNED);lead.setStatus(LeadStage.ASSIGNED.toLegacyStatus());lead.setUnreachableCount(0);LocalDateTime base = lead.getCreatedTimestamp() != null ? lead.getCreatedTimestamp() : LocalDateTime.now();lead.setFirstContactDue(base.plusHours(SLA_HOURS));return a;}// ---- LMS id ---------------------------------------------------------------------------/** LMS-<REGION>-<YY>-<6-digit id>, e.g. LMS-UPW-26-000123. Call after the lead has an id. */public String generateLmsCode(Lead lead) {String region = (lead.getRegionCode() != null && !lead.getRegionCode().isEmpty())? lead.getRegionCode() : "GEN";LocalDateTime created = lead.getCreatedTimestamp() != null ? lead.getCreatedTimestamp() : LocalDateTime.now();int yy = created.getYear() % 100;return String.format("LMS-%s-%02d-%06d", region, yy, lead.getId());}// ---- SLA ------------------------------------------------------------------------------/** MET / BREACHED / AT_RISK / RUNNING / NA — for the record SLA card. */public String slaState(Lead lead) {LocalDateTime due = lead.getFirstContactDue();if (lead.getFirstContactedAt() != null) {if (due == null) {return "MET";}return !lead.getFirstContactedAt().isAfter(due) ? "MET" : "BREACHED";}if (due == null) {return "NA";}LocalDateTime now = LocalDateTime.now();if (now.isAfter(due)) {return "BREACHED";}if (now.isAfter(due.minusHours(1))) {return "AT_RISK";}return "RUNNING";}}