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; package com.teterialosjuanjos.tfg.sync_service.integration.dolibarr;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.*; import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.*;
import java.util.Collections;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.exception.DolibarrApiException; import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.exception.DolibarrApiException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -167,10 +168,28 @@ public class DolibarrClient {
.retrieve() .retrieve()
.body(Integer.class); .body(Integer.class);
return restClient.get() DolibarrThirdpartyDto created = restClient.get()
.uri("/thirdparties/{id}", newId) .uri("/thirdparties/{id}", newId)
.retrieve() .retrieve()
.body(DolibarrThirdpartyDto.class); .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) { public DolibarrAddressDto createThirdpartyAddress(Integer socid, DolibarrAddressDto dto) {
@ -229,6 +248,62 @@ public class DolibarrClient {
.toBodilessEntity(); .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 ──────────────────────────────────────────────────────────── // ── 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.config.IntegrationProperties;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.DolibarrClient; 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.DolibarrInvoiceDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrOrderDto; import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrOrderDto;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrThirdpartyDto; import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrThirdpartyDto;
@ -134,9 +134,11 @@ public class OrderSyncService {
DolibarrOrderDto createdOrder = dolibarrClient.createOrder(orderDto); DolibarrOrderDto createdOrder = dolibarrClient.createOrder(orderDto);
// 3. Create linked invoice with lines copied from the order and billing address // 3. Create linked invoice with lines copied from the order
Integer fkAddress = resolveInvoiceAddress(psOrder, customer.id()); DolibarrInvoiceDto invoice = dolibarrClient.createInvoiceFromOrder(createdOrder, null);
DolibarrInvoiceDto invoice = dolibarrClient.createInvoiceFromOrder(createdOrder, fkAddress);
// 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 // 4. Persist mapping to prevent re-import
orderMappingRepository.save(OrderMapping.builder() orderMappingRepository.save(OrderMapping.builder()
@ -192,125 +194,106 @@ public class OrderSyncService {
DolibarrThirdpartyDto thirdparty = dolibarrClient.getOrCreateThirdparty(email, name, address, city, zip); 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) { if (!isGuest && psCustomer != null) {
syncCustomerAddresses(Integer.parseInt(psOrder.idCustomer()), thirdparty.id()); syncCustomerContacts(Integer.parseInt(psOrder.idCustomer()), thirdparty.id());
} }
return thirdparty; return thirdparty;
} }
private void syncCustomerAddresses(Integer psCustomerId, Integer dolibarrThirdpartyId) { private void syncCustomerContacts(Integer psCustomerId, Integer dolibarrThirdpartyId) {
try { try {
// 1. Obtener direcciones de PrestaShop
List<PrestashopAddressDto> psAddresses = prestashopClient.getCustomerAddresses(psCustomerId); List<PrestashopAddressDto> psAddresses = prestashopClient.getCustomerAddresses(psCustomerId);
List<DolibarrContactDto> dolibarrContacts = dolibarrClient.getThirdpartyContacts(dolibarrThirdpartyId);
// 2. Obtener direcciones existentes en Dolibarr
List<DolibarrAddressDto> dolibarrAddresses = dolibarrClient.getThirdpartyAddresses(dolibarrThirdpartyId);
PrestashopCustomerDto psCustomer = prestashopClient.getCustomer(psCustomerId); PrestashopCustomerDto psCustomer = prestashopClient.getCustomer(psCustomerId);
String defaultAddressId = psCustomer != null ? psCustomer.idDefaultAddress() : null; String defaultAddressId = psCustomer != null ? psCustomer.idDefaultAddress() : null;
// 3. Procesar cada dirección de PrestaShop
for (PrestashopAddressDto psAddr : psAddresses) { for (PrestashopAddressDto psAddr : psAddresses) {
// Ignorar la dirección por defecto (ya está en thirdparty.address/city/zip) // Default address is already on the thirdparty itself (address/zip/city fields)
if (psAddr.id() != null && psAddr.id().toString().equals(defaultAddressId)) { if (psAddr.id() != null && psAddr.id().toString().equals(defaultAddressId)) continue;
continue;
}
String formattedAddress = formatAddress(psAddr.address1(), psAddr.address2()); String formattedAddr = formatAddress(psAddr.address1(), psAddr.address2());
String formattedCity = psAddr.city();
// Buscar si existe una dirección con los mismos datos en Dolibarr DolibarrContactDto existing = dolibarrContacts.stream()
DolibarrAddressDto existingAddr = dolibarrAddresses.stream() .filter(c -> c.address() != null && c.address().equals(formattedAddr)
.filter(a -> a.address() != null && a.address().equals(formattedAddress) && c.town() != null && c.town().equals(psAddr.city()))
&& a.city() != null && a.city().equals(formattedCity))
.findFirst() .findFirst()
.orElse(null); .orElse(null);
if (existingAddr != null) { if (existing != null) {
// ACTUALIZAR: La dirección existe pero podría tener cambios en zip, phone, etc. dolibarrClient.updateContact(existing.id(), new DolibarrContactDto(
DolibarrAddressDto updatedAddr = new DolibarrAddressDto( existing.id(), dolibarrThirdpartyId,
existingAddr.id(), psAddr.firstname(), psAddr.lastname(),
dolibarrThirdpartyId, formattedAddr, psAddr.postcode(), psAddr.city(),
formatPersonName(psAddr.firstname(), psAddr.lastname()), psAddr.phone(), null));
formattedAddress, log.debug("Updated PS address {} as Dolibarr contact", psAddr.id());
psAddr.postcode(),
formattedCity,
null,
psAddr.phone(),
0
);
dolibarrClient.updateThirdpartyAddress(existingAddr.id(), updatedAddr);
log.debug("Updated PS address {} in Dolibarr", psAddr.id());
} else { } else {
// CREAR: Dirección nueva en PrestaShop dolibarrClient.createContact(new DolibarrContactDto(
DolibarrAddressDto newAddr = new DolibarrAddressDto( null, dolibarrThirdpartyId,
null, psAddr.firstname(), psAddr.lastname(),
dolibarrThirdpartyId, formattedAddr, psAddr.postcode(), psAddr.city(),
formatPersonName(psAddr.firstname(), psAddr.lastname()), psAddr.phone(), null));
formattedAddress, log.debug("Created PS address {} as Dolibarr contact", psAddr.id());
psAddr.postcode(),
formattedCity,
null,
psAddr.phone(),
0
);
dolibarrClient.createThirdpartyAddress(dolibarrThirdpartyId, newAddr);
log.debug("Created PS address {} in Dolibarr", psAddr.id());
} }
} }
// 4. ELIMINAR direcciones que ya no existen en PrestaShop // Remove contacts that no longer exist in PrestaShop
Set<String> psAddressStrings = psAddresses.stream() Set<String> psKeys = psAddresses.stream()
.filter(a -> !(a.id() != null && a.id().toString().equals(defaultAddressId))) .filter(a -> !(a.id() != null && a.id().toString().equals(defaultAddressId)))
.map(a -> formatAddress(a.address1(), a.address2()) + "|" + a.city()) .map(a -> formatAddress(a.address1(), a.address2()) + "|" + a.city())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
for (DolibarrAddressDto dolibarrAddr : dolibarrAddresses) { for (DolibarrContactDto c : dolibarrContacts) {
String dolibarrAddressStr = dolibarrAddr.address() + "|" + dolibarrAddr.city(); if (!psKeys.contains(c.address() + "|" + c.town())) {
if (!psAddressStrings.contains(dolibarrAddressStr)) { dolibarrClient.deleteContact(c.id());
dolibarrClient.deleteThirdpartyAddress(dolibarrAddr.id()); log.debug("Deleted orphaned Dolibarr contact {}", c.id());
log.debug("Deleted orphaned address {} from Dolibarr", dolibarrAddr.id());
} }
} }
} catch (Exception e) { } 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(); 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 boolean isGuest = psOrder.idCustomer() == null
|| psOrder.idCustomer().isBlank() || psOrder.idCustomer().isBlank()
|| "0".equals(psOrder.idCustomer()); || "0".equals(psOrder.idCustomer());
// If billing addr = customer default addr → thirdparty main address, no contact to link
if (!isGuest) { if (!isGuest) {
try { try {
PrestashopCustomerDto c = prestashopClient.getCustomer(Integer.parseInt(psOrder.idCustomer())); 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) { } 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 { try {
PrestashopAddressDto billing = prestashopClient.getAddress(Integer.parseInt(addrId)); PrestashopAddressDto billing = prestashopClient.getAddress(Integer.parseInt(addrId));
if (billing == null) return null; if (billing == null) return;
String formatted = formatAddress(billing.address1(), billing.address2()); String formatted = formatAddress(billing.address1(), billing.address2());
return dolibarrClient.getThirdpartyAddresses(dolibarrThirdpartyId).stream()
.filter(a -> a.address() != null && a.address().equals(formatted) dolibarrClient.getThirdpartyContacts(dolibarrThirdpartyId).stream()
&& a.city() != null && a.city().equals(billing.city())) .filter(c -> c.address() != null && c.address().equals(formatted)
.map(DolibarrAddressDto::id) && c.town() != null && c.town().equals(billing.city()))
.findFirst() .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) { } catch (Exception e) {
log.warn("Could not resolve billing address for PS order {}: {}", psOrder.id(), e.getMessage()); log.warn("Could not resolve billing contact for PS order {}: {}", psOrder.id(), e.getMessage());
return null;
} }
} }
@ -320,11 +303,6 @@ public class OrderSyncService {
return addr1 + " " + addr2; 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. * Maps PrestaShop order rows to Dolibarr order lines.
* Converts tax-included unit price to excl-tax using the configured {@code defaultTaxRate}. * Converts tax-included unit price to excl-tax using the configured {@code defaultTaxRate}.