From a727878b0c95e10cc58a850801e08466f5c7089d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:44:06 -0600 Subject: [PATCH 1/8] 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()); + } +} From 1bab36f7270e0b876ca29661d4eec6776d2377ff Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:44:10 -0600 Subject: [PATCH 2/8] Remove debit payment data --- .../petshop/backend/service/SaleService.java | 20 ++++++++++++++++++- .../V10__remove_debit_payment_method.sql | 3 +++ .../resources/db/migration/V2__seed_data.sql | 6 +++--- .../backend/service/SaleServiceTest.java | 17 ++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql create mode 100644 backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b426dc38..b8d5861e 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -78,7 +78,7 @@ public class SaleService { sale.setSaleDate(LocalDateTime.now()); sale.setEmployee(employee); sale.setStore(store); - sale.setPaymentMethod(request.getPaymentMethod()); + sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); if (request.getCustomerId() != null) { @@ -215,4 +215,22 @@ public class SaleService { return response; } + + String normalizePaymentMethod(String paymentMethod) { + if (paymentMethod == null) { + return null; + } + + String normalized = paymentMethod.trim(); + if (normalized.equalsIgnoreCase("Debit")) { + return "Card"; + } + if (normalized.equalsIgnoreCase("Cash")) { + return "Cash"; + } + if (normalized.equalsIgnoreCase("Card")) { + return "Card"; + } + return normalized; + } } diff --git a/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql b/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql new file mode 100644 index 00000000..874b0205 --- /dev/null +++ b/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql @@ -0,0 +1,3 @@ +UPDATE sale +SET paymentMethod = 'Card' +WHERE LOWER(paymentMethod) = 'debit'; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 5e8d3fb6..d7308ad4 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -128,7 +128,7 @@ VALUES ('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1), ('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2), ('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3), -('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1), +('2026-01-15 10:45:00', 150.00, 'Card', 1, 1, 1), ('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2), ('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL), ('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4), @@ -136,12 +136,12 @@ VALUES ('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1), ('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3), ('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL), -('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2), +('2026-02-08 16:15:00', 160.00, 'Card', 1, 1, 2), ('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL), ('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1), ('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL), ('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4), -('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL), +('2026-02-20 14:35:00', 155.00, 'Card', 4, 3, NULL), ('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2), ('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL), (NOW(), 95.00, 'Card', 1, 1, 1); diff --git a/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java b/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java new file mode 100644 index 00000000..b0ffeded --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java @@ -0,0 +1,17 @@ +package com.petshop.backend.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SaleServiceTest { + + @Test + void normalizePaymentMethodMapsDebitToCard() { + SaleService saleService = new SaleService(null, null, null, null, null, null, null, null); + + assertEquals("Card", saleService.normalizePaymentMethod("Debit")); + assertEquals("Card", saleService.normalizePaymentMethod("debit")); + assertEquals("Cash", saleService.normalizePaymentMethod("Cash")); + } +} From 24041f4242ea4d40d90557322afa015d0cc6a71c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:52:45 -0600 Subject: [PATCH 3/8] Fix user linking --- .../backend/service/CustomerService.java | 9 ++++++++ .../migration/V9__backfill_user_accounts.sql | 2 ++ .../backend/service/CustomerServiceTest.java | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+) 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 a34bcb2b..fb4f75c0 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -13,6 +13,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.CONFLICT; @Service public class CustomerService { @@ -57,6 +60,12 @@ public class CustomerService { customer = customerRepository.save(customer); Customer savedCustomer = customer; User user = userRepository.findByEmail(savedCustomer.getEmail()) + .map(existing -> { + if (existing.getRole() != User.Role.CUSTOMER) { + throw new ResponseStatusException(CONFLICT, "Email already exists for a different account type"); + } + return existing; + }) .orElseGet(() -> createLinkedUser(savedCustomer)); Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); 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 index 9a78abe3..273313a3 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -41,11 +41,13 @@ WHERE e.user_id IS NULL UPDATE customer c JOIN users u ON u.email = c.email + AND u.role = 'CUSTOMER' SET c.user_id = u.id WHERE c.user_id IS NULL; UPDATE employee e JOIN users u ON u.email = e.email + AND u.role IN ('STAFF', 'ADMIN') SET e.user_id = u.id WHERE e.user_id 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 index b9152511..6d2bfad2 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -12,10 +12,12 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.server.ResponseStatusException; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -82,4 +84,23 @@ class CustomerServiceTest { assertEquals("Pat Owner", createdUser.getFullName()); assertEquals("200-000-0007", createdUser.getPhone()); } + + @Test + void createCustomerRejectsExistingNonCustomerEmail() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("staff1"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.STAFF); + + when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); + } } From 277d1dce8f53e23aec9fcf76d9d0516c02c21817 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:59:43 -0600 Subject: [PATCH 4/8] Tighten user linking --- .../backend/service/CustomerService.java | 16 +++++++ .../migration/V9__backfill_user_accounts.sql | 12 +++-- .../backend/service/CustomerServiceTest.java | 45 ++++++++++++++++++- 3 files changed, 68 insertions(+), 5 deletions(-) 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 fb4f75c0..d3c1354d 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -52,6 +52,8 @@ public class CustomerService { @Transactional public CustomerResponse createCustomer(CustomerRequest request) { + ensureEmailAvailable(request.getEmail(), null); + Customer customer = new Customer(); customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); @@ -78,6 +80,8 @@ public class CustomerService { Customer customer = customerRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + ensureEmailAvailable(request.getEmail(), customer.getUserId()); + customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); @@ -142,4 +146,16 @@ public class CustomerService { private String generatePhone(Customer customer) { return String.format("200-000-%04d", customer.getCustomerId()); } + + private void ensureEmailAvailable(String email, Long currentUserId) { + if (email == null || email.isBlank()) { + return; + } + + userRepository.findByEmail(email).ifPresent(existing -> { + if (currentUserId == null || !existing.getId().equals(currentUserId)) { + throw new ResponseStatusException(CONFLICT, "Email already exists"); + } + }); + } } 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 index 273313a3..05cee442 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,7 +2,10 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - c.email, + CASE + WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) THEN CONCAT('customer_', c.customerId, '@petshop.local') + ELSE c.email + END AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -14,14 +17,16 @@ WHERE c.user_id IS NULL 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, + CASE + WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) THEN CONCAT('employee_', e.employeeId, '@petshop.local') + ELSE e.email + END AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE @@ -36,7 +41,6 @@ WHERE e.user_id IS NULL SELECT 1 FROM users u WHERE u.username = CONCAT('employee_', e.employeeId) - OR u.email = e.email ); UPDATE customer c diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index 6d2bfad2..4b18a1cd 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -98,9 +98,52 @@ class CustomerServiceTest { existing.setEmail("pat@example.com"); existing.setRole(User.Role.STAFF); - when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); } + + @Test + void createCustomerRejectsExistingCustomerEmail() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("customer1"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.CUSTOMER); + + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); + } + + @Test + void updateCustomerRejectsExistingEmailFromOtherUser() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + customer.setEmail("old@example.com"); + + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("customer2"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.CUSTOMER); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request)); + } } From f6147aa810d133821b5aac404679bc6b36a12cf1 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:02:44 -0600 Subject: [PATCH 5/8] Tighten backfill migration --- .../db/migration/V9__backfill_user_accounts.sql | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 index 05cee442..ecb020b1 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,10 +2,7 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) THEN CONCAT('customer_', c.customerId, '@petshop.local') - ELSE c.email - END AS email, + CONCAT('customer_', c.customerId, '@petshop.local') AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -23,10 +20,7 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('employee_', e.employeeId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) THEN CONCAT('employee_', e.employeeId, '@petshop.local') - ELSE e.email - END AS email, + CONCAT('employee_', e.employeeId, '@petshop.local') AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE @@ -44,13 +38,13 @@ WHERE e.user_id IS NULL ); UPDATE customer c -JOIN users u ON u.email = c.email +JOIN users u ON u.username = CONCAT('customer_', c.customerId) AND u.role = 'CUSTOMER' SET c.user_id = u.id WHERE c.user_id IS NULL; UPDATE employee e -JOIN users u ON u.email = e.email +JOIN users u ON u.username = CONCAT('employee_', e.employeeId) AND u.role IN ('STAFF', 'ADMIN') SET e.user_id = u.id WHERE e.user_id IS NULL; From 1a0fe7f95d59b207572967629aa5243364e94682 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:09:39 -0600 Subject: [PATCH 6/8] Disable generated user accounts --- .../java/com/petshop/backend/service/CustomerService.java | 2 +- .../resources/db/migration/V9__backfill_user_accounts.sql | 4 ++-- .../java/com/petshop/backend/service/CustomerServiceTest.java | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) 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 d3c1354d..3b1e7dfe 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -134,7 +134,7 @@ public class CustomerService { user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); user.setPhone(generatePhone(customer)); user.setRole(User.Role.CUSTOMER); - user.setActive(true); + user.setActive(false); user.setTokenVersion(0); return userRepository.save(user); } 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 index ecb020b1..1c62e4eb 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -6,7 +6,7 @@ SELECT CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, - TRUE AS active, + FALSE AS active, 0 AS tokenVersion FROM customer c WHERE c.user_id IS NULL @@ -27,7 +27,7 @@ SELECT WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' ELSE 'STAFF' END AS role, - TRUE AS active, + FALSE AS active, 0 AS tokenVersion FROM employee e WHERE e.user_id 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 index 4b18a1cd..a9d82f5b 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -83,6 +83,7 @@ class CustomerServiceTest { assertEquals("pat@example.com", createdUser.getEmail()); assertEquals("Pat Owner", createdUser.getFullName()); assertEquals("200-000-0007", createdUser.getPhone()); + assertEquals(false, createdUser.getActive()); } @Test From 3fbb108646236def03a8515ad1f84ee17e7319b4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:14:53 -0600 Subject: [PATCH 7/8] Preserve backfill emails --- .../migration/V9__backfill_user_accounts.sql | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 index 1c62e4eb..4af69669 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,7 +2,15 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CONCAT('customer_', c.customerId, '@petshop.local') AS email, + CASE + WHEN c.email IS NOT NULL + AND c.email <> '' + AND (SELECT COUNT(*) FROM customer c2 WHERE c2.email = c.email) = 1 + AND NOT EXISTS (SELECT 1 FROM employee e2 WHERE e2.email = c.email) + AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) + THEN c.email + ELSE CONCAT('customer_', c.customerId, '@petshop.local') + END AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -20,7 +28,15 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('employee_', e.employeeId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CONCAT('employee_', e.employeeId, '@petshop.local') AS email, + CASE + WHEN e.email IS NOT NULL + AND e.email <> '' + AND (SELECT COUNT(*) FROM employee e2 WHERE e2.email = e.email) = 1 + AND NOT EXISTS (SELECT 1 FROM customer c2 WHERE c2.email = e.email) + AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) + THEN e.email + ELSE CONCAT('employee_', e.employeeId, '@petshop.local') + END AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE From 5d3efa0af5c229d16d64aa235691df2feb54e59e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:37:18 -0600 Subject: [PATCH 8/8] Clean up customer accounts --- .../backend/service/CustomerService.java | 19 +++++------- .../backend/service/CustomerServiceTest.java | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) 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 3b1e7dfe..33c731d1 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -60,15 +60,7 @@ public class CustomerService { customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - Customer savedCustomer = customer; - User user = userRepository.findByEmail(savedCustomer.getEmail()) - .map(existing -> { - if (existing.getRole() != User.Role.CUSTOMER) { - throw new ResponseStatusException(CONFLICT, "Email already exists for a different account type"); - } - return existing; - }) - .orElseGet(() -> createLinkedUser(savedCustomer)); + User user = createLinkedUser(customer); Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); syncLinkedUser(linkedCustomer); @@ -93,9 +85,14 @@ public class CustomerService { @Transactional public void deleteCustomer(Long id) { - if (!customerRepository.existsById(id)) { - throw new ResourceNotFoundException("Customer not found with id: " + id); + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + + if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) { + userRepository.deleteById(customer.getUserId()); + return; } + customerRepository.deleteById(id); } diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index a9d82f5b..dde07768 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -147,4 +148,33 @@ class CustomerServiceTest { assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request)); } + + @Test + void deleteCustomerDeletesLinkedUser() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.existsById(11L)).thenReturn(true); + + customerService.deleteCustomer(7L); + + verify(userRepository).deleteById(11L); + verify(customerRepository, never()).deleteById(7L); + } + + @Test + void deleteCustomerDeletesCustomerWhenNoLinkedUserExists() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.existsById(11L)).thenReturn(false); + + customerService.deleteCustomer(7L); + + verify(customerRepository).deleteById(7L); + } }