addresses

This commit is contained in:
Mireya Serrano 2026-05-24 18:44:34 +02:00
parent 877cb594b2
commit c3c5b22b12
3 changed files with 152 additions and 80 deletions

View File

@ -1,6 +1,7 @@
package com.teterialosjuanjos.tfg.sync_service.integration.dolibarr;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.*;
import java.util.Collections;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.exception.DolibarrApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
@ -167,10 +168,28 @@ public class DolibarrClient {
.retrieve()
.body(Integer.class);
return restClient.get()
DolibarrThirdpartyDto created = restClient.get()
.uri("/thirdparties/{id}", newId)
.retrieve()
.body(DolibarrThirdpartyDto.class);
// Dolibarr returns the existing thirdparty ID when name already exists (name uniqueness constraint).
// If the returned thirdparty belongs to a different customer, retry with email suffix to disambiguate.
if (created != null && !email.equalsIgnoreCase(created.email())) {
log.warn("Dolibarr name collision for '{}', retrying with email suffix", name);
DolibarrThirdpartyDto disambiguated = new DolibarrThirdpartyDto(
null, name + " (" + email + ")", email, 1, null, address, zip, city, null, null);
Integer newId2 = restClient.post()
.uri("/thirdparties")
.body(disambiguated)
.retrieve()
.body(Integer.class);
return restClient.get()
.uri("/thirdparties/{id}", newId2)
.retrieve()
.body(DolibarrThirdpartyDto.class);
}
return created;
}
public DolibarrAddressDto createThirdpartyAddress(Integer socid, DolibarrAddressDto dto) {
@ -229,6 +248,62 @@ public class DolibarrClient {
.toBodilessEntity();
}
// ── Contacts (llx_socpeople) ──────────────────────────────────────────
public List<DolibarrContactDto> getThirdpartyContacts(Integer socid) {
try {
List<DolibarrContactDto> contacts = restClient.get()
.uri(u -> u.path("/contacts")
.queryParam("sqlfilters", "(t.fk_soc:=:" + socid + ")")
.queryParam("limit", 100)
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<DolibarrContactDto>>() {});
return contacts != null ? contacts : Collections.emptyList();
} catch (DolibarrApiException e) {
if (e.getStatusCode().value() == 404) return Collections.emptyList();
throw e;
}
}
public DolibarrContactDto createContact(DolibarrContactDto dto) {
log.debug("Creating Dolibarr contact fk_soc={}", dto.fkSoc());
Integer newId = restClient.post()
.uri("/contacts")
.body(dto)
.retrieve()
.body(Integer.class);
return restClient.get()
.uri("/contacts/{id}", newId)
.retrieve()
.body(DolibarrContactDto.class);
}
public void updateContact(Integer id, DolibarrContactDto dto) {
log.debug("Updating Dolibarr contact id={}", id);
restClient.put()
.uri("/contacts/{id}", id)
.body(dto)
.retrieve()
.toBodilessEntity();
}
public void deleteContact(Integer id) {
log.debug("Deleting Dolibarr contact id={}", id);
restClient.delete()
.uri("/contacts/{id}", id)
.retrieve()
.toBodilessEntity();
}
public void linkContactToInvoice(Integer invoiceId, Integer contactId, String type) {
log.debug("Linking contact {} to invoice {} as {}", contactId, invoiceId, type);
restClient.post()
.uri("/invoices/{id}/contact/{contactId}/{type}", invoiceId, contactId, type)
.retrieve()
.toBodilessEntity();
}
// ── Orders ────────────────────────────────────────────────────────────
/**

View File

@ -0,0 +1,19 @@
package com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record DolibarrContactDto(
Integer id,
@JsonProperty("fk_soc") Integer fkSoc,
String firstname,
String lastname,
String address,
String zip,
String town,
@JsonProperty("phone_pro") String phonePro,
String email
) {}

View File

@ -2,7 +2,7 @@ package com.teterialosjuanjos.tfg.sync_service.sync;
import com.teterialosjuanjos.tfg.sync_service.config.IntegrationProperties;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.DolibarrClient;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrAddressDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrContactDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrInvoiceDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrOrderDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrThirdpartyDto;
@ -134,9 +134,11 @@ public class OrderSyncService {
DolibarrOrderDto createdOrder = dolibarrClient.createOrder(orderDto);
// 3. Create linked invoice with lines copied from the order and billing address
Integer fkAddress = resolveInvoiceAddress(psOrder, customer.id());
DolibarrInvoiceDto invoice = dolibarrClient.createInvoiceFromOrder(createdOrder, fkAddress);
// 3. Create linked invoice with lines copied from the order
DolibarrInvoiceDto invoice = dolibarrClient.createInvoiceFromOrder(createdOrder, null);
// 4a. Link billing contact to invoice (populates "Contactos/Direcciones" on the invoice)
linkInvoiceBillingContact(psOrder, customer.id(), invoice.id());
// 4. Persist mapping to prevent re-import
orderMappingRepository.save(OrderMapping.builder()
@ -192,125 +194,106 @@ public class OrderSyncService {
DolibarrThirdpartyDto thirdparty = dolibarrClient.getOrCreateThirdparty(email, name, address, city, zip);
// Sync additional addresses if customer exists and is not a guest
// Sync additional addresses as Dolibarr contacts (llx_socpeople → "Contactos/Direcciones" tab)
if (!isGuest && psCustomer != null) {
syncCustomerAddresses(Integer.parseInt(psOrder.idCustomer()), thirdparty.id());
syncCustomerContacts(Integer.parseInt(psOrder.idCustomer()), thirdparty.id());
}
return thirdparty;
}
private void syncCustomerAddresses(Integer psCustomerId, Integer dolibarrThirdpartyId) {
private void syncCustomerContacts(Integer psCustomerId, Integer dolibarrThirdpartyId) {
try {
// 1. Obtener direcciones de PrestaShop
List<PrestashopAddressDto> psAddresses = prestashopClient.getCustomerAddresses(psCustomerId);
// 2. Obtener direcciones existentes en Dolibarr
List<DolibarrAddressDto> dolibarrAddresses = dolibarrClient.getThirdpartyAddresses(dolibarrThirdpartyId);
List<DolibarrContactDto> dolibarrContacts = dolibarrClient.getThirdpartyContacts(dolibarrThirdpartyId);
PrestashopCustomerDto psCustomer = prestashopClient.getCustomer(psCustomerId);
String defaultAddressId = psCustomer != null ? psCustomer.idDefaultAddress() : null;
// 3. Procesar cada dirección de PrestaShop
for (PrestashopAddressDto psAddr : psAddresses) {
// Ignorar la dirección por defecto (ya está en thirdparty.address/city/zip)
if (psAddr.id() != null && psAddr.id().toString().equals(defaultAddressId)) {
continue;
}
// Default address is already on the thirdparty itself (address/zip/city fields)
if (psAddr.id() != null && psAddr.id().toString().equals(defaultAddressId)) continue;
String formattedAddress = formatAddress(psAddr.address1(), psAddr.address2());
String formattedCity = psAddr.city();
String formattedAddr = formatAddress(psAddr.address1(), psAddr.address2());
// Buscar si existe una dirección con los mismos datos en Dolibarr
DolibarrAddressDto existingAddr = dolibarrAddresses.stream()
.filter(a -> a.address() != null && a.address().equals(formattedAddress)
&& a.city() != null && a.city().equals(formattedCity))
DolibarrContactDto existing = dolibarrContacts.stream()
.filter(c -> c.address() != null && c.address().equals(formattedAddr)
&& c.town() != null && c.town().equals(psAddr.city()))
.findFirst()
.orElse(null);
if (existingAddr != null) {
// ACTUALIZAR: La dirección existe pero podría tener cambios en zip, phone, etc.
DolibarrAddressDto updatedAddr = new DolibarrAddressDto(
existingAddr.id(),
dolibarrThirdpartyId,
formatPersonName(psAddr.firstname(), psAddr.lastname()),
formattedAddress,
psAddr.postcode(),
formattedCity,
null,
psAddr.phone(),
0
);
dolibarrClient.updateThirdpartyAddress(existingAddr.id(), updatedAddr);
log.debug("Updated PS address {} in Dolibarr", psAddr.id());
if (existing != null) {
dolibarrClient.updateContact(existing.id(), new DolibarrContactDto(
existing.id(), dolibarrThirdpartyId,
psAddr.firstname(), psAddr.lastname(),
formattedAddr, psAddr.postcode(), psAddr.city(),
psAddr.phone(), null));
log.debug("Updated PS address {} as Dolibarr contact", psAddr.id());
} else {
// CREAR: Dirección nueva en PrestaShop
DolibarrAddressDto newAddr = new DolibarrAddressDto(
null,
dolibarrThirdpartyId,
formatPersonName(psAddr.firstname(), psAddr.lastname()),
formattedAddress,
psAddr.postcode(),
formattedCity,
null,
psAddr.phone(),
0
);
dolibarrClient.createThirdpartyAddress(dolibarrThirdpartyId, newAddr);
log.debug("Created PS address {} in Dolibarr", psAddr.id());
dolibarrClient.createContact(new DolibarrContactDto(
null, dolibarrThirdpartyId,
psAddr.firstname(), psAddr.lastname(),
formattedAddr, psAddr.postcode(), psAddr.city(),
psAddr.phone(), null));
log.debug("Created PS address {} as Dolibarr contact", psAddr.id());
}
}
// 4. ELIMINAR direcciones que ya no existen en PrestaShop
Set<String> psAddressStrings = psAddresses.stream()
// Remove contacts that no longer exist in PrestaShop
Set<String> psKeys = psAddresses.stream()
.filter(a -> !(a.id() != null && a.id().toString().equals(defaultAddressId)))
.map(a -> formatAddress(a.address1(), a.address2()) + "|" + a.city())
.collect(Collectors.toSet());
for (DolibarrAddressDto dolibarrAddr : dolibarrAddresses) {
String dolibarrAddressStr = dolibarrAddr.address() + "|" + dolibarrAddr.city();
if (!psAddressStrings.contains(dolibarrAddressStr)) {
dolibarrClient.deleteThirdpartyAddress(dolibarrAddr.id());
log.debug("Deleted orphaned address {} from Dolibarr", dolibarrAddr.id());
for (DolibarrContactDto c : dolibarrContacts) {
if (!psKeys.contains(c.address() + "|" + c.town())) {
dolibarrClient.deleteContact(c.id());
log.debug("Deleted orphaned Dolibarr contact {}", c.id());
}
}
} catch (Exception e) {
log.warn("Failed to sync addresses for PS customer {}: {}", psCustomerId, e.getMessage());
log.warn("Failed to sync contacts for PS customer {}: {}", psCustomerId, e.getMessage());
}
}
private Integer resolveInvoiceAddress(PrestashopOrderDto psOrder, Integer dolibarrThirdpartyId) {
private void linkInvoiceBillingContact(PrestashopOrderDto psOrder, Integer dolibarrThirdpartyId, Integer invoiceId) {
String addrId = psOrder.idAddressInvoice();
if (addrId == null || addrId.isBlank() || "0".equals(addrId)) return null;
if (addrId == null || addrId.isBlank() || "0".equals(addrId)) return;
// If billing addr = customer default addr → already on thirdparty, no fk_address needed
boolean isGuest = psOrder.idCustomer() == null
|| psOrder.idCustomer().isBlank()
|| "0".equals(psOrder.idCustomer());
// If billing addr = customer default addr → thirdparty main address, no contact to link
if (!isGuest) {
try {
PrestashopCustomerDto c = prestashopClient.getCustomer(Integer.parseInt(psOrder.idCustomer()));
if (c != null && addrId.equals(c.idDefaultAddress())) return null;
if (c != null && addrId.equals(c.idDefaultAddress())) return;
} catch (Exception e) {
log.warn("Could not fetch PS customer for invoice address resolution: {}", e.getMessage());
log.warn("Could not fetch PS customer for billing contact resolution: {}", e.getMessage());
return;
}
}
// Find matching Dolibarr socaddress by content
try {
PrestashopAddressDto billing = prestashopClient.getAddress(Integer.parseInt(addrId));
if (billing == null) return null;
if (billing == null) return;
String formatted = formatAddress(billing.address1(), billing.address2());
return dolibarrClient.getThirdpartyAddresses(dolibarrThirdpartyId).stream()
.filter(a -> a.address() != null && a.address().equals(formatted)
&& a.city() != null && a.city().equals(billing.city()))
.map(DolibarrAddressDto::id)
dolibarrClient.getThirdpartyContacts(dolibarrThirdpartyId).stream()
.filter(c -> c.address() != null && c.address().equals(formatted)
&& c.town() != null && c.town().equals(billing.city()))
.findFirst()
.orElse(null);
.ifPresent(contact -> {
try {
dolibarrClient.linkContactToInvoice(invoiceId, contact.id(), "BILLING");
log.debug("Linked contact {} to invoice {} as BILLING", contact.id(), invoiceId);
} catch (Exception ex) {
log.warn("Could not link billing contact {} to invoice {}: {}", contact.id(), invoiceId, ex.getMessage());
}
});
} catch (Exception e) {
log.warn("Could not resolve billing address for PS order {}: {}", psOrder.id(), e.getMessage());
return null;
log.warn("Could not resolve billing contact for PS order {}: {}", psOrder.id(), e.getMessage());
}
}
@ -320,11 +303,6 @@ public class OrderSyncService {
return addr1 + " " + addr2;
}
private String formatPersonName(String firstname, String lastname) {
String full = (firstname != null ? firstname : "") + " " + (lastname != null ? lastname : "");
return full.trim();
}
/**
* Maps PrestaShop order rows to Dolibarr order lines.
* Converts tax-included unit price to excl-tax using the configured {@code defaultTaxRate}.