Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.service.lms;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.cs.Position;import com.spice.profitmandi.dao.entity.user.Lead;import com.spice.profitmandi.dao.entity.user.LeadCall;import com.spice.profitmandi.dao.repository.cs.PositionRepository;import com.spice.profitmandi.service.AuthService;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.Arrays;import java.util.HashSet;import java.util.List;import java.util.Set;/*** Who may listen to a lead's call recording (SOP §15). Until now this rule existed only as a static* HTML table on the LMS dashboard — five roles against five columns, read by nobody — while the* actual recording URL rendered raw into the page for anyone who could open the lead.** <p>Four widening tiers, evaluated narrowest-first so the audit log records the most specific reason* access was allowed:** <ul>* <li><b>OWN</b> — the agent who placed the call.</li>* <li><b>REPORTS</b> — anyone above them on {@code auth_user.manager_id}.</li>* <li><b>STATE</b> — a BM/RSM holding a SALES position in the lead's region.</li>* <li><b>ALL</b> — the audit team and upper management.</li>* </ul>** <p><b>On the ALL tier:</b> this codebase has no role for "audit team" — {@code RoleType} is only* USER/RETAILER/FOFO/FOFO_ADMIN, and essentially every internal staff member is FOFO_ADMIN, so* granting on that would void the gate entirely. The house pattern is a hardcoded email set, but* there are already four copies of that in the Beat controllers and they have drifted apart, so this* uses a single config property instead — same effect, one place, changeable without a redeploy.*/@Servicepublic class RecordingAccessService {private static final Logger LOGGER = LogManager.getLogger(RecordingAccessService.class);/** Ticket category that carries sales positions; BM/RSM live here. */private static final int SALES = com.spice.profitmandi.common.model.ProfitMandiConstants.TICKET_CATEGORY_SALES;/*** Wildcard region meaning "all partners" ({@code CsServiceImpl.ALL_PARTNERS_REGION}). Someone* holding a position in it covers every region, so a plain {@code contains} would under-grant.*/private static final int ALL_PARTNERS_REGION = 5;public static final String BASIS_OWN = "OWN";public static final String BASIS_REPORTS = "REPORTS";public static final String BASIS_STATE = "STATE";public static final String BASIS_ALL = "ALL";/** Comma-separated emails granted the ALL tier — audit team and upper management. */@Value("${lms.recording.audit.emails:}")private String auditEmails;@Autowiredprivate AuthService authService;@Autowiredprivate PositionRepository positionRepository;/** Outcome of an access check, carrying the basis the audit log wants. */public static class Decision {public final boolean granted;/** OWN | REPORTS | STATE | ALL, or null when denied. */public final String basis;public final String deniedReason;private Decision(boolean granted, String basis, String deniedReason) {this.granted = granted;this.basis = basis;this.deniedReason = deniedReason;}static Decision grant(String basis) {return new Decision(true, basis, null);}static Decision deny(String reason) {return new Decision(false, null, reason);}}/*** May {@code me} open the recording of {@code call} on {@code lead}? Never throws — a failure to* resolve the hierarchy denies rather than opens.*/public Decision decide(AuthUser me, LeadCall call, Lead lead) {if (me == null) {return Decision.deny("No authenticated user");}if (call == null) {return Decision.deny("Call not found");}if (call.getAuthId() == me.getId()) {return Decision.grant(BASIS_OWN);}if (isAuditor(me)) {return Decision.grant(BASIS_ALL);}if (isInDownline(me, call.getAuthId())) {return Decision.grant(BASIS_REPORTS);}if (coversRegion(me, lead)) {return Decision.grant(BASIS_STATE);}return Decision.deny("Outside your reporting line and region");}/** The ALL tier — audit team / upper management, by configured email. */public boolean isAuditor(AuthUser me) {if (me == null || me.getEmailId() == null) {return false;}return auditEmailSet().contains(me.getEmailId().trim().toLowerCase());}private Set<String> auditEmailSet() {Set<String> out = new HashSet<>();if (auditEmails == null || auditEmails.trim().isEmpty()) {return out;}for (String e : auditEmails.split(",")) {String trimmed = e.trim().toLowerCase();if (!trimmed.isEmpty()) {out.add(trimmed);}}return out;}/*** REPORTS tier. Note {@code getAllReportees} only walks <em>active</em> users, so a recording made* by a since-deactivated agent falls out of their old manager's downline. That is a real hole for* ex-employees; the ALL tier is the intended way to reach those.*/private boolean isInDownline(AuthUser me, int agentAuthId) {try {List<Integer> reportees = authService.getAllReportees(me.getId());return reportees != null && reportees.contains(agentAuthId);} catch (Exception e) {LOGGER.warn("Could not resolve the downline for auth {} — denying", me.getId(), e);return false;}}/** STATE tier — a SALES position in the lead's region (or the all-partners wildcard region). */private boolean coversRegion(AuthUser me, Lead lead) {if (lead == null || lead.getRegionId() == null || lead.getRegionId() <= 0) {// Pre-LMS leads carry no region; there is nothing for a regional grant to match on.return false;}try {List<Position> positions = positionRepository.selectPositionByAuthId(me.getId());if (positions == null) {return false;}for (Position p : positions) {if (p.getCategoryId() != SALES) {continue;}if (p.getRegionId() == ALL_PARTNERS_REGION|| p.getRegionId() == lead.getRegionId().intValue()) {return true;}}return false;} catch (Exception e) {LOGGER.warn("Could not resolve regions for auth {} — denying", me.getId(), e);return false;}}/** Exposed for the dashboard's access-matrix tile. */public Set<String> configuredAuditEmails() {return new HashSet<>(auditEmailSet());}/** Tier names, narrowest first — used by the dashboard to render the live matrix. */public static List<String> tiers() {return Arrays.asList(BASIS_OWN, BASIS_REPORTS, BASIS_STATE, BASIS_ALL);}}