This commit is contained in:
Codex
2026-04-05 21:37:37 +05:00
parent d34b760fb5
commit 43579d67b5
3 changed files with 193 additions and 18 deletions
@@ -1,22 +1,127 @@
package kz.konturai.parser.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "facebook.leads.call-center")
@Data
public class FacebookLeadCallCenterProperties {
private boolean enabled = false;
private String baseUrl = "http://localhost:8080";
private String customerPath = "/proxy/customer/customers";
private String interactionPath = "/proxy/interaction/interactions";
private String authLoginPath = "/proxy/auth/auth/login";
private String authUsername = "admin";
private String authPassword = "admin123";
private String actorUser = "facebook-lead-bot";
private String actorRole = "admin";
private String interactionChannel = "webchat";
private String interactionQueueId = "q_main";
private int timeoutMs = 30000;
private String subjectPrefix = "[Facebook]";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getCustomerPath() {
return customerPath;
}
public void setCustomerPath(String customerPath) {
this.customerPath = customerPath;
}
public String getInteractionPath() {
return interactionPath;
}
public void setInteractionPath(String interactionPath) {
this.interactionPath = interactionPath;
}
public String getAuthLoginPath() {
return authLoginPath;
}
public void setAuthLoginPath(String authLoginPath) {
this.authLoginPath = authLoginPath;
}
public String getAuthUsername() {
return authUsername;
}
public void setAuthUsername(String authUsername) {
this.authUsername = authUsername;
}
public String getAuthPassword() {
return authPassword;
}
public void setAuthPassword(String authPassword) {
this.authPassword = authPassword;
}
public String getActorUser() {
return actorUser;
}
public void setActorUser(String actorUser) {
this.actorUser = actorUser;
}
public String getActorRole() {
return actorRole;
}
public void setActorRole(String actorRole) {
this.actorRole = actorRole;
}
public String getInteractionChannel() {
return interactionChannel;
}
public void setInteractionChannel(String interactionChannel) {
this.interactionChannel = interactionChannel;
}
public String getInteractionQueueId() {
return interactionQueueId;
}
public void setInteractionQueueId(String interactionQueueId) {
this.interactionQueueId = interactionQueueId;
}
public int getTimeoutMs() {
return timeoutMs;
}
public void setTimeoutMs(int timeoutMs) {
this.timeoutMs = timeoutMs;
}
public String getSubjectPrefix() {
return subjectPrefix;
}
public void setSubjectPrefix(String subjectPrefix) {
this.subjectPrefix = subjectPrefix;
}
}
@@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import kz.konturai.parser.config.FacebookLeadCallCenterProperties;
import kz.konturai.parser.model.FacebookLead;
import kz.konturai.parser.repository.FacebookLeadRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
@@ -18,16 +18,24 @@ import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
@Slf4j
public class FacebookLeadCallCenterSyncService {
private static final List<String> RETRYABLE_SYNC_STATUSES = List.of("PENDING", "FAILED");
private static final Logger log = LoggerFactory.getLogger(FacebookLeadCallCenterSyncService.class);
private final FacebookLeadCallCenterProperties properties;
private final FacebookLeadRepository facebookLeadRepository;
private final WebClient.Builder webClientBuilder;
public FacebookLeadCallCenterSyncService(
FacebookLeadCallCenterProperties properties,
FacebookLeadRepository facebookLeadRepository,
WebClient.Builder webClientBuilder) {
this.properties = properties;
this.facebookLeadRepository = facebookLeadRepository;
this.webClientBuilder = webClientBuilder;
}
public void syncPendingLeads() {
if (!properties.isEnabled()) {
return;
@@ -64,8 +72,10 @@ public class FacebookLeadCallCenterSyncService {
FacebookLead managedLead = loadManagedLead(lead);
try {
Map<String, String> authHeaders = resolveAuthHeaders();
if (!StringUtils.hasText(managedLead.getCallCenterCustomerId())) {
String customerId = createCustomer(managedLead);
String customerId = createCustomer(managedLead, authHeaders);
managedLead.setCallCenterCustomerId(customerId);
managedLead.setCallCenterSyncStatus("CUSTOMER_CREATED");
managedLead.setCallCenterSyncError(null);
@@ -75,7 +85,7 @@ public class FacebookLeadCallCenterSyncService {
}
if (!StringUtils.hasText(managedLead.getCallCenterInteractionId())) {
String interactionId = createInteraction(managedLead);
String interactionId = createInteraction(managedLead, authHeaders);
managedLead.setCallCenterInteractionId(interactionId);
log.info("Facebook lead {} linked to call-center interaction {}",
managedLead.getExternalCommentId(), interactionId);
@@ -107,7 +117,50 @@ public class FacebookLeadCallCenterSyncService {
return lead;
}
private String createCustomer(FacebookLead lead) {
private Map<String, String> resolveAuthHeaders() {
if (StringUtils.hasText(properties.getAuthUsername()) && StringUtils.hasText(properties.getAuthPassword())) {
try {
String accessToken = loginAndGetAccessToken();
return Map.of("Authorization", "Bearer " + accessToken);
} catch (Exception e) {
log.warn("Call-center bearer auth failed. Falling back to legacy headers. message={}",
shorten(e.getMessage()));
}
}
Map<String, String> headers = new LinkedHashMap<>();
headers.put("X-User", properties.getActorUser());
headers.put("X-Role", properties.getActorRole());
return headers;
}
private String loginAndGetAccessToken() {
Map<String, Object> payload = Map.of(
"username", properties.getAuthUsername(),
"password", properties.getAuthPassword()
);
try {
JsonNode response = callCenterClient().post()
.uri(properties.getAuthLoginPath())
.bodyValue(payload)
.retrieve()
.bodyToMono(JsonNode.class)
.block(Duration.ofMillis(properties.getTimeoutMs()));
String accessToken = response != null ? response.path("access_token").asText(null) : null;
if (!StringUtils.hasText(accessToken)) {
throw new IllegalStateException("Call-center auth response does not contain access_token");
}
return accessToken;
} catch (WebClientResponseException e) {
log.error("Call-center auth login failed. status={}, body={}",
e.getStatusCode().value(), e.getResponseBodyAsString());
throw e;
}
}
private String createCustomer(FacebookLead lead, Map<String, String> authHeaders) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("display_name", customerDisplayName(lead));
payload.put("phones", List.of());
@@ -115,10 +168,11 @@ public class FacebookLeadCallCenterSyncService {
payload.put("tags", List.of("facebook", "hot-lead"));
try {
JsonNode response = callCenterClient().post()
.uri(properties.getCustomerPath())
.header("X-User", properties.getActorUser())
.header("X-Role", properties.getActorRole())
WebClient.RequestBodySpec request = callCenterClient().post()
.uri(properties.getCustomerPath());
request = applyHeaders(request, authHeaders);
JsonNode response = request
.bodyValue(payload)
.retrieve()
.bodyToMono(JsonNode.class)
@@ -136,7 +190,7 @@ public class FacebookLeadCallCenterSyncService {
}
}
private String createInteraction(FacebookLead lead) {
private String createInteraction(FacebookLead lead, Map<String, String> authHeaders) {
if (!StringUtils.hasText(lead.getCallCenterCustomerId())) {
throw new IllegalStateException("Call-center customer must be created before interaction");
}
@@ -150,10 +204,11 @@ public class FacebookLeadCallCenterSyncService {
);
try {
JsonNode response = callCenterClient().post()
.uri(properties.getInteractionPath())
.header("X-User", properties.getActorUser())
.header("X-Role", properties.getActorRole())
WebClient.RequestBodySpec request = callCenterClient().post()
.uri(properties.getInteractionPath());
request = applyHeaders(request, authHeaders);
JsonNode response = request
.bodyValue(payload)
.retrieve()
.bodyToMono(JsonNode.class)
@@ -177,6 +232,18 @@ public class FacebookLeadCallCenterSyncService {
.build();
}
private WebClient.RequestBodySpec applyHeaders(
WebClient.RequestBodySpec request,
Map<String, String> headers) {
WebClient.RequestBodySpec current = request;
for (Map.Entry<String, String> header : headers.entrySet()) {
if (StringUtils.hasText(header.getValue())) {
current = current.header(header.getKey(), header.getValue());
}
}
return current;
}
private String customerDisplayName(FacebookLead lead) {
String authorName = StringUtils.hasText(lead.getAuthorName())
? lead.getAuthorName().trim()
@@ -155,6 +155,9 @@ facebook.leads.call-center.enabled=${FACEBOOK_LEADS_CALL_CENTER_ENABLED:true}
facebook.leads.call-center.base-url=${FACEBOOK_LEADS_CALL_CENTER_BASE_URL:http://localhost:8080}
facebook.leads.call-center.customer-path=${FACEBOOK_LEADS_CALL_CENTER_CUSTOMER_PATH:/proxy/customer/customers}
facebook.leads.call-center.interaction-path=${FACEBOOK_LEADS_CALL_CENTER_INTERACTION_PATH:/proxy/interaction/interactions}
facebook.leads.call-center.auth-login-path=${FACEBOOK_LEADS_CALL_CENTER_AUTH_LOGIN_PATH:/proxy/auth/auth/login}
facebook.leads.call-center.auth-username=${FACEBOOK_LEADS_CALL_CENTER_AUTH_USERNAME:admin}
facebook.leads.call-center.auth-password=${FACEBOOK_LEADS_CALL_CENTER_AUTH_PASSWORD:admin123}
facebook.leads.call-center.actor-user=${FACEBOOK_LEADS_CALL_CENTER_ACTOR_USER:facebook-lead-bot}
facebook.leads.call-center.actor-role=${FACEBOOK_LEADS_CALL_CENTER_ACTOR_ROLE:admin}
facebook.leads.call-center.interaction-channel=${FACEBOOK_LEADS_CALL_CENTER_INTERACTION_CHANNEL:webchat}