Subversion Repositories SmartDukaan

Rev

Rev 37125 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37125 vikas 1
package com.spice.profitmandi.service;
2
 
3
import com.spice.profitmandi.common.model.ProfitMandiConstants;
4
import com.spice.profitmandi.dao.entity.auth.AuthUser;
5
import com.spice.profitmandi.dao.entity.cs.Position;
6
import com.spice.profitmandi.dao.entity.cs.Region;
7
import com.spice.profitmandi.dao.entity.user.Lead;
8
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
9
import com.spice.profitmandi.dao.enumuration.dtr.LeadStage;
10
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
11
import com.spice.profitmandi.dao.repository.cs.PositionRepository;
12
import com.spice.profitmandi.dao.repository.cs.RegionRepository;
13
import org.apache.logging.log4j.LogManager;
14
import org.apache.logging.log4j.Logger;
15
import org.springframework.beans.factory.annotation.Autowired;
16
import org.springframework.stereotype.Service;
17
 
18
import java.time.LocalDateTime;
19
import java.util.Arrays;
20
import java.util.List;
21
 
22
/**
23
 * LMS auto-assignment engine (SOP §10) + LMS-id generation + SLA helpers.
24
 *
25
 * Uses the EXISTING mapping: a region is picked from {@code cs.region} and the owning BM/RSM is
26
 * resolved live from {@code cs.position} (category = SALES, escalation = L4, region_id). Unmapped
27
 * regions land in the HOLD queue so nothing breaks while the position mapping is completed.
28
 *
29
 * Reused by lead create + the Create-form preview endpoint.
30
 */
31
@Service
32
public class LmsAssignmentService {
33
 
34
    private static final Logger LOGGER = LogManager.getLogger(LmsAssignmentService.class);
35
 
36
    /** First-contact SLA window (SOP §11): reminder at T+3h, escalation at T+5h. Due = created + 5h. */
37
    public static final int SLA_HOURS = 5;
38
    /** DROPPED after this many NOT_REACHABLE attempts (SOP §8, "config"). */
39
    public static final int MAX_UNREACHABLE = 3;
40
 
41
    private static final int SALES = ProfitMandiConstants.TICKET_CATEGORY_SALES;
42
 
43
    /** BM/RSM tiers to try, highest-relevant first. L4 ≈ BM; fall back up/down if a region lacks L4. */
44
    private static final List<EscalationType> BM_TIERS =
45
            Arrays.asList(EscalationType.L4, EscalationType.L5, EscalationType.L3);
46
 
47
    @Autowired
48
    private RegionRepository regionRepository;
49
 
50
    @Autowired
51
    private PositionRepository positionRepository;
52
 
53
    @Autowired
54
    private AuthRepository authRepository;
55
 
56
    // ---- Region resolution ----------------------------------------------------------------
57
 
58
    /** Result of resolving a lead to a region + its owning BM. */
59
    public static class Assignment {
60
        public Integer regionId;          // cs.region id (null if not picked / not found)
61
        public String regionCode;         // cs.region.region_code (fallback: derived from name) — LMS id prefix
62
        public AuthUser bm;               // owning BM/RSM (null → HOLD)
63
        public String assignmentStatus;   // ASSIGNED | HOLD
64
    }
65
 
66
    /** Resolve a picked {@code cs.region} id → its code + owning BM/RSM (via cs.position). */
67
    public Assignment resolve(Integer regionId) {
68
        Assignment a = new Assignment();
69
        if (regionId != null && regionId > 0) {
70
            Region region = regionRepository.selectById(regionId);
71
            if (region != null) {
72
                a.regionId = region.getId();
73
                a.regionCode = codeOf(region);
74
                a.bm = resolveBm(region.getId());
75
            }
76
        }
77
        a.assignmentStatus = a.bm != null ? "ASSIGNED" : "HOLD";
78
        return a;
79
    }
80
 
81
    /** region_code if set, otherwise a short slug of the region name (for the LMS id prefix). */
82
    private String codeOf(Region region) {
83
        if (region.getRegionCode() != null && !region.getRegionCode().trim().isEmpty()) {
84
            return region.getRegionCode().trim().toUpperCase();
85
        }
86
        String name = region.getName() == null ? "" : region.getName().replaceAll("[^A-Za-z]", "");
87
        if (name.isEmpty()) {
88
            return "GEN";
89
        }
90
        return name.substring(0, Math.min(3, name.length())).toUpperCase();
91
    }
92
 
93
    /** First active BM/RSM (SALES position) for a region, trying L4 → L5 → L3. Null → HOLD. */
94
    public AuthUser resolveBm(Integer regionId) {
95
        if (regionId == null) {
96
            return null;
97
        }
98
        for (EscalationType tier : BM_TIERS) {
99
            List<Position> positions = positionRepository.selectPositionbyCategoryIdAndEscalationType(SALES, tier, regionId);
100
            if (positions == null) {
101
                continue;
102
            }
103
            for (Position p : positions) {
104
                AuthUser u = authRepository.selectById(p.getAuthUserId());
105
                if (u != null && Boolean.TRUE.equals(u.isActive())) {
106
                    return u;
107
                }
108
            }
109
        }
110
        return null;
111
    }
112
 
113
    // ---- Apply assignment to a lead -------------------------------------------------------
114
 
115
    /**
116
     * Stamp region + owner + stage + SLA onto a freshly-built lead (SOP §9), using the lead's
117
     * picked {@code regionId}. Path A owner = BM (BM maps an ASM later); Path B owner = creator
118
     * (assignTo already set). A manual assignTo override (assignTo &gt; 0) is preserved. Legacy
119
     * status is synced to the stage. Does NOT persist — caller persists (then {@link #generateLmsCode}).
120
     */
121
    public Assignment assign(Lead lead, String path) {
122
        String creationPath = (path != null && path.trim().equalsIgnoreCase("B")) ? "B" : "A";
123
        lead.setCreationPath(creationPath);
124
 
125
        Assignment a = resolve(lead.getRegionId());
126
        lead.setRegionId(a.regionId);
127
        lead.setRegionCode(a.regionCode);
128
        lead.setAssignmentStatus(a.assignmentStatus);
129
 
130
        if (a.bm != null) {
131
            lead.setOwnerBmId(a.bm.getId());
132
            if ("A".equals(creationPath) && lead.getAssignTo() <= 0) {
133
                lead.setAssignTo(a.bm.getId());
134
            }
135
        }
136
 
137
        lead.setStage(LeadStage.ASSIGNED);
138
        lead.setStatus(LeadStage.ASSIGNED.toLegacyStatus());
139
        lead.setUnreachableCount(0);
140
 
141
        LocalDateTime base = lead.getCreatedTimestamp() != null ? lead.getCreatedTimestamp() : LocalDateTime.now();
142
        lead.setFirstContactDue(base.plusHours(SLA_HOURS));
143
        return a;
144
    }
145
 
37737 vikas 146
    /**
147
     * Stamp the LMS fields onto a lead created by a path that predates the LMS form.
148
     *
149
     * <p>Deliberately narrower than {@link #assign}: it never writes {@code status}. The legacy
150
     * Leads screen owns that column and sets it per submission (pending / followUp / notInterested);
151
     * {@code assign()} would flatten every one of those to pending and change what those screens
152
     * show. Stage is likewise only set when the caller left it null.
153
     *
154
     * <p>What it always does is start the SLA clock, which is the point: 0 of 37,756 production
155
     * leads had {@code first_contact_due}, so the SLA heat-map and the funnel's SLA% column had
156
     * nothing to measure however correct their queries were.
157
     *
158
     * <p>Region comes from {@code lead.regionId} when the caller knows it, otherwise from the state
159
     * name. An unmappable state (Bihar, Maharashtra) or an ambiguous one (Uttar Pradesh, which spans
160
     * UP West / UP East / Remaining UP West) leaves the lead in HOLD with no owner — still clocked,
161
     * still visible in the HOLD queue, which is the honest outcome rather than a guessed owner.
162
     */
163
    public Assignment stamp(Lead lead, String creationPath) {
164
        if (lead.getCreationPath() == null || lead.getCreationPath().trim().isEmpty()) {
165
            lead.setCreationPath((creationPath != null && creationPath.trim().equalsIgnoreCase("B")) ? "B" : "A");
166
        }
167
 
168
        Integer regionId = lead.getRegionId();
169
        if (regionId == null || regionId <= 0) {
170
            regionId = resolveRegionIdByState(lead.getState());
171
        }
172
        Assignment a = resolve(regionId);
173
        lead.setRegionId(a.regionId);
174
        lead.setRegionCode(a.regionCode);
175
        lead.setAssignmentStatus(a.assignmentStatus);
176
        if (a.bm != null) {
177
            lead.setOwnerBmId(a.bm.getId());
178
        }
179
 
180
        // Stage only when absent — a caller that already knows better keeps its value.
181
        if (lead.getStage() == null) {
182
            lead.setStage(LeadStage.ASSIGNED);
183
        }
184
        if (lead.getUnreachableCount() == null) {
185
            lead.setUnreachableCount(0);
186
        }
187
        // Stamped once. Re-stamping a lead on edit would move the deadline and quietly clear a breach.
188
        if (lead.getFirstContactDue() == null) {
189
            LocalDateTime base = lead.getCreatedTimestamp() != null ? lead.getCreatedTimestamp() : LocalDateTime.now();
190
            lead.setFirstContactDue(base.plusHours(SLA_HOURS));
191
        }
192
        return a;
193
    }
194
 
195
    /**
196
     * cs.region id for a free-text state, matched case-insensitively on the region name.
197
     *
198
     * <p>Six of the seven SOP regions are named exactly as the states the legacy form captures
199
     * (Haryana, Delhi, Punjab, Rajasthan, Uttarakhand, Madhya Pradesh). "Uttar Pradesh" is
200
     * deliberately NOT mapped: it covers three regions and the state alone cannot say which, so
201
     * guessing would hand 15,881 leads to the wrong BM. Those go to HOLD for a human to place.
202
     */
203
    public Integer resolveRegionIdByState(String state) {
204
        if (state == null || state.trim().isEmpty()) {
205
            return null;
206
        }
207
        String wanted = state.trim();
208
        List<Region> all = regionRepository.selectAll();
209
        if (all == null) {
210
            return null;
211
        }
212
        for (Region r : all) {
213
            if (r.getName() != null && r.getName().trim().equalsIgnoreCase(wanted)) {
214
                return r.getId();
215
            }
216
        }
217
        for (Region r : all) {
218
            if (r.getRegionCode() != null && r.getRegionCode().trim().equalsIgnoreCase(wanted)) {
219
                return r.getId();
220
            }
221
        }
222
        return null;
223
    }
224
 
37125 vikas 225
    // ---- LMS id ---------------------------------------------------------------------------
226
 
227
    /** LMS-&lt;REGION&gt;-&lt;YY&gt;-&lt;6-digit id&gt;, e.g. LMS-UPW-26-000123. Call after the lead has an id. */
228
    public String generateLmsCode(Lead lead) {
229
        String region = (lead.getRegionCode() != null && !lead.getRegionCode().isEmpty())
230
                ? lead.getRegionCode() : "GEN";
231
        LocalDateTime created = lead.getCreatedTimestamp() != null ? lead.getCreatedTimestamp() : LocalDateTime.now();
232
        int yy = created.getYear() % 100;
233
        return String.format("LMS-%s-%02d-%06d", region, yy, lead.getId());
234
    }
235
 
236
    // ---- SLA ------------------------------------------------------------------------------
237
 
238
    /** MET / BREACHED / AT_RISK / RUNNING / NA — for the record SLA card. */
239
    public String slaState(Lead lead) {
240
        LocalDateTime due = lead.getFirstContactDue();
241
        if (lead.getFirstContactedAt() != null) {
242
            if (due == null) {
243
                return "MET";
244
            }
245
            return !lead.getFirstContactedAt().isAfter(due) ? "MET" : "BREACHED";
246
        }
247
        if (due == null) {
248
            return "NA";
249
        }
250
        LocalDateTime now = LocalDateTime.now();
251
        if (now.isAfter(due)) {
252
            return "BREACHED";
253
        }
254
        if (now.isAfter(due.minusHours(1))) {
255
            return "AT_RISK";
256
        }
257
        return "RUNNING";
258
    }
259
}