Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37651 vikas 1
package com.spice.profitmandi.service.lms;
2
 
3
import com.spice.profitmandi.dao.entity.auth.AuthUser;
4
import com.spice.profitmandi.dao.entity.cs.Position;
5
import com.spice.profitmandi.dao.entity.user.Lead;
6
import com.spice.profitmandi.dao.entity.user.LeadCall;
7
import com.spice.profitmandi.dao.repository.cs.PositionRepository;
8
import com.spice.profitmandi.service.AuthService;
9
import org.apache.logging.log4j.LogManager;
10
import org.apache.logging.log4j.Logger;
11
import org.springframework.beans.factory.annotation.Autowired;
12
import org.springframework.beans.factory.annotation.Value;
13
import org.springframework.stereotype.Service;
14
 
15
import java.util.Arrays;
16
import java.util.HashSet;
17
import java.util.List;
18
import java.util.Set;
19
 
20
/**
21
 * Who may listen to a lead's call recording (SOP §15). Until now this rule existed only as a static
22
 * HTML table on the LMS dashboard — five roles against five columns, read by nobody — while the
23
 * actual recording URL rendered raw into the page for anyone who could open the lead.
24
 *
25
 * <p>Four widening tiers, evaluated narrowest-first so the audit log records the most specific reason
26
 * access was allowed:
27
 *
28
 * <ul>
29
 *   <li><b>OWN</b> — the agent who placed the call.</li>
30
 *   <li><b>REPORTS</b> — anyone above them on {@code auth_user.manager_id}.</li>
31
 *   <li><b>STATE</b> — a BM/RSM holding a SALES position in the lead's region.</li>
32
 *   <li><b>ALL</b> — the audit team and upper management.</li>
33
 * </ul>
34
 *
35
 * <p><b>On the ALL tier:</b> this codebase has no role for "audit team" — {@code RoleType} is only
36
 * USER/RETAILER/FOFO/FOFO_ADMIN, and essentially every internal staff member is FOFO_ADMIN, so
37
 * granting on that would void the gate entirely. The house pattern is a hardcoded email set, but
38
 * there are already four copies of that in the Beat controllers and they have drifted apart, so this
39
 * uses a single config property instead — same effect, one place, changeable without a redeploy.
40
 */
