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; package kz.konturai.parser.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@ConfigurationProperties(prefix = "facebook.leads.call-center") @ConfigurationProperties(prefix = "facebook.leads.call-center")
@Data
public class FacebookLeadCallCenterProperties { public class FacebookLeadCallCenterProperties {
private boolean enabled = false; private boolean enabled = false;
private String baseUrl = "http://localhost:8080"; private String baseUrl = "http://localhost:8080";
private String customerPath = "/proxy/customer/customers"; private String customerPath = "/proxy/customer/customers";
private String interactionPath = "/proxy/interaction/interactions"; 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 actorUser = "facebook-lead-bot";
private String actorRole = "admin"; private String actorRole = "admin";
private String interactionChannel = "webchat"; private String interactionChannel = "webchat";
private String interactionQueueId = "q_main"; private String interactionQueueId = "q_main";
private int timeoutMs = 30000; private int timeoutMs = 30000;
private String subjectPrefix = "[Facebook]"; 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.config.FacebookLeadCallCenterProperties;
import kz.konturai.parser.model.FacebookLead; import kz.konturai.parser.model.FacebookLead;
import kz.konturai.parser.repository.FacebookLeadRepository; import kz.konturai.parser.repository.FacebookLeadRepository;
import lombok.RequiredArgsConstructor; import org.slf4j.Logger;
import lombok.extern.slf4j.Slf4j; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
@@ -18,16 +18,24 @@ import java.util.List;
import java.util.Map; import java.util.Map;
@Service @Service
@RequiredArgsConstructor
@Slf4j
public class FacebookLeadCallCenterSyncService { public class FacebookLeadCallCenterSyncService {
private static final List<String> RETRYABLE_SYNC_STATUSES = List.of("PENDING", "FAILED"); 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 FacebookLeadCallCenterProperties properties;
private final FacebookLeadRepository facebookLeadRepository; private final FacebookLeadRepository facebookLeadRepository;
private final WebClient.Builder webClientBuilder; 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() { public void syncPendingLeads() {
if (!properties.isEnabled()) { if (!properties.isEnabled()) {
return; return;
@@ -64,8 +72,10 @@ public class FacebookLeadCallCenterSyncService {
FacebookLead managedLead = loadManagedLead(lead); FacebookLead managedLead = loadManagedLead(lead);
try { try {
Map<String, String> authHeaders = resolveAuthHeaders();
if (!StringUtils.hasText(managedLead.getCallCenterCustomerId())) { if (!StringUtils.hasText(managedLead.getCallCenterCustomerId())) {
String customerId = createCustomer(managedLead); String customerId = createCustomer(managedLead, authHeaders);
managedLead.setCallCenterCustomerId(customerId); managedLead.setCallCenterCustomerId(customerId);
managedLead.setCallCenterSyncStatus("CUSTOMER_CREATED"); managedLead.setCallCenterSyncStatus("CUSTOMER_CREATED");
managedLead.setCallCenterSyncError(null); managedLead.setCallCenterSyncError(null);
@@ -75,7 +85,7 @@ public class FacebookLeadCallCenterSyncService {
} }
if (!StringUtils.hasText(managedLead.getCallCenterInteractionId())) { if (!StringUtils.hasText(managedLead.getCallCenterInteractionId())) {
String interactionId = createInteraction(managedLead); String interactionId = createInteraction(managedLead, authHeaders);
managedLead.setCallCenterInteractionId(interactionId); managedLead.setCallCenterInteractionId(interactionId);
log.info("Facebook lead {} linked to call-center interaction {}", log.info("Facebook lead {} linked to call-center interaction {}",
managedLead.getExternalCommentId(), interactionId); managedLead.getExternalCommentId(), interactionId);
@@ -107,7 +117,50 @@ public class FacebookLeadCallCenterSyncService {
return lead; 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<>(); Map<String, Object> payload = new LinkedHashMap<>();
payload.put("display_name", customerDisplayName(lead)); payload.put("display_name", customerDisplayName(lead));
payload.put("phones", List.of()); payload.put("phones", List.of());
@@ -115,10 +168,11 @@ public class FacebookLeadCallCenterSyncService {
payload.put("tags", List.of("facebook", "hot-lead")); payload.put("tags", List.of("facebook", "hot-lead"));
try { try {
JsonNode response = callCenterClient().post() WebClient.RequestBodySpec request = callCenterClient().post()
.uri(properties.getCustomerPath()) .uri(properties.getCustomerPath());
.header("X-User", properties.getActorUser()) request = applyHeaders(request, authHeaders);
.header("X-Role", properties.getActorRole())
JsonNode response = request
.bodyValue(payload) .bodyValue(payload)
.retrieve() .retrieve()
.bodyToMono(JsonNode.class) .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())) { if (!StringUtils.hasText(lead.getCallCenterCustomerId())) {
throw new IllegalStateException("Call-center customer must be created before interaction"); throw new IllegalStateException("Call-center customer must be created before interaction");
} }
@@ -150,10 +204,11 @@ public class FacebookLeadCallCenterSyncService {
); );
try { try {
JsonNode response = callCenterClient().post() WebClient.RequestBodySpec request = callCenterClient().post()
.uri(properties.getInteractionPath()) .uri(properties.getInteractionPath());
.header("X-User", properties.getActorUser()) request = applyHeaders(request, authHeaders);
.header("X-Role", properties.getActorRole())
JsonNode response = request
.bodyValue(payload) .bodyValue(payload)
.retrieve() .retrieve()
.bodyToMono(JsonNode.class) .bodyToMono(JsonNode.class)
@@ -177,6 +232,18 @@ public class FacebookLeadCallCenterSyncService {
.build(); .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) { private String customerDisplayName(FacebookLead lead) {
String authorName = StringUtils.hasText(lead.getAuthorName()) String authorName = StringUtils.hasText(lead.getAuthorName())
? lead.getAuthorName().trim() ? 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.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.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.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-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.actor-role=${FACEBOOK_LEADS_CALL_CENTER_ACTOR_ROLE:admin}
facebook.leads.call-center.interaction-channel=${FACEBOOK_LEADS_CALL_CENTER_INTERACTION_CHANNEL:webchat} facebook.leads.call-center.interaction-channel=${FACEBOOK_LEADS_CALL_CENTER_INTERACTION_CHANNEL:webchat}