| 37167 |
amit |
1 |
package com.spice.profitmandi.service;
|
|
|
2 |
|
|
|
3 |
import com.spice.profitmandi.dao.enumuration.dtr.AgendaType;
|
|
|
4 |
|
|
|
5 |
import java.util.LinkedHashMap;
|
|
|
6 |
import java.util.Map;
|
|
|
7 |
|
|
|
8 |
/**
|
|
|
9 |
* Pure decision logic for the 5 AUTO agenda rules. No Spring, no DB — the
|
|
|
10 |
* nightly sync gathers inputs and this class says per agenda type whether the
|
|
|
11 |
* AUTO instance should be OPEN, CLOSED, or left as-is (LEAVE).
|
|
|
12 |
*
|
|
|
13 |
* LEAVE is load-bearing for LOW_SALES hysteresis: between flagPct and clearPct
|
|
|
14 |
* the current state (whatever it is) holds.
|
|
|
15 |
*/
|
|
|
16 |
public class AgendaRuleDecider {
|
|
|
17 |
|
|
|
18 |
public enum Action {OPEN, CLOSE, LEAVE}
|
|
|
19 |
|
|
|
20 |
public static class Decision {
|
|
|
21 |
public final Action action;
|
|
|
22 |
public final String metric;
|
|
|
23 |
|
|
|
24 |
Decision(Action action, String metric) {
|
|
|
25 |
this.action = action;
|
|
|
26 |
this.metric = metric;
|
|
|
27 |
}
|
|
|
28 |
}
|
|
|
29 |
|
|
|
30 |
/** Per-store inputs, gathered by the sync orchestrator. */
|
|
|
31 |
public static class Inputs {
|
|
|
32 |
public int fofoId;
|
|
|
33 |
public boolean revival; // fofo_store.activation_type == REVIVAL
|
|
|
34 |
public boolean storeActivatedRecently; // active_timestamp inside the sales lookback window
|
|
|
35 |
public float minInvestment;
|
|
|
36 |
public Double shortPercentage; // null = investment could not be computed
|
|
|
37 |
public double baselineAmt; // fofo sales, days (windowDays..windowDays+baselineDays]
|
|
|
38 |
public double currentAmt; // fofo sales, last windowDays
|
|
|
39 |
public Integer lastBillDaysAgo; // null = never billed
|
|
|
40 |
public Integer oldestLoanAgeDays; // null = no open loan with pending amount
|
|
|
41 |
public double pendingLoanAmt;
|
|
|
42 |
}
|
|
|
43 |
|
|
|
44 |
public static Map<AgendaType, Decision> decide(Inputs in,
|
|
|
45 |
Map<String, Double> investParams,
|
|
|
46 |
Map<String, Double> salesParams,
|
|
|
47 |
Map<String, Double> purchaseParams,
|
|
|
48 |
Map<String, Double> creditParams) {
|
|
|
49 |
Map<AgendaType, Decision> out = new LinkedHashMap<>();
|
|
|
50 |
|
| 37181 |
amit |
51 |
// NOTE on REVIVAL stores: instances keep their own lifecycle here — the
|
|
|
52 |
// rules evaluate on data regardless of activation state. Suppression for
|
|
|
53 |
// revival stores happens at the DISPLAY layer (visit agenda + checkout
|
|
|
54 |
// remarks show only REVIVAL, CREDIT_DUES and MANUAL instances — see
|
|
|
55 |
// AgendaInstanceServiceImpl.filterVisibleForVisit).
|
| 37180 |
amit |
56 |
|
| 37167 |
amit |
57 |
// LOW_INVESTMENT — existing platform metric, PJP trigger at shortPct.
|
| 37181 |
amit |
58 |
if (in.shortPercentage == null || in.minInvestment <= 0) {
|
| 37167 |
amit |
59 |
out.put(AgendaType.LOW_INVESTMENT, new Decision(Action.LEAVE, null));
|
|
|
60 |
} else {
|
|
|
61 |
double shortPct = param(investParams, "shortPct", 50);
|
|
|
62 |
String metric = "{\"shortPct\":" + Math.round(in.shortPercentage) + "}";
|
|
|
63 |
out.put(AgendaType.LOW_INVESTMENT, new Decision(
|
|
|
64 |
in.shortPercentage > shortPct ? Action.OPEN : Action.CLOSE, metric));
|
|
|
65 |
}
|
|
|
66 |
|
|
|
67 |
// LOW_SALES — hysteresis vs trailing baseline.
|
|
|
68 |
double monthlyBaseline = in.baselineAmt / (param(salesParams, "baselineDays", 90) / 30d);
|
| 37181 |
amit |
69 |
if (in.storeActivatedRecently || monthlyBaseline <= 0) {
|
| 37167 |
amit |
70 |
out.put(AgendaType.LOW_SALES, new Decision(Action.LEAVE, null));
|
|
|
71 |
} else {
|
|
|
72 |
double flagPct = param(salesParams, "flagPct", 40);
|
|
|
73 |
double clearPct = param(salesParams, "clearPct", 75);
|
|
|
74 |
String metric = "{\"current\":" + Math.round(in.currentAmt)
|
|
|
75 |
+ ",\"monthlyBaseline\":" + Math.round(monthlyBaseline) + "}";
|
|
|
76 |
if (in.currentAmt < flagPct / 100d * monthlyBaseline) {
|
|
|
77 |
out.put(AgendaType.LOW_SALES, new Decision(Action.OPEN, metric));
|
|
|
78 |
} else if (in.currentAmt > clearPct / 100d * monthlyBaseline) {
|
|
|
79 |
out.put(AgendaType.LOW_SALES, new Decision(Action.CLOSE, metric));
|
|
|
80 |
} else {
|
|
|
81 |
out.put(AgendaType.LOW_SALES, new Decision(Action.LEAVE, metric));
|
|
|
82 |
}
|
|
|
83 |
}
|
|
|
84 |
|
| 37180 |
amit |
85 |
// LOW_PURCHASE — days since last billed purchase.
|
| 37181 |
amit |
86 |
{
|
| 37167 |
amit |
87 |
double gapDays = param(purchaseParams, "gapDays", 15);
|
|
|
88 |
boolean flag = in.lastBillDaysAgo == null || in.lastBillDaysAgo >= gapDays;
|
|
|
89 |
String metric = "{\"gapDays\":" + (in.lastBillDaysAgo == null ? -1 : in.lastBillDaysAgo) + "}";
|
|
|
90 |
out.put(AgendaType.LOW_PURCHASE, new Decision(flag ? Action.OPEN : Action.CLOSE, metric));
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
// CREDIT_DUES — open loan aged >= loanAgeDays from disbursal (created_on).
|
|
|
94 |
double loanAgeDays = param(creditParams, "loanAgeDays", 20);
|
|
|
95 |
if (in.oldestLoanAgeDays != null && in.oldestLoanAgeDays >= loanAgeDays) {
|
|
|
96 |
out.put(AgendaType.CREDIT_DUES, new Decision(Action.OPEN,
|
|
|
97 |
"{\"oldestLoanDays\":" + in.oldestLoanAgeDays
|
|
|
98 |
+ ",\"pending\":" + Math.round(in.pendingLoanAmt) + "}"));
|
|
|
99 |
} else {
|
|
|
100 |
out.put(AgendaType.CREDIT_DUES, new Decision(Action.CLOSE,
|
|
|
101 |
"{\"oldestLoanDays\":" + (in.oldestLoanAgeDays == null ? 0 : in.oldestLoanAgeDays) + "}"));
|
|
|
102 |
}
|
|
|
103 |
|
|
|
104 |
// REVIVAL — mirrors the 23:30 activation-type cron.
|
|
|
105 |
out.put(AgendaType.REVIVAL, new Decision(
|
|
|
106 |
in.revival ? Action.OPEN : Action.CLOSE, "{\"activationType\":\"" + (in.revival ? "REVIVAL" : "ACTIVE") + "\"}"));
|
|
|
107 |
|
|
|
108 |
return out;
|
|
|
109 |
}
|
|
|
110 |
|
|
|
111 |
private static double param(Map<String, Double> params, String key, double fallback) {
|
|
|
112 |
if (params == null) return fallback;
|
|
|
113 |
Double v = params.get(key);
|
|
|
114 |
return v != null ? v : fallback;
|
|
|
115 |
}
|
|
|
116 |
}
|