41
@Service
42
public class RecordingAccessService {
43
 
44
    private static final Logger LOGGER = LogManager.getLogger(RecordingAccessService.class);
45
 
46
    /** Ticket category that carries sales positions; BM/RSM live here. */
47
    private static final int SALES = com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES;
48
 
49
    /**
50
     * Wildcard region meaning "all partners" ({@code CsServiceImpl.ALL_PARTNERS_REGION}). Someone
51
     * holding a position in it covers every region, so a plain {@code contains} would under-grant.
52
     */
53
    private static final int ALL_PARTNERS_REGION = 5;
54
 
55
    public static final String BASIS_OWN = "OWN";
56
    public static final String BASIS_REPORTS = "REPORTS";
57
    public static final String BASIS_STATE = "STATE";
58
    public static final String BASIS_ALL = "ALL";
59
 
60
    /** Comma-separated emails granted the ALL tier — audit team and upper management. */
61
    @Value("${lms.recording.audit.emails:}")
62
    private String auditEmails;
63
 
64
    @Autowired
65
    private AuthService authService;
66
 
67
    @Autowired
68
    private PositionRepository positionRepository;
69
 
70
    /** Outcome of an access check, carrying the basis the audit log wants. */
71
    public static class Decision {
72
        public final boolean granted;
73
        /** OWN | REPORTS | STATE | ALL, or null when denied. */
74
        public final String basis;
75
        public final String deniedReason;
76
 
77
        private Decision(boolean granted, String basis, String deniedReason) {
78
            this.granted = granted;
79
            this.basis = basis;
80
            this.deniedReason = deniedReason;
81
        }
82
 
83
        static Decision grant(String basis) {
84
            return new Decision(true, basis, null);
85
        }
86
 
87
        static Decision deny(String reason) {
88
            return new Decision(false, null, reason);
89
        }
90
    }
91
 
92
    /**
93
     * May {@code me} open the recording of {@code call} on {@code lead}? Never throws — a failure to
94
     * resolve the hierarchy denies rather than opens.
95
     */
96
    public Decision decide(AuthUser me, LeadCall call, Lead lead) {
97
        if (me == null) {
98
            return Decision.deny("No authenticated user");
99
        }
100
        if (call == null) {
101
            return Decision.deny("Call not found");
102
        }
103
 
104
        if (call.getAuthId() == me.getId()) {
105
            return Decision.grant(BASIS_OWN);
106
        }
107
        if (isAuditor(me)) {
108
            return Decision.grant(BASIS_ALL);
109
        }
110
        if (isInDownline(me, call.getAuthId())) {
111
            return Decision.grant(BASIS_REPORTS);
112
        }
113
        if (coversRegion(me, lead)) {
114
            return Decision.grant(BASIS_STATE);
115
        }
116
        return Decision.deny("Outside your reporting line and region");
117
    }
118
 
119
    /** The ALL tier — audit team / upper management, by configured email. */
120
    public boolean isAuditor(AuthUser me) {
121
        if (me == null || me.getEmailId() == null) {
122
            return false;
123
        }
124
        return auditEmailSet().contains(me.getEmailId().trim().toLowerCase());
125
    }
126
 
127
    private Set<String> auditEmailSet() {
128
        Set<String> out = new HashSet<>();
129
        if (auditEmails == null || auditEmails.trim().isEmpty()) {
130
            return out;
131
        }
132
        for (String e : auditEmails.split(",")) {
133
            String trimmed = e.trim().toLowerCase();
134
            if (!trimmed.isEmpty()) {
135
                out.add(trimmed);
136
            }
137
        }
138
        return out;
139
    }
140
 
141
    /**
142
     * REPORTS tier. Note {@code getAllReportees} only walks <em>active</em> users, so a recording made
143
     * by a since-deactivated agent falls out of their old manager's downline. That is a real hole for
144
     * ex-employees; the ALL tier is the intended way to reach those.
145
     */
146
    private boolean isInDownline(AuthUser me, int agentAuthId) {
147
        try {
148
            List<Integer> reportees = authService.getAllReportees(me.getId());
149
            return reportees != null && reportees.contains(agentAuthId);
150
        } catch (Exception e) {
151
            LOGGER.warn("Could not resolve the downline for auth {} — denying", me.getId(), e);
152
            return false;
153
        }
154
    }
155
 
156
    /** STATE tier — a SALES position in the lead's region (or the all-partners wildcard region). */
157
    private boolean coversRegion(AuthUser me, Lead lead) {
158
        if (lead == null || lead.getRegionId() == null || lead.getRegionId() <= 0) {
159
            // Pre-LMS leads carry no region; there is nothing for a regional grant to match on.
160
            return false;
161
        }
162
        try {
163
            List<Position> positions = positionRepository.selectPositionByAuthId(me.getId());
164
            if (positions == null) {
165
                return false;
166
            }
167
            for (Position p : positions) {
168
                if (p.getCategoryId() != SALES) {
169
                    continue;
170
                }
171
                if (p.getRegionId() == ALL_PARTNERS_REGION
172
                        || p.getRegionId() == lead.getRegionId().intValue()) {
173
                    return true;
174
                }
175
            }
176
            return false;
177
        } catch (Exception e) {
178
            LOGGER.warn("Could not resolve regions for auth {} — denying", me.getId(), e);
179
            return false;
180
        }
181
    }
182
 
183
    /** Exposed for the dashboard's access-matrix tile. */
184
    public Set<String> configuredAuditEmails() {
185
        return new HashSet<>(auditEmailSet());
186
    }
187
 
188
    /** Tier names, narrowest first — used by the dashboard to render the live matrix. */
189
    public static List<String> tiers() {
190
        return Arrays.asList(BASIS_OWN, BASIS_REPORTS, BASIS_STATE, BASIS_ALL);
191
    }
192
}