From d8622df3186c4cd377eca3457ff812eac263a928 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:44:06 -0600 Subject: [PATCH] Backfill user accounts --- .../backend/service/CustomerService.java | 40 ++++++++- .../migration/V9__backfill_user_accounts.sql | 75 ++++++++++++++++ .../backend/service/CustomerServiceTest.java | 85 +++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql create mode 100644 backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index 040be22a..a34bcb2b 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -4,23 +4,31 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.customer.CustomerRequest; import com.petshop.backend.dto.customer.CustomerResponse; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class CustomerService { + private static final String TEMP_PASSWORD = "TempPass123!"; + private final CustomerRepository customerRepository; private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) { + public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.customerRepository = customerRepository; this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } public Page getAllCustomers(String query, Pageable pageable) { @@ -47,8 +55,13 @@ public class CustomerService { customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - syncLinkedUser(customer); - return mapToResponse(customer); + Customer savedCustomer = customer; + User user = userRepository.findByEmail(savedCustomer.getEmail()) + .orElseGet(() -> createLinkedUser(savedCustomer)); + + Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); + syncLinkedUser(linkedCustomer); + return mapToResponse(linkedCustomer); } @Transactional @@ -99,4 +112,25 @@ public class CustomerService { userRepository.save(user); }); } + + private User createLinkedUser(Customer customer) { + User user = new User(); + user.setUsername(generateUsername(customer)); + user.setPassword(passwordEncoder.encode(TEMP_PASSWORD)); + user.setEmail(customer.getEmail()); + user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); + user.setPhone(generatePhone(customer)); + user.setRole(User.Role.CUSTOMER); + user.setActive(true); + user.setTokenVersion(0); + return userRepository.save(user); + } + + private String generateUsername(Customer customer) { + return "customer_" + customer.getCustomerId(); + } + + private String generatePhone(Customer customer) { + return String.format("200-000-%04d", customer.getCustomerId()); + } } diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql new file mode 100644 index 00000000..9a78abe3 --- /dev/null +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -0,0 +1,75 @@ +INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) +SELECT + CONCAT('customer_', c.customerId) AS username, + '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, + c.email, + CONCAT(c.firstName, ' ', c.lastName) AS fullName, + CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, + 'CUSTOMER' AS role, + TRUE AS active, + 0 AS tokenVersion +FROM customer c +WHERE c.user_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM users u + WHERE u.username = CONCAT('customer_', c.customerId) + OR u.email = c.email + ); + +INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) +SELECT + CONCAT('employee_', e.employeeId) AS username, + '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, + e.email, + CONCAT(e.firstName, ' ', e.lastName) AS fullName, + CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, + CASE + WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' + ELSE 'STAFF' + END AS role, + TRUE AS active, + 0 AS tokenVersion +FROM employee e +WHERE e.user_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM users u + WHERE u.username = CONCAT('employee_', e.employeeId) + OR u.email = e.email + ); + +UPDATE customer c +JOIN users u ON u.email = c.email +SET c.user_id = u.id +WHERE c.user_id IS NULL; + +UPDATE employee e +JOIN users u ON u.email = e.email +SET e.user_id = u.id +WHERE e.user_id IS NULL; + +UPDATE users +SET + fullName = CASE + WHEN fullName IS NULL OR fullName = '' THEN username + ELSE fullName + END, + email = CASE + WHEN email IS NULL OR email = '' THEN CONCAT(username, '@petshop.local') + ELSE email + END, + phone = CASE + WHEN phone IS NULL OR phone = '' THEN CONCAT('000-000-', LPAD(id, 4, '0')) + ELSE phone + END, + active = COALESCE(active, TRUE), + tokenVersion = COALESCE(tokenVersion, 0) +WHERE fullName IS NULL + OR fullName = '' + OR email IS NULL + OR email = '' + OR phone IS NULL + OR phone = '' + OR active IS NULL + OR tokenVersion IS NULL; diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java new file mode 100644 index 00000000..b9152511 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -0,0 +1,85 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.customer.CustomerRequest; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CustomerServiceTest { + + @Mock + private CustomerRepository customerRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private UserBusinessLinkageService userBusinessLinkageService; + + @InjectMocks + private CustomerService customerService; + + @Test + void createCustomerCreatesLinkedUser() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + Customer savedCustomer = new Customer(); + savedCustomer.setCustomerId(7L); + savedCustomer.setFirstName("Pat"); + savedCustomer.setLastName("Owner"); + savedCustomer.setEmail("pat@example.com"); + + when(customerRepository.save(any(Customer.class))).thenReturn(savedCustomer); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.empty()); + when(passwordEncoder.encode(any())).thenReturn("hashed-temp-password"); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + user.setId(11L); + return user; + }); + when(userBusinessLinkageService.ensureLinkedCustomer(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + savedCustomer.setUserId(user.getId()); + return savedCustomer; + }); + + var response = customerService.createCustomer(request); + + assertNotNull(response); + assertEquals("Pat", response.getFirstName()); + assertEquals("Owner", response.getLastName()); + assertEquals("pat@example.com", response.getEmail()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(userRepository).save(userCaptor.capture()); + User createdUser = userCaptor.getValue(); + assertEquals("customer_7", createdUser.getUsername()); + assertEquals("hashed-temp-password", createdUser.getPassword()); + assertEquals("pat@example.com", createdUser.getEmail()); + assertEquals("Pat Owner", createdUser.getFullName()); + assertEquals("200-000-0007", createdUser.getPhone()); + } +}