From c3c5b22b12263e636b40360a35752432d6690522 Mon Sep 17 00:00:00 2001 From: Mireya Serrano Date: Sun, 24 May 2026 18:44:34 +0200 Subject: [PATCH] addresses --- .../integration/dolibarr/DolibarrClient.java | 77 +++++++++- .../dolibarr/dto/DolibarrContactDto.java | 19 +++ .../sync_service/sync/OrderSyncService.java | 136 ++++++++---------- 3 files changed, 152 insertions(+), 80 deletions(-) create mode 100644 sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/dto/DolibarrContactDto.java diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/DolibarrClient.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/DolibarrClient.java index 0d46ce6..f24ccdf 100644 --- a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/DolibarrClient.java +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/DolibarrClient.java @@ -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 getThirdpartyContacts(Integer socid) { + try { + List contacts = restClient.get() + .uri(u -> u.path("/contacts") + .queryParam("sqlfilters", "(t.fk_soc:=:" + socid + ")") + .queryParam("limit", 100) + .build()) + .retrieve() + .body(new ParameterizedTypeReference>() {}); + 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 ──────────────────────────────────────────────────────────── /** diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/dto/DolibarrContactDto.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/dto/DolibarrContactDto.java new file mode 100644 index 0000000..43586fb --- /dev/null +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/dolibarr/dto/DolibarrContactDto.java @@ -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 +) {} diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/OrderSyncService.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/OrderSyncService.java index f362021..bce6f3b 100644 --- a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/OrderSyncService.java +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/OrderSyncService.java @@ -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 psAddresses = prestashopClient.getCustomerAddresses(psCustomerId); - - // 2. Obtener direcciones existentes en Dolibarr - List dolibarrAddresses = dolibarrClient.getThirdpartyAddresses(dolibarrThirdpartyId); + List 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 psAddressStrings = psAddresses.stream() + // Remove contacts that no longer exist in PrestaShop + Set 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}.