Subversion Repositories SmartDukaan

Rev

Rev 37180 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.service;

import com.spice.profitmandi.dao.enumuration.dtr.AgendaType;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Pure decision logic for the 5 AUTO agenda rules. No Spring, no DB — the
 * nightly sync gathers inputs and this class says per agenda type whether the
 * AUTO instance should be OPEN, CLOSED, or left as-is (LEAVE).
 *
 * LEAVE is load-bearing for LOW_SALES hysteresis: between flagPct and clearPct
 * the current state (whatever it is) holds.
 */
public class AgendaRuleDecider {

    public enum Action {OPEN, CLOSE, LEAVE}

    public static class Decision {
        public final Action action;
        public final String metric;

        Decision(Action action, String metric) {
            this.action = action;
            this.metric = metric;
        }
    }

    /** Per-store inputs, gathered by the sync orchestrator. */
    public static class Inputs {
        public int fofoId;
        public boolean revival;                 // fofo_store.activation_type == REVIVAL
        public boolean storeActivatedRecently;  // active_timestamp inside the sales lookback window
        public float minInvestment;
        public Double shortPercentage;          // null = investment could not be computed
        public double baselineAmt;              // fofo sales, days (windowDays..windowDays+baselineDays]
        public double currentAmt;               // fofo sales, last windowDays
        public Integer lastBillDaysAgo;         // null = never billed
        public Integer oldestLoanAgeDays;       // null = no open loan with pending amount
        public double pendingLoanAmt;
    }

    public static Map<AgendaType, Decision> decide(Inputs in,
                                                   Map<String, Double> investParams,
                                                   Map<String, Double> salesParams,
                                                   Map<String, Double> purchaseParams,
                                                   Map<String, Double> creditParams) {
        Map<AgendaType, Decision> out = new LinkedHashMap<>();

        // LOW_INVESTMENT — existing platform metric, PJP trigger at shortPct.
        if (in.shortPercentage == null || in.minInvestment <= 0) {
            out.put(AgendaType.LOW_INVESTMENT, new Decision(Action.LEAVE, null));
        } else {
            double shortPct = param(investParams, "shortPct", 50);
            String metric = "{\"shortPct\":" + Math.round(in.shortPercentage) + "}";
            out.put(AgendaType.LOW_INVESTMENT, new Decision(
                    in.shortPercentage > shortPct ? Action.OPEN : Action.CLOSE, metric));
        }

        // LOW_SALES — hysteresis vs trailing baseline.
        double monthlyBaseline = in.baselineAmt / (param(salesParams, "baselineDays", 90) / 30d);
        if (in.storeActivatedRecently || monthlyBaseline <= 0) {
            out.put(AgendaType.LOW_SALES, new Decision(Action.LEAVE, null));
        } else {
            double flagPct = param(salesParams, "flagPct", 40);
            double clearPct = param(salesParams, "clearPct", 75);
            String metric = "{\"current\":" + Math.round(in.currentAmt)
                    + ",\"monthlyBaseline\":" + Math.round(monthlyBaseline) + "}";
            if (in.currentAmt < flagPct / 100d * monthlyBaseline) {
                out.put(AgendaType.LOW_SALES, new Decision(Action.OPEN, metric));
            } else if (in.currentAmt > clearPct / 100d * monthlyBaseline) {
                out.put(AgendaType.LOW_SALES, new Decision(Action.CLOSE, metric));
            } else {
                out.put(AgendaType.LOW_SALES, new Decision(Action.LEAVE, metric));
            }
        }

        // LOW_PURCHASE — days since last billed purchase; REVIVAL stores are
        // the Revival agenda's problem, not this one's.
        if (in.revival) {
            out.put(AgendaType.LOW_PURCHASE, new Decision(Action.CLOSE, "{\"reason\":\"REVIVAL\"}"));
        } else {
            double gapDays = param(purchaseParams, "gapDays", 15);
            boolean flag = in.lastBillDaysAgo == null || in.lastBillDaysAgo >= gapDays;
            String metric = "{\"gapDays\":" + (in.lastBillDaysAgo == null ? -1 : in.lastBillDaysAgo) + "}";
            out.put(AgendaType.LOW_PURCHASE, new Decision(flag ? Action.OPEN : Action.CLOSE, metric));
        }

        // CREDIT_DUES — open loan aged >= loanAgeDays from disbursal (created_on).
        double loanAgeDays = param(creditParams, "loanAgeDays", 20);
        if (in.oldestLoanAgeDays != null && in.oldestLoanAgeDays >= loanAgeDays) {
            out.put(AgendaType.CREDIT_DUES, new Decision(Action.OPEN,
                    "{\"oldestLoanDays\":" + in.oldestLoanAgeDays
                            + ",\"pending\":" + Math.round(in.pendingLoanAmt) + "}"));
        } else {
            out.put(AgendaType.CREDIT_DUES, new Decision(Action.CLOSE,
                    "{\"oldestLoanDays\":" + (in.oldestLoanAgeDays == null ? 0 : in.oldestLoanAgeDays) + "}"));
        }

        // REVIVAL — mirrors the 23:30 activation-type cron.
        out.put(AgendaType.REVIVAL, new Decision(
                in.revival ? Action.OPEN : Action.CLOSE, "{\"activationType\":\"" + (in.revival ? "REVIVAL" : "ACTIVE") + "\"}"));

        return out;
    }

    private static double param(Map<String, Double> params, String key, double fallback) {
        if (params == null) return fallback;
        Double v = params.get(key);
        return v != null ? v : fallback;
    }
}