Subversion Repositories SmartDukaan

Rev

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.
 */
@Service
public 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;

    @Autowired
    private AuthService authService;

    @Autowired
    private 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);
    }
}