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.user.LeadCall;
4
import com.spice.profitmandi.dao.repository.dtr.LeadCallRepository;
5
import com.spice.profitmandi.service.storage.ObjectStoreService;
6
import org.apache.logging.log4j.LogManager;
7
import org.apache.logging.log4j.Logger;
8
import org.springframework.beans.factory.annotation.Autowired;
9
import org.springframework.beans.factory.annotation.Value;
10
import org.springframework.stereotype.Service;
11
import org.springframework.transaction.annotation.Transactional;
12
 
13
import java.util.List;
14
 
15
/**
16
 * Finds call recordings the provider has announced but we have not stored yet, and hands each to
17
 * {@link RecordingArchiveWorker}. Owning the audio ourselves is what makes the 24-month retention in
18
 * SOP §17 our promise to keep rather than the vendor's, and what keeps playback working after a
19
 * provider switch.
20
 *
21
 * <p>Deliberately a sweep rather than work done straight off the recording webhook: the webhook fires
22
 * the moment the call ends, but the provider needs a moment before the file is actually fetchable,
23
 * and a webhook has to ACK immediately regardless. The sweep doubles as the retry — the work list is
24
 * simply "rows with a recording handle and no archived document", so nothing is lost across a restart.
25
 *
26
 * <p>This class holds no {@code @Scheduled} annotation on purpose. It lives in {@code profitmandi-dao},
27
 * which is loaded by the fofo WAR, profitmandi-web and profitmandi-cron alike; scheduling it here
28
 * would run the same sweep in every one of them. The trigger belongs to whichever app owns the job.
29
 */
30
@Service
31
public class RecordingArchiveService {
32
 
33
    private static final Logger LOGGER = LogManager.getLogger(RecordingArchiveService.class);
34
 
35
    @Autowired
36
    private LeadCallRepository leadCallRepository;
37
 
38
    @Autowired
39
    private ObjectStoreService objectStoreService;
40
 
41
    @Autowired
42
    private LmsDialerProvider dialerProvider;
43
 
44
    @Autowired
45
    private RecordingArchiveWorker recordingArchiveWorker;
46
 
47
    @Value("${lms.recording.archive.enabled:true}")
48
    private boolean enabled;
49
 
50
    @Value("${lms.recording.archive.batch.size:25}")
51
    private int batchSize;
52
 
53
    /**
54
     * How far back to look for recordings. MUST track the provider's own retention — Vonage keeps a
55
     * recording for 30 days, and a shorter window here silently makes recoverable audio
56
     * unrecoverable after any pause in the sweep.
57
     */
58
    @Value("${lms.recording.provider.retention.days:30}")
59
    private int providerRetentionDays;
60
 
61
    /** Give up after this many failed fetches so a dead row cannot occupy a batch slot forever. */
62
    @Value("${lms.recording.archive.max.attempts:5}")
63
    private int maxAttempts;
64
 
65
    /**
66
     * Log recordings the sweep will never pick up again.
67
     *
68
     * <p>The pending query excludes rows past the provider's retention window or over the attempt
69
     * ceiling — necessary, or one dead row starves every batch behind it. But an exclusion that
70
     * happens silently is how you discover during an audit that a month of audio is missing, so the
71
     * dropped rows are named here.
72
     */
73
    private void reportAbandoned() {
74
        try {
75
            List<LeadCall> abandoned =
76
                    leadCallRepository.selectAbandonedRecordings(50, providerRetentionDays, maxAttempts);
77
            if (abandoned == null || abandoned.isEmpty()) {
78
                return;
79
            }
80
            StringBuilder ids = new StringBuilder();
81
            for (LeadCall call : abandoned) {
82
                if (ids.length() > 0) {
83
                    ids.append(", ");
84
                }
85
                ids.append(call.getId());
86
            }
87
            LOGGER.error("{} call recording(s) will NOT be archived — past the {}-day provider window "
88
                            + "or over {} failed attempts. Call ids: {}",
89
                    abandoned.size(), providerRetentionDays, maxAttempts, ids);
90
        } catch (Exception e) {
91
            LOGGER.warn("Could not report abandoned recordings", e);
92
        }
93
    }
94
 
95
    /**
96
     * Ask the vendor for the recording of each recently answered call and stamp the handle on the row.
97
     *
98
     * <p>Runs before {@link #sweep()} and is what makes recordings exist at all for a server-placed
99
     * provider: those place the call over REST and receive no recording webhook, so without this step
100
     * {@code recording_uuid} stays null forever and the archival sweep — which selects on that column
101
     * — never sees a single call.
102
     *
103
     * <p>Deliberately not done at hang-up: the vendor needs a moment to finalise the file, and a call
104
     * that has just ended usually is not in the recording list yet. Being a sweep makes the retry free.
105
     *
106
     * @return how many recordings were newly linked
107
     */
108
    @Transactional
109
    public int linkSweep() {
110
        if (!enabled || !dialerProvider.isConfigured()) {
111
            return 0;
112
        }
113
        List<LeadCall> pending = leadCallRepository.selectPendingRecordingLink(batchSize, providerRetentionDays);
114
        if (pending == null || pending.isEmpty()) {
115
            return 0;
116
        }
117
        int linked = 0;
118
        for (LeadCall call : pending) {
119
            try {
120
                LmsDialerProvider.RecordingRef ref = dialerProvider.findRecording(call);
121
                if (ref == null || ref.id == null) {
122
                    // Normal: not recorded, or not finalised yet. The next sweep asks again.
123
                    continue;
124
                }
125
                call.setRecordingUuid(ref.id);
126
                if (ref.durationSeconds > 0) {
127
                    call.setRecordingDuration(ref.durationSeconds);
128
                }
129
                leadCallRepository.persist(call);
130
                linked++;
131
                LOGGER.info("Linked recording {} to call {} (vendor call {})",
132
                        ref.id, call.getId(), call.getProviderCallUuid());
133
            } catch (Exception e) {
134
                // One bad row must not stop the batch — the rest still get linked.
135
                LOGGER.warn("Could not link a recording for call {}", call.getId(), e);
136
            }
137
        }
138
        return linked;
139
    }
140
 
141
    /**
142
     * Queue every pending recording for archival.
143
     *
144
     * @return how many were queued; 0 when disabled, unconfigured, or nothing is waiting
145
     */
146
    @Transactional
147
    public int sweep() {
148
        if (!enabled) {
149
            return 0;
150
        }
151
        if (!objectStoreService.isEnabled() || !dialerProvider.isConfigured()) {
152
            LOGGER.debug("Recording archive skipped — object store or dialer not configured");
153
            return 0;
154
        }
155
        List<LeadCall> pending = leadCallRepository.selectPendingRecordingArchive(batchSize, providerRetentionDays, maxAttempts);
156
        if (pending == null || pending.isEmpty()) {
157
            return 0;
158
        }
159
        LOGGER.info("Queueing {} call recording(s) for archival", pending.size());
160
        reportAbandoned();
161
        for (LeadCall call : pending) {
162
            // Through the injected worker, never this.archiveOne — a self-call would bypass the
163
            // proxy and lose both @Async and @Transactional.
164
            recordingArchiveWorker.archiveOne(call.getId());
165
        }
166
        return pending.size();
167
    }
168
}