From 2dcf881148bcfc66dd66da5d8d31548ff1e7d608 Mon Sep 17 00:00:00 2001 From: Francisco Cuandon Date: Fri, 3 Jul 2026 23:39:11 -0600 Subject: [PATCH] FINERACT-535: Allow the same name of teller if its from different branchs --- .../TellerWritePlatformServiceJpaImpl.java | 2 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0242_update_m_tellers_unique_key.xml | 39 ++ .../migrations/sample_data/barebones_db.sql | 2 +- .../sample_data/load_sample_data.sql | 2 +- ...TellerWritePlatformServiceJpaImplTest.java | 482 ++++++++++++++++++ 6 files changed, 525 insertions(+), 3 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0242_update_m_tellers_unique_key.xml create mode 100644 fineract-provider/src/test/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImplTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java index b00b5c98ba1..06f118804cb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java @@ -183,7 +183,7 @@ public CommandProcessingResult deleteTeller(Long tellerId) { * Guaranteed to throw an exception no matter what the data integrity issue is. */ private void handleTellerDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) { - if (realCause.getMessage().contains("m_tellers_name_unq")) { + if (realCause.getMessage().contains("m_tellers_office_name_unq")) { final String name = command.stringValueOfParameterNamed("name"); throw new PlatformDataIntegrityException("error.msg.teller.duplicate.name", "Teller with name `" + name + "` already exists", "name", name); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index c603611fb2e..dd9549ade47 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -259,4 +259,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0242_update_m_tellers_unique_key.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0242_update_m_tellers_unique_key.xml new file mode 100644 index 00000000000..b4638d07c4e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0242_update_m_tellers_unique_key.xml @@ -0,0 +1,39 @@ + + + + + + + + ALTER TABLE m_tellers DROP INDEX name + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql b/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql index 09cd583eb5c..8e7642fb469 100644 --- a/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql +++ b/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql @@ -5291,7 +5291,7 @@ CREATE TABLE IF NOT EXISTS `m_tellers` ( `valid_to` date DEFAULT NULL, `state` SMALLINT DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `m_tellers_name_unq` (`name`), + UNIQUE KEY `m_tellers_office_name_unq` (`office_id`, `name`), KEY `IK_m_tellers_m_office` (`office_id`), KEY `FK_m_tellers_gl_account_debit_account_id` (`debit_account_id`), KEY `FK_m_tellers_gl_account_credit_account_id` (`credit_account_id`), diff --git a/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql b/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql index f800b6fa8a5..9747ff30160 100644 --- a/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql +++ b/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql @@ -5548,7 +5548,7 @@ CREATE TABLE IF NOT EXISTS `m_tellers` ( `valid_to` date DEFAULT NULL, `state` SMALLINT DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `m_tellers_name_unq` (`name`), + UNIQUE KEY `m_tellers_office_name_unq` (`office_id`, `name`), KEY `IK_m_tellers_m_office` (`office_id`), KEY `FK_m_tellers_gl_account_debit_account_id` (`debit_account_id`), KEY `FK_m_tellers_gl_account_credit_account_id` (`credit_account_id`), diff --git a/fineract-provider/src/test/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImplTest.java new file mode 100644 index 00000000000..7e06d13d0ba --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImplTest.java @@ -0,0 +1,482 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.organisation.teller.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; +import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; +import org.apache.fineract.accounting.journalentry.domain.JournalEntry; +import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepository; +import org.apache.fineract.organisation.staff.exception.StaffNotFoundException; +import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; +import org.apache.fineract.organisation.teller.domain.Cashier; +import org.apache.fineract.organisation.teller.domain.CashierRepository; +import org.apache.fineract.organisation.teller.domain.CashierTransaction; +import org.apache.fineract.organisation.teller.domain.CashierTransactionRepository; +import org.apache.fineract.organisation.teller.domain.Teller; +import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper; +import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException; +import org.apache.fineract.organisation.teller.exception.CashierNotFoundException; +import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer; +import org.apache.fineract.useradministration.domain.AppUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.orm.jpa.JpaSystemException; + +@ExtendWith(MockitoExtension.class) +public class TellerWritePlatformServiceJpaImplTest { + + @Mock + private AppUser currentUser; + + @Mock + private JsonCommand command; + + @Mock + private Office office; + + @Mock + private Teller teller; + + @Mock + private PlatformSecurityContext context; + + @Mock + private TellerCommandFromApiJsonDeserializer fromApiJsonDeserializer; + + @Mock + private OfficeRepositoryWrapper officeRepositoryWrapper; + + @Mock + private TellerRepositoryWrapper tellerRepositoryWrapper; + + @Mock + private StaffRepository staffRepository; + + @Mock + private CashierTransactionDataValidator cashierTransactionDataValidator; + + @Mock + private CashierRepository cashierRepository; + + @Mock + private CashierTransactionRepository cashierTxnRepository; + + @Mock + private FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper; + + @Mock + private JournalEntryRepository glJournalEntryRepository; + + @InjectMocks + private TellerWritePlatformServiceJpaImpl underTest; + + @BeforeEach + void initGlobalMocks() { + HashMap businessDates = new HashMap<>(); + businessDates.put(BusinessDateType.BUSINESS_DATE, LocalDate.of(2026, 1, 1)); + ThreadLocalContextUtil.setBusinessDates(businessDates); + + lenient().when(context.authenticatedUser()).thenReturn(currentUser); + } + + @Nested + public class CreateTeller { + + @BeforeEach + void setUp() { + when(command.longValueOfParameterNamed("officeId")).thenReturn(1L); + doNothing().when(fromApiJsonDeserializer).validateForCreateAndUpdateTeller(any()); + when(officeRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(office); + } + + @Test + void shouldCreateATeller() { + when(tellerRepositoryWrapper.saveAndFlush(any(Teller.class))).thenReturn(teller); + when(command.commandId()).thenReturn(1L); + when(office.getId()).thenReturn(1L); + + CommandProcessingResult result = underTest.createTeller(command); + + verify(fromApiJsonDeserializer, times(1)).validateForCreateAndUpdateTeller(any()); + verify(tellerRepositoryWrapper, times(1)).saveAndFlush(any(Teller.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(null, result.getResourceId()); + assertEquals(1L, result.getOfficeId()); + } + + @Test + void shouldThrowExceptionWhenTellerExistsInTheSameOffice() { + JpaSystemException exception = newException("m_tellers_office_name_unq"); + when(tellerRepositoryWrapper.saveAndFlush(any(Teller.class))).thenThrow(exception); + when(command.stringValueOfParameterNamed("name")).thenReturn("DUPLICATED TELLER"); + + assertDataIntegrityCode("error.msg.teller.duplicate.name", () -> underTest.createTeller(command)); + verify(fromApiJsonDeserializer, times(1)).validateForCreateAndUpdateTeller(any()); + verify(tellerRepositoryWrapper, times(1)).saveAndFlush(any(Teller.class)); + } + + @Test + void shouldThrowExceptionWhenUnknownDataIntegrityIssueOccurs() { + JpaSystemException exception = newException("Persistence provider encountered an internal error"); + when(tellerRepositoryWrapper.saveAndFlush(any(Teller.class))).thenThrow(exception); + + assertDataIntegrityCode("error.msg.teller.unknown.data.integrity.issue", () -> underTest.createTeller(command)); + verify(fromApiJsonDeserializer, times(1)).validateForCreateAndUpdateTeller(any()); + verify(tellerRepositoryWrapper, times(1)).saveAndFlush(any(Teller.class)); + } + } + + @Nested + public class ModifyTeller { + + @BeforeEach + void setUp() { + when(command.longValueOfParameterNamed("officeId")).thenReturn(1L); + when(officeRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(office); + doNothing().when(fromApiJsonDeserializer).validateForCreateAndUpdateTeller(any()); + when(officeRepositoryWrapper.findOfficeHierarchy(anyLong())).thenReturn(office); + when(tellerRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(teller); + } + + @Test + void shouldModifyATellerWhenChangesArePresent() { + when(currentUser.getOffice()).thenReturn(office); + when(office.getId()).thenReturn(1L); + when(teller.update(any(Office.class), any(JsonCommand.class))).thenReturn(getTellerChanges()); + when(tellerRepositoryWrapper.saveAndFlush(any(Teller.class))).thenReturn(teller); + when(command.commandId()).thenReturn(1L); + when(teller.getId()).thenReturn(1L); + when(teller.officeId()).thenReturn(1L); + + CommandProcessingResult result = underTest.modifyTeller(1L, command); + + verify(fromApiJsonDeserializer, times(1)).validateForCreateAndUpdateTeller(any()); + verify(tellerRepositoryWrapper, times(1)).saveAndFlush(any(Teller.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + assertEquals(1L, result.getOfficeId()); + } + } + + @Nested + class DeleteTeller { + + @BeforeEach + void setUp() { + when(tellerRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(teller); + } + + @Test + void shouldDeleteATeller() { + when(teller.getCashiers()).thenReturn(Collections.emptySet()); + when(teller.getId()).thenReturn(1L); + + CommandProcessingResult result = underTest.deleteTeller(1L); + + verify(tellerRepositoryWrapper, times(1)).delete(any(Teller.class)); + assertNotNull(result); + assertEquals(1L, result.getResourceId()); + } + + @Test + void shouldThrowExceptionWhenCashierExistsForTeller() { + Cashier cashier = mock(Cashier.class); + Set cashiers = new HashSet<>(); + cashiers.add(cashier); + + when(teller.getCashiers()).thenReturn(cashiers); + when(cashier.getTeller()).thenReturn(teller); + when(teller.getId()).thenReturn(1L); + + assertThrows(CashierExistForTellerException.class, () -> underTest.deleteTeller(1L)); + verify(tellerRepositoryWrapper, times(0)).delete(any(Teller.class)); + } + } + + @Nested + class AllocateCashier { + + @BeforeEach + void setUp() { + when(tellerRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(teller); + when(teller.getOffice()).thenReturn(office); + when(command.longValueOfParameterNamed("staffId")).thenReturn(1L); + doNothing().when(fromApiJsonDeserializer).validateForAllocateCashier(any()); + } + + @Test + void shouldAllocateCashierToTeller() { + Staff staff = mock(Staff.class); + Cashier cashier = mock(Cashier.class); + + when(staffRepository.findById(anyLong())).thenReturn(Optional.of(staff)); + when(command.booleanObjectValueOfParameterNamed("isFullDay")).thenReturn(true); + doNothing().when(cashierTransactionDataValidator).validateCashierAllowedDateAndTime(any(Cashier.class), any(Teller.class)); + when(cashierRepository.save(any(Cashier.class))).thenReturn(cashier); + when(command.commandId()).thenReturn(1L); + when(teller.getId()).thenReturn(1L); + + CommandProcessingResult result = underTest.allocateCashierToTeller(1L, command); + + verify(fromApiJsonDeserializer, times(1)).validateForAllocateCashier(any()); + verify(cashierTransactionDataValidator).validateCashierAllowedDateAndTime(any(Cashier.class), any(Teller.class)); + verify(cashierRepository, times(1)).save(any(Cashier.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + } + + @Test + void shouldThrowStaffNotFoundException() { + when(staffRepository.findById(anyLong())).thenThrow(new StaffNotFoundException(1L)); + + assertThrows(StaffNotFoundException.class, () -> underTest.allocateCashierToTeller(1L, command)); + verify(fromApiJsonDeserializer, times(1)).validateForAllocateCashier(any()); + } + + @Test + void shouldAllocateCashierToTellerWithStartAndEndTimePeriod() { + Staff staff = mock(Staff.class); + Cashier cashier = mock(Cashier.class); + + when(staffRepository.findById(anyLong())).thenReturn(Optional.of(staff)); + when(command.booleanObjectValueOfParameterNamed("isFullDay")).thenReturn(false); + when(command.longValueOfParameterNamed("hourStartTime")).thenReturn(9L); + when(command.longValueOfParameterNamed("minStartTime")).thenReturn(0L); + when(command.longValueOfParameterNamed("hourEndTime")).thenReturn(17L); + when(command.longValueOfParameterNamed("minEndTime")).thenReturn(0L); + + doNothing().when(cashierTransactionDataValidator).validateCashierAllowedDateAndTime(any(Cashier.class), any(Teller.class)); + when(cashierRepository.save(any(Cashier.class))).thenReturn(cashier); + when(command.commandId()).thenReturn(1L); + when(teller.getId()).thenReturn(1L); + + CommandProcessingResult result = underTest.allocateCashierToTeller(1L, command); + + verify(fromApiJsonDeserializer, times(1)).validateForAllocateCashier(any()); + verify(cashierTransactionDataValidator).validateCashierAllowedDateAndTime(any(Cashier.class), any(Teller.class)); + verify(cashierRepository, times(1)).save(any(Cashier.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + } + } + + @Nested + class UpdateCashierAllocation { + + @BeforeEach + void setUp() { + doNothing().when(fromApiJsonDeserializer).validateForAllocateCashier(any()); + when(command.longValueOfParameterNamed("staffId")).thenReturn(1L); + } + + @Test + void shouldUpdateCashierAllocation() { + Staff staff = mock(Staff.class); + Cashier cashier = mock(Cashier.class); + Map simulatedChanges = Map.of("isFullDay", "false"); + + when(staffRepository.findById(anyLong())).thenReturn(Optional.of(staff)); + when(currentUser.getOffice()).thenReturn(office); + when(office.getId()).thenReturn(1L); + when(officeRepositoryWrapper.findOfficeHierarchy(anyLong())).thenReturn(office); + when(tellerRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(teller); + when(teller.officeId()).thenReturn(1L); + when(cashierRepository.findById(anyLong())).thenReturn(Optional.of(cashier)); + when(cashier.update(any(JsonCommand.class))).thenReturn(simulatedChanges); + when(cashierRepository.saveAndFlush(any(Cashier.class))).thenReturn(cashier); + when(command.commandId()).thenReturn(1L); + when(cashier.getTeller()).thenReturn(teller); + when(teller.getId()).thenReturn(1L); + when(cashier.getId()).thenReturn(1L); + + CommandProcessingResult result = underTest.updateCashierAllocation(1L, 1L, command); + + verify(fromApiJsonDeserializer, times(1)).validateForAllocateCashier(any()); + verify(cashierRepository, times(1)).saveAndFlush(any(Cashier.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + assertEquals(1L, result.getSubResourceId()); + } + + @Test + void shouldThrowStaffNotFoundException() { + when(staffRepository.findById(anyLong())).thenThrow(new StaffNotFoundException(1L)); + + assertThrows(StaffNotFoundException.class, () -> underTest.updateCashierAllocation(1L, 1L, command)); + verify(fromApiJsonDeserializer, times(1)).validateForAllocateCashier(any()); + } + } + + @Nested + class DeleteCashierAllocation { + + @Test + void shouldDeleteCashierAllocation() { + Cashier cashier = mock(Cashier.class); + + when(currentUser.getOffice()).thenReturn(office); + when(office.getId()).thenReturn(1L); + when(officeRepositoryWrapper.findOfficeHierarchy(anyLong())).thenReturn(office); + when(tellerRepositoryWrapper.findOneWithNotFoundDetection(anyLong())).thenReturn(teller); + when(teller.officeId()).thenReturn(1L); + when(cashierRepository.findById(anyLong())).thenReturn(Optional.of(cashier)); + + CommandProcessingResult result = underTest.deleteCashierAllocation(1L, 1L, command); + + verify(cashierRepository, times(1)).delete(any(Cashier.class)); + assertNotNull(result); + assertEquals(1L, result.getResourceId()); + } + } + + @Nested + class AllocateCashToTeller { + + @Test + void shouldAllocateCashToTeller() { + Cashier cashier = mock(Cashier.class); + CashierTransaction cashierTxn = mock(CashierTransaction.class); + setupSharedTransactionMocks(cashier, cashierTxn); + + CommandProcessingResult result = underTest.allocateCashToCashier(1L, command); + + verify(fromApiJsonDeserializer, times(1)).validateForCashTxnForCashier(any()); + verify(cashierTxnRepository, times(1)).save(any(CashierTransaction.class)); + verify(glJournalEntryRepository, times(2)).saveAndFlush(any(JournalEntry.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + } + + @Test + void shouldThrowCashierNotFoundException() { + when(cashierRepository.findById(anyLong())).thenThrow(new CashierNotFoundException(1L)); + + assertThrows(CashierNotFoundException.class, () -> underTest.allocateCashToCashier(1L, command)); + } + } + + @Nested + class SettleCashFromCashier { + + @Test + void shouldSettleCashFromCashier() { + doNothing().when(cashierTransactionDataValidator).validateSettleCashAndCashOutTransactions(anyLong(), any(JsonCommand.class)); + + Cashier cashier = mock(Cashier.class); + CashierTransaction cashierTxn = mock(CashierTransaction.class); + setupSharedTransactionMocks(cashier, cashierTxn); + + CommandProcessingResult result = underTest.settleCashFromCashier(1L, command); + + verify(cashierTransactionDataValidator, times(1)).validateSettleCashAndCashOutTransactions(anyLong(), any(JsonCommand.class)); + verify(fromApiJsonDeserializer, times(1)).validateForCashTxnForCashier(any()); + verify(cashierTxnRepository, times(1)).save(any(CashierTransaction.class)); + verify(glJournalEntryRepository, times(2)).saveAndFlush(any(JournalEntry.class)); + assertNotNull(result); + assertEquals(1L, result.getCommandId()); + assertEquals(1L, result.getResourceId()); + } + } + + private void setupSharedTransactionMocks(Cashier cashier, CashierTransaction cashierTxn) { + when(cashierRepository.findById(anyLong())).thenReturn(Optional.of(cashier)); + doNothing().when(fromApiJsonDeserializer).validateForCashTxnForCashier(any()); + when(command.stringValueOfParameterNamed("entityType")).thenReturn("loan account"); + when(cashierTxnRepository.save(any(CashierTransaction.class))).thenReturn(cashierTxn); + + FinancialActivityAccount financialActivityAccount = mock(FinancialActivityAccount.class); + when(financialActivityAccountRepositoryWrapper.findByFinancialActivityTypeWithNotFoundDetection(anyInt())).thenReturn(financialActivityAccount); + + GLAccount glAccount = mock(GLAccount.class); + when(financialActivityAccount.getGlAccount()).thenReturn(glAccount); + when(cashier.getTeller()).thenReturn(teller); + when(teller.getOffice()).thenReturn(office); + + lenient().when(currentUser.getId()).thenReturn(1L); + lenient().when(office.getId()).thenReturn(1L); + + lenient().when(cashierTxn.getCurrencyCode()).thenReturn("USD"); + lenient().when(cashierTxn.getTxnDate()).thenReturn(LocalDate.of(2026, 1, 1)); + lenient().when(cashierTxn.getTxnAmount()).thenReturn(BigDecimal.valueOf(100)); + lenient().when(cashierTxn.getTxnNote()).thenReturn("CASHIER ALLOCATION"); + + JournalEntry journalEntry = mock(JournalEntry.class); + when(glJournalEntryRepository.saveAndFlush(any(JournalEntry.class))).thenReturn(journalEntry); + + when(command.commandId()).thenReturn(1L); + when(cashier.getId()).thenReturn(1L); + } + + private JpaSystemException newException(final String message) { + return new JpaSystemException(new RuntimeException(message)); + } + + private void assertDataIntegrityCode(String expectedCode, Runnable runnable) { + PlatformDataIntegrityException ex = assertThrows(PlatformDataIntegrityException.class, runnable::run); + assertEquals(expectedCode, ex.getGlobalisationMessageCode()); + } + + private Map getTellerChanges() { + return Map.ofEntries(Map.entry("name", "NEW NAME")); + } +}