Subversion Repositories SmartDukaan

Rev

Rev 37409 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37409 Rev 37508
Line 1... Line 1...
1
package com.spice.profitmandi.service;
1
package com.spice.profitmandi.service;
2
 
2
 
3
import com.google.gson.Gson;
3
import com.google.gson.Gson;
4
import com.spice.profitmandi.common.enumuration.MessageType;
4
import com.spice.profitmandi.common.enumuration.MessageType;
-
 
5
import com.spice.profitmandi.common.enumuration.NotificationFormat;
-
 
6
import com.spice.profitmandi.common.enumuration.ScheduleStatus;
5
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
7
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
6
import com.spice.profitmandi.common.model.CustomRetailer;
8
import com.spice.profitmandi.common.model.CustomRetailer;
-
 
9
import com.spice.profitmandi.common.model.NotificationTargetCriteria;
7
import com.spice.profitmandi.common.model.ProfitMandiConstants;
10
import com.spice.profitmandi.common.model.ProfitMandiConstants;
8
import com.spice.profitmandi.common.model.SendNotificationModel;
11
import com.spice.profitmandi.common.model.SendNotificationModel;
9
import com.spice.profitmandi.dao.entity.auth.AuthUser;
12
import com.spice.profitmandi.dao.entity.auth.AuthUser;
10
import com.spice.profitmandi.dao.entity.dtr.*;
13
import com.spice.profitmandi.dao.entity.dtr.*;
11
import com.spice.profitmandi.dao.entity.fofo.Customer;
14
import com.spice.profitmandi.dao.entity.fofo.Customer;
12
import com.spice.profitmandi.dao.entity.user.Device;
15
import com.spice.profitmandi.dao.entity.user.Device;
13
import com.spice.profitmandi.dao.entity.whatsapp.WhatsappMessage;
16
import com.spice.profitmandi.dao.entity.whatsapp.WhatsappMessage;
14
import com.spice.profitmandi.dao.model.SimpleCampaign;
-
 
15
import com.spice.profitmandi.dao.model.SimpleCampaignParams;
17
import com.spice.profitmandi.dao.model.SimpleCampaignParams;
16
import com.spice.profitmandi.dao.repository.catalog.DeviceRepository;
18
import com.spice.profitmandi.dao.repository.catalog.DeviceRepository;
17
import com.spice.profitmandi.dao.repository.cs.CsService;
19
import com.spice.profitmandi.dao.repository.cs.CsService;
18
import com.spice.profitmandi.dao.repository.cs.PartnerRegionRepository;
20
import com.spice.profitmandi.dao.repository.cs.PartnerRegionRepository;
19
import com.spice.profitmandi.dao.repository.dtr.*;
21
import com.spice.profitmandi.dao.repository.dtr.*;
20
import com.spice.profitmandi.dao.repository.fofo.CustomerRepository;
22
import com.spice.profitmandi.dao.repository.fofo.CustomerRepository;
21
import com.spice.profitmandi.dao.repository.whatsapp.WhatsappMessageRepository;
23
import com.spice.profitmandi.dao.repository.whatsapp.WhatsappMessageRepository;
22
import com.spice.profitmandi.service.user.RetailerService;
24
import com.spice.profitmandi.service.user.RetailerService;
23
import com.spice.profitmandi.service.whatsapp.WhatsappFlow;
25
import com.spice.profitmandi.service.whatsapp.*;
24
import com.spice.profitmandi.service.whatsapp.WhatsappMessageService;
-
 
25
import com.spice.profitmandi.service.whatsapp.WhatsappMessageType;
-
 
26
import com.spice.profitmandi.service.whatsapp.WhatsappNumbers;
-
 
27
import com.spice.profitmandi.service.whatsapp.BotPenguinWhatsappService;
-
 
28
import org.apache.logging.log4j.LogManager;
26
import org.apache.logging.log4j.LogManager;
29
import org.apache.logging.log4j.Logger;
27
import org.apache.logging.log4j.Logger;
30
import org.springframework.beans.factory.annotation.Autowired;
28
import org.springframework.beans.factory.annotation.Autowired;
31
import org.springframework.beans.factory.annotation.Value;
29
import org.springframework.beans.factory.annotation.Value;
32
import org.springframework.stereotype.Component;
30
import org.springframework.stereotype.Component;
-
 
31
import org.springframework.transaction.annotation.Propagation;
-
 
32
import org.springframework.transaction.annotation.Transactional;
33
 
33
 
34
import java.nio.charset.StandardCharsets;
34
import java.nio.charset.StandardCharsets;
35
import java.time.LocalDate;
35
import java.time.LocalDate;
36
import java.time.LocalDateTime;
36
import java.time.LocalDateTime;
37
import java.util.*;
37
import java.util.*;
Line 70... Line 70...
70
    @Autowired
70
    @Autowired
71
    CustomerRepository customerRepository;
71
    CustomerRepository customerRepository;
72
    @Autowired
72
    @Autowired
73
    PushNotificationRepository pushNotificationRepository;
73
    PushNotificationRepository pushNotificationRepository;
74
    @Autowired
74
    @Autowired
-
 
75
    NotificationScheduleRepository notificationScheduleRepository;
-
 
76
    @Autowired
75
    private Gson gson;
77
    private Gson gson;
76
    @Value("${prod}")
78
    @Value("${prod}")
77
    private boolean isProd;
79
    private boolean isProd;
78
 
80
 
79
    @Autowired
81
    @Autowired
Line 86... Line 88...
86
    private BotPenguinWhatsappService botPenguin;
88
    private BotPenguinWhatsappService botPenguin;
87
 
89
 
88
    @Override
90
    @Override
89
    public void sendNotification(SendNotificationModel sendNotificationModel) throws ProfitMandiBusinessException {
91
    public void sendNotification(SendNotificationModel sendNotificationModel) throws ProfitMandiBusinessException {
90
 
92
 
-
 
93
        NotificationTargetCriteria criteria = new NotificationTargetCriteria(
-
 
94
                sendNotificationModel.getUserIds(),
-
 
95
                sendNotificationModel.getFofoIds(),
-
 
96
                sendNotificationModel.getStateIds(),
-
 
97
                sendNotificationModel.getRegionIds());
-
 
98
 
-
 
99
        NotificationCampaign nc = buildAndPersistCampaign(sendNotificationModel, criteria);
-
 
100
 
-
 
101
        List<LocalDateTime> schedule = sendNotificationModel.getScheduledTimestamps();
-
 
102
        if (schedule != null && !schedule.isEmpty()) {
-
 
103
            LocalDateTime now = LocalDateTime.now();
-
 
104
            for (LocalDateTime ts : schedule) {
-
 
105
                NotificationSchedule ns = new NotificationSchedule();
-
 
106
                ns.setCampaignId(nc.getId());
-
 
107
                ns.setScheduledTimestamp(ts);
-
 
108
                ns.setStatus(ScheduleStatus.SCHEDULED);
-
 
109
                ns.setCreatedTimestamp(now);
-
 
110
                notificationScheduleRepository.persist(ns);
-
 
111
            }
-
 
112
            return;
-
 
113
        }
-
 
114
 
-
 
115
        Set<Integer> userIds = resolveTargetUserIds(criteria);
-
 
116
        if (userIds.isEmpty()) {
-
 
117
            LOGGER.info("Failed to send notification to any retailer with this model - {}", sendNotificationModel);
-
 
118
            return;
-
 
119
        }
-
 
120
        fanOutToUsers(nc.getId(), userIds, nc.getFormat());
-
 
121
    }
-
 
122
 
-
 
123
    private NotificationCampaign buildAndPersistCampaign(SendNotificationModel model, NotificationTargetCriteria criteria) {
91
        SimpleCampaignParams scp = new SimpleCampaignParams();
124
        SimpleCampaignParams scp = new SimpleCampaignParams();
92
        scp.setMessage(sendNotificationModel.getMessage());
125
        scp.setMessage(model.getMessage());
93
        scp.setTitle(sendNotificationModel.getTitle());
126
        scp.setTitle(model.getTitle());
94
        scp.setImageUrl(sendNotificationModel.getImageUrl());
127
        scp.setImageUrl(model.getImageUrl());
95
        scp.setType(sendNotificationModel.getType());
128
        scp.setType(model.getType());
96
        scp.setUrl(sendNotificationModel.getUrl());
129
        scp.setUrl(model.getUrl());
97
        scp.setShowImage(sendNotificationModel.getShowImage());
130
        scp.setShowImage(model.getShowImage());
98
        scp.setExpireTimestamp(sendNotificationModel.getExpiresat());
131
        scp.setExpireTimestamp(model.getExpiresat());
-
 
132
 
-
 
133
        // Pop-up / Story fields — silently no-op for PUSH since model getters return null.
-
 
134
        scp.setButtonLabel(model.getButtonLabel());
-
 
135
        scp.setShowCta(model.getShowCta());
-
 
136
        scp.setShowClose(model.getShowClose());
99
        SimpleCampaign sc = new SimpleCampaign(scp);
137
        scp.setShowOnPage(model.getShowOnPage());
-
 
138
        scp.setAutoCloseSeconds(model.getAutoCloseSeconds());
-
 
139
        scp.setFrequencyCap(model.getFrequencyCap());
-
 
140
        scp.setLiveFrom(model.getLiveFrom());
-
 
141
        scp.setLiveUntil(model.getLiveUntil());
-
 
142
        scp.setStoryCards(model.getStoryCards());
-
 
143
        scp.setPostInCategory(model.getPostInCategory());
-
 
144
        scp.setPlayDurationSeconds(model.getPlayDurationSeconds());
-
 
145
        scp.setExpiresAfterPolicy(model.getExpiresAfterPolicy());
100
        sc.setSimpleCampaignParams(scp);
146
        scp.setImages(model.getImages());
-
 
147
        scp.setLinkedEntities(model.getLinkedEntities());
101
 
148
 
102
        NotificationCampaign nc = new NotificationCampaign();
149
        NotificationCampaign nc = new NotificationCampaign();
103
        nc.setName(sendNotificationModel.getCampaignName());
150
        nc.setName(model.getCampaignName());
104
        nc.setImplementationType("SimpleCampaign");
151
        nc.setImplementationType("SimpleCampaign");
105
        nc.setImplementationParams(gson.toJson(scp));
152
        nc.setImplementationParams(gson.toJson(scp));
106
        nc.setMessageType(sendNotificationModel.getMessageType());
153
        nc.setMessageType(model.getMessageType());
107
        nc.setDocumentId(sendNotificationModel.getDocumentId());
154
        nc.setDocumentId(model.getDocumentId());
108
        nc.setCreatedTimestamp(LocalDateTime.now());
155
        nc.setCreatedTimestamp(LocalDateTime.now());
-
 
156
        nc.setTargetCriteria(gson.toJson(criteria));
-
 
157
        nc.setFormat(model.getFormat() != null ? model.getFormat() : NotificationFormat.PUSH);
-
 
158
        nc.setCategory(model.getCategory());
-
 
159
        nc.setCampaignGroupId(model.getCampaignGroupId());
109
        notificationCampaignRepository.persist(nc);
160
        notificationCampaignRepository.persist(nc);
-
 
161
        return nc;
-
 
162
    }
110
 
163
 
-
 
164
    @Override
-
 
165
    public Set<Integer> resolveTargetUserIds(NotificationTargetCriteria criteria) throws ProfitMandiBusinessException {
111
        Set<Integer> userIds = new HashSet<>();
166
        Set<Integer> userIds = new HashSet<>();
112
        if (sendNotificationModel.getUserIds() != null && sendNotificationModel.getUserIds().size() > 0) {
167
        if (criteria.getUserIds() != null && !criteria.getUserIds().isEmpty()) {
113
            userIds.addAll(sendNotificationModel.getUserIds());
168
            userIds.addAll(criteria.getUserIds());
114
        }
169
        }
115
 
-
 
116
        if (sendNotificationModel.getStateIds() != null && sendNotificationModel.getStateIds().size() > 0) {
170
        if (criteria.getStateIds() != null && !criteria.getStateIds().isEmpty()) {
117
            List<Integer> fofoIds = fofoStoreRepository.selectByWarehouseIds(sendNotificationModel.getStateIds()).stream()
171
            List<Integer> fofoIds = fofoStoreRepository.selectByWarehouseIds(criteria.getStateIds()).stream()
118
                    .map(x -> x.getId()).collect(Collectors.toList());
172
                    .map(x -> x.getId()).collect(Collectors.toList());
119
            if (fofoIds.size() > 0) {
173
            if (!fofoIds.isEmpty()) {
120
                userIds.addAll(userAccountRepository.selectUserIdsByRetailerIds(fofoIds));
174
                userIds.addAll(userAccountRepository.selectUserIdsByRetailerIds(fofoIds));
121
            }
175
            }
122
 
-
 
123
        }
176
        }
124
        if(sendNotificationModel.getRegionIds() != null && sendNotificationModel.getRegionIds().size() > 0) {
177
        if (criteria.getRegionIds() != null && !criteria.getRegionIds().isEmpty()) {
125
            if (sendNotificationModel.getRegionIds().contains(ALL_PARTNERS_REGION)) {
178
            if (criteria.getRegionIds().contains(ALL_PARTNERS_REGION)) {
126
                // "ALL partners" region: partner_region only holds a sentinel, so expand to every partner.
179
                // "ALL partners" region: partner_region only holds a sentinel, so expand to every partner.
127
                userIds.addAll(fofoStoreRepository.selectAllDtrUserIds());
180
                userIds.addAll(fofoStoreRepository.selectAllDtrUserIds());
128
            } else {
181
            } else {
129
                List<Integer> fofoIds = partnerRegionRepository.selectAllByRegionIds(sendNotificationModel.getRegionIds()).stream().map(x->x.getFofoId()).collect(Collectors.toList());
182
                List<Integer> fofoIds = partnerRegionRepository.selectAllByRegionIds(criteria.getRegionIds()).stream()
-
 
183
                        .map(x -> x.getFofoId()).collect(Collectors.toList());
130
                if(fofoIds.size()>0) {
184
                if (!fofoIds.isEmpty()) {
131
                    userIds.addAll(userAccountRepository.selectUserIdsByRetailerIds(fofoIds));
185
                    userIds.addAll(userAccountRepository.selectUserIdsByRetailerIds(fofoIds));
132
                }
186
                }
133
            }
187
            }
134
        }
188
        }
-
 
189
        return userIds;
-
 
190
    }
135
 
191
 
-
 
192
    private void fanOutToUsers(int campaignId, Set<Integer> userIds, NotificationFormat format) {
136
        if (userIds.size() > 0) {
193
        LocalDateTime now = LocalDateTime.now();
137
            for (Integer userId : userIds) {
194
        for (Integer userId : userIds) {
138
                UserCampaign uc = new UserCampaign();
195
            UserCampaign uc = new UserCampaign();
139
                uc.setCampaignId(nc.getId());
196
            uc.setCampaignId(campaignId);
140
                uc.setUserId(userId);
197
            uc.setUserId(userId);
141
                uc.setPushTimestamp(LocalDateTime.now());
198
            uc.setPushTimestamp(now);
142
                userCampaignRepository.persist(uc);
199
            userCampaignRepository.persist(uc);
143
            }
200
        }
-
 
201
        // FCM/APNs delivery only for PUSH. Pop-ups and Stories are pull-based (mobile fetches on app open),
-
 
202
        // so we still write user_campaign rows above but skip pushnotifications.
-
 
203
        if (format == null || format == NotificationFormat.PUSH) {
144
            List<Device> devices = deviceRepository.selectByUserIdAndModifiedTimestamp(new ArrayList<>(userIds),
204
            List<Device> devices = deviceRepository.selectByUserIdAndModifiedTimestamp(new ArrayList<>(userIds),
145
                    LocalDateTime.now().minusMonths(1), LocalDateTime.now());
205
                    now.minusMonths(1), now);
146
            pushNotification(nc.getId(), devices);
206
            pushNotification(campaignId, devices);
147
        } else {
-
 
148
            LOGGER.info("Failed to send notification to any retailer with this model - {}", sendNotificationModel);
-
 
149
        }
207
        }
-
 
208
    }
150
 
209
 
-
 
210
    @Override
-
 
211
    public void dispatchScheduled(int scheduleId) throws ProfitMandiBusinessException {
-
 
212
        NotificationSchedule ns = notificationScheduleRepository.selectById(scheduleId);
-
 
213
        if (ns == null) {
-
 
214
            LOGGER.warn("dispatchScheduled: schedule id {} not found", scheduleId);
-
 
215
            return;
-
 
216
        }
-
 
217
        if (ns.getStatus() != ScheduleStatus.SCHEDULED) {
-
 
218
            LOGGER.debug("dispatchScheduled: skip id {} in status {}", scheduleId, ns.getStatus());
-
 
219
            return;
-
 
220
        }
-
 
221
        // Atomic claim: only one worker transitions SCHEDULED -> DISPATCHED; others get 0 rows.
-
 
222
        int claimed = notificationScheduleRepository.claimForDispatch(scheduleId, LocalDateTime.now());
-
 
223
        if (claimed == 0) {
-
 
224
            LOGGER.debug("dispatchScheduled: id {} already claimed by another worker", scheduleId);
-
 
225
            return;
-
 
226
        }
-
 
227
        try {
-
 
228
            NotificationCampaign campaign = notificationCampaignRepository.selectById(ns.getCampaignId());
-
 
229
            if (campaign == null) {
-
 
230
                LOGGER.error("dispatchScheduled: campaign {} missing for schedule {}", ns.getCampaignId(), scheduleId);
-
 
231
                notificationScheduleRepository.markFailed(scheduleId);
-
 
232
                return;
-
 
233
            }
-
 
234
            NotificationTargetCriteria criteria = campaign.getTargetCriteria() == null
-
 
235
                    ? new NotificationTargetCriteria()
-
 
236
                    : gson.fromJson(campaign.getTargetCriteria(), NotificationTargetCriteria.class);
-
 
237
            Set<Integer> userIds = resolveTargetUserIds(criteria);
-
 
238
            if (userIds.isEmpty()) {
-
 
239
                LOGGER.info("dispatchScheduled: no users resolved for schedule {} (campaign {})",
-
 
240
                        scheduleId, campaign.getId());
-
 
241
                return;
-
 
242
            }
-
 
243
            fanOutToUsers(campaign.getId(), userIds, campaign.getFormat());
-
 
244
        } catch (RuntimeException e) {
-
 
245
            // Swallow: markFailed must commit, and cron shouldn't abort the batch on one bad row.
-
 
246
            LOGGER.error("dispatchScheduled: fan-out failed for schedule {}", scheduleId, e);
-
 
247
            notificationScheduleRepository.markFailed(scheduleId);
-
 
248
        }
-
 
249
    }
-
 
250
 
-
 
251
    @Override
-
 
252
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
-
 
253
    public int dispatchDueScheduled(int batchSize) throws ProfitMandiBusinessException {
-
 
254
        List<Integer> due = notificationScheduleRepository.selectDueScheduledIds(LocalDateTime.now(), batchSize);
-
 
255
        if (due.isEmpty()) {
-
 
256
            return 0;
-
 
257
        }
-
 
258
        LOGGER.info("dispatchDueScheduled: dispatching {} scheduled notification(s)", due.size());
-
 
259
        for (Integer id : due) {
-
 
260
            dispatchScheduled(id);
-
 
261
        }
-
 
262
        return due.size();
-
 
263
    }
-
 
264
 
-
 
265
    @Override
-
 
266
    public boolean cancelSchedule(int scheduleId) {
-
 
267
        return notificationScheduleRepository.cancelIfScheduled(scheduleId) == 1;
-
 
268
    }
-
 
269
 
-
 
270
    @Override
-
 
271
    public int resolveAudienceSize(NotificationTargetCriteria criteria) throws ProfitMandiBusinessException {
-
 
272
        if (criteria == null) return 0;
-
 
273
        return resolveTargetUserIds(criteria).size();
-
 
274
    }
-
 
275
 
-
 
276
    @Override
-
 
277
    public int addScheduleForCampaign(int campaignId, LocalDateTime scheduledAt) throws ProfitMandiBusinessException {
-
 
278
        NotificationCampaign nc = notificationCampaignRepository.selectById(campaignId);
-
 
279
        if (nc == null) {
-
 
280
            throw new ProfitMandiBusinessException("Campaign not found: ", campaignId, "- campaignId");
-
 
281
        }
-
 
282
        NotificationSchedule ns = new NotificationSchedule();
-
 
283
        ns.setCampaignId(campaignId);
-
 
284
        ns.setScheduledTimestamp(scheduledAt);
-
 
285
        ns.setStatus(ScheduleStatus.SCHEDULED);
-
 
286
        ns.setCreatedTimestamp(LocalDateTime.now());
-
 
287
        notificationScheduleRepository.persist(ns);
-
 
288
        LOGGER.info("addScheduleForCampaign: campaignId={} scheduledAt={} newScheduleId={}",
-
 
289
                campaignId, scheduledAt, ns.getId());
-
 
290
        return ns.getId();
151
    }
291
    }
152
 
292
 
153
    @Override
293
    @Override
154
    public void sendNotificationToAll(SendNotificationModel sendNotificationModel) throws ProfitMandiBusinessException {
294
    public void sendNotificationToAll(SendNotificationModel sendNotificationModel) throws ProfitMandiBusinessException {
155
        sendNotificationModel.setUserIds(fofoStoreRepository.selectAllDtrUserIds());
295
        sendNotificationModel.setUserIds(fofoStoreRepository.selectAllDtrUserIds());