AddressService.java
package com.rbctcsworld.ecommerce.address;
import com.rbctcsworld.ecommerce.address.AddressDtos.AddressRequest;
import com.rbctcsworld.ecommerce.address.AddressDtos.AddressResponse;
import com.rbctcsworld.ecommerce.auth.AppUser;
import com.rbctcsworld.ecommerce.auth.UserRepository;
import com.rbctcsworld.ecommerce.common.exception.ConflictException;
import com.rbctcsworld.ecommerce.common.exception.NotFoundException;
import com.rbctcsworld.ecommerce.order.ShippingAddress;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Locale;
/**
* The customer's address book. Rules:
* - at most {@value #MAX} addresses (409 when full);
* - the same address twice is refused (409; compared without case and extra spaces, label ignored);
* - the first address is the default; makeDefault moves the default; deleting the default makes the newest
* remaining address the default;
* - somebody else's address does not exist for you (404), so ids cannot be probed.
*/
@Service
@Transactional
public class AddressService {
public static final int MAX = 10;
private final CustomerAddressRepository repo;
private final UserRepository users;
public AddressService(CustomerAddressRepository repo, UserRepository users) {
this.repo = repo;
this.users = users;
}
@Transactional(readOnly = true)
public List<AddressResponse> list(String email) {
return mine(user(email).getId()).stream().map(AddressResponse::from).toList();
}
public AddressResponse add(String email, AddressRequest r) {
Long userId = user(email).getId();
List<CustomerAddress> all = mine(userId);
if (all.size() >= MAX) throw new ConflictException("Your address book is full (" + MAX + " addresses). Delete one first.");
ShippingAddress a = r.address().normalized();
refuseTwin(all, a, null);
CustomerAddress saved = new CustomerAddress(userId);
saved.change(label(r.label()), a);
boolean first = all.isEmpty();
if (first || r.makeDefault()) clearDefault(all);
saved.setDefault(first || r.makeDefault());
return AddressResponse.from(repo.save(saved));
}
public AddressResponse update(String email, long id, AddressRequest r) {
Long userId = user(email).getId();
CustomerAddress a = owned(id, userId);
ShippingAddress normalized = r.address().normalized();
List<CustomerAddress> all = mine(userId);
refuseTwin(all, normalized, id);
a.change(label(r.label()), normalized);
if (r.makeDefault() && !a.isDefault()) {
clearDefault(all);
a.setDefault(true);
}
return AddressResponse.from(repo.save(a));
}
public AddressResponse makeDefault(String email, long id) {
Long userId = user(email).getId();
CustomerAddress a = owned(id, userId);
clearDefault(mine(userId));
a.setDefault(true);
return AddressResponse.from(repo.save(a));
}
public void delete(String email, long id) {
Long userId = user(email).getId();
CustomerAddress a = owned(id, userId);
boolean wasDefault = a.isDefault();
repo.delete(a);
repo.flush();
if (wasDefault) {
mine(userId).stream().max(java.util.Comparator.comparing(CustomerAddress::getCreatedAt)
.thenComparing(CustomerAddress::getId))
.ifPresent(next -> { next.setDefault(true); repo.save(next); });
}
}
private void refuseTwin(List<CustomerAddress> all, ShippingAddress a, Long self) {
String key = key(a);
for (CustomerAddress other : all) {
if (!other.getId().equals(self) && key(other.address()).equals(key)) {
throw new ConflictException("This address is already in your address book"
+ (other.getLabel() == null ? "" : " as \"" + other.getLabel() + "\""));
}
}
}
/** Name, street, city, state and ZIP without case or extra spaces (phone and label do not make it another place). */
static String key(ShippingAddress a) {
return String.join("|", a.fullName(), a.line1(), a.line2() == null ? "" : a.line2(), a.city(), a.state(), a.zip())
.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ").trim();
}
private static String label(String label) {
return label == null || label.isBlank() ? null : label.trim().replaceAll("\\s+", " ");
}
private void clearDefault(List<CustomerAddress> all) {
for (CustomerAddress a : all) {
if (a.isDefault()) {
a.setDefault(false);
repo.save(a);
}
}
repo.flush();
}
private List<CustomerAddress> mine(Long userId) {
return repo.findByUserIdOrderByIsDefaultDescCreatedAtDescIdDesc(userId);
}
private CustomerAddress owned(long id, Long userId) {
return repo.findByIdAndUserId(id, userId).orElseThrow(() -> new NotFoundException("Address not found: " + id));
}
private AppUser user(String email) {
return users.findByEmail(email).orElseThrow(() -> new NotFoundException("User not found"));
}
}