- Controller pattern:
@Path+@Component+@RequiredArgsConstructor(see JAX-RS Endpoints) - Service pattern:
@Service+ constructor injection + transactional write methods (see Transaction Management) - Validation pattern: validator component + required field checks + typed exceptions (see Input Validation)
- Security pattern:
@PreAuthorize+ self-service access validation via security context (see Security Implementation) - Testing pattern: Mockito-based unit tests and Testcontainers integration tests (see Testing Patterns)
Use this checklist first, then follow the detailed sections below for full examples and conventions.
This document defines procedural rules and patterns for AI agents working with the Mifos Self Service Plugin codebase.
All Java files must include the standard MPL-2.0 license header:
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package org.apache.fineract.selfservice.example;Follow the established package structure:
org.apache.fineract.selfservice.*
- security/ # Authentication, authorization, security context
- useradministration/ # User management and roles
- client/ # Client operations and data
- savings/ # Savings account operations
- loanaccount/ # Loan account operations
- products/ # Product browsing and discovery
- registration/ # User registration workflows
- config/ # Configuration and beans
- runreport/ # Reporting functionality
- spm/ # Survey participation management
- shareaccounts/ # Share account operations
- API Resources:
SelfXxxApiResource(e.g.,SelfSavingsApiResource) - Services:
XxxService/XxxServiceImpl(e.g.,SelfSavingsService) - Data Validators:
XxxDataValidator(e.g.,SelfSavingsDataValidator) - Repositories:
XxxRepository(e.g.,SelfSavingsRepository) - Domain Objects:
Xxx(e.g.,SelfSavingsAccount)
Use Lombok's @RequiredArgsConstructor for constructor injection:
@Service
@RequiredArgsConstructor
public class SelfSavingsServiceImpl implements SelfSavingsService {
private final SelfSavingsRepository repository;
private final SelfSavingsDataValidator validator;
}@Path("/v1/self/savingsaccounts")
@Component
@Tag(name = "Self Savings Account", description = "Self-service savings account operations")
@RequiredArgsConstructor
public class SelfSavingsApiResource {
@GET
@Path("/{accountId}")
@Produces(MediaType.APPLICATION_JSON)
public Response getSavingsAccount(@PathParam("accountId") Long accountId) {
// Implementation
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createSavingsAccount(String jsonRequestBody) {
// Implementation
}
}Always use proper HTTP status codes and Response objects:
return Response.ok().entity(responseData).build();
return Response.status(Response.Status.CREATED).entity(createdData).build();
return Response.status(Response.Status.BAD_REQUEST).entity(errorMessage).build();@PreAuthorize("hasAuthority('READ_SAVINGSACCOUNT')")
public SavingsAccountData getSavingsAccount(Long accountId) {
// Implementation
}Inject and use the security context properly:
@RequiredArgsConstructor
public class SelfSavingsService {
private final PlatformSelfServiceSecurityContext securityContext;
public void validateAccess(Long accountId) {
securityContext.validateSelfServiceUserAccess(accountId);
}
}@Component
public class SelfSavingsDataValidator {
private final FromJsonHelper fromJsonHelper;
public void validateCreate(String json) {
if (StringUtils.isBlank(json)) {
throw new PlatformApiDataValidationException("validation.msg.empty.json");
}
Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
Map<String, Object> data = fromJsonHelper.extractObject(json, typeOfMap);
validateRequiredFields(data);
validateFieldFormats(data);
}
private void validateRequiredFields(Map<String, Object> data) {
List<String> missingFields = new ArrayList<>();
if (!data.containsKey("clientId")) {
missingFields.add("clientId");
}
if (!missingFields.isEmpty()) {
throw new PlatformApiDataValidationException(
"validation.msg.required.fields.missing",
String.join(", ", missingFields)
);
}
}
}@Repository
public interface SelfSavingsRepository {
SelfSavingsAccount findById(Long id);
@Query("SELECT s FROM SelfSavingsAccount s WHERE s.clientId = :clientId")
List<SelfSavingsAccount> findByClientId(@Param("clientId") Long clientId);
SelfSavingsAccount save(SelfSavingsAccount account);
void delete(SelfSavingsAccount account);
}@Service
@Transactional
public class SelfSavingsServiceImpl implements SelfSavingsService {
@Transactional
public CommandProcessingResult createSavingsAccount(Command command) {
validateCommand(command);
SelfSavingsAccount account = createAccountFromCommand(command);
SelfSavingsAccount savedAccount = repository.save(account);
return new CommandProcessingResultBuilder().withEntityId(savedAccount.getId()).build();
}
}public class SelfSavingsException extends RuntimeException {
public SelfSavingsException(String message) {
super(message);
}
public SelfSavingsException(String message, Throwable cause) {
super(message, cause);
}
}Use Fineract's standard exception handling patterns:
@ExceptionHandler(SelfSavingsException.class)
public Response handleSelfSavingsException(SelfSavingsException ex) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(ApiGlobalExceptionHandler.createErrorResponse(ex.getMessage()))
.build();
}@ExtendWith(MockitoExtension.class)
class SelfSavingsServiceImplTest {
@Mock
private SelfSavingsRepository repository;
@Mock
private SelfSavingsDataValidator validator;
@InjectMocks
private SelfSavingsServiceImpl service;
@Test
void shouldCreateSavingsAccount() {
// Given
Command command = createValidCommand();
SelfSavingsAccount expectedAccount = createExpectedAccount();
when(repository.save(any())).thenReturn(expectedAccount);
// When
CommandProcessingResult result = service.createSavingsAccount(command);
// Then
assertThat(result.getEntityId()).isEqualTo(expectedAccount.getId());
verify(validator).validateCreate(any());
verify(repository).save(any());
}
}@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class SelfSavingsApiResourceIntegrationTest extends SelfServiceIntegrationTestBase {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("fineract_test")
.withUsername("mifos")
.withPassword("password");
@Test
@DisplayName("GET /v1/self/savingsaccounts/{id} should return account details")
void shouldReturnSavingsAccountDetails() {
// Test implementation
}
}# Self Service Plugin Configuration
fineract.selfservice.enabled=true
fineract.selfservice.base-path=/v1/self
fineract.security.basicauth.enabled=true
fineract.security.oauth.enabled=false@Configuration
@EnableWebSecurity
public class SelfServiceSecurityConfig {
@Bean
public SecurityFilterChain selfServiceFilterChain(HttpSecurity http) throws Exception {
// Security configuration
}
}Ensure proper plugin configuration in pom.xml:
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<configuration>
<java>
<googleJavaFormat>
<version>1.17.0</version>
<style>GOOGLE</style>
</googleJavaFormat>
</java>
</configuration>
</plugin>This is a library/plugin that extends Apache Fineract. It does not run as a standalone Docker container.
# As dependency in Apache Fineract (Docker)
The JAR is loaded via -Dloader.path parameter when running Fineract
# As dependency in Apache Fineract (Tomcat)
Copy JAR to $TOMCAT_HOME/webapps/fineract-provider/WEB-INF/lib/- Always validate input data before processing
- Use proper security annotations on all service methods
- Follow RESTful conventions for API design
- Write comprehensive tests for all functionality
- Handle exceptions gracefully with meaningful error messages
- Use proper logging for debugging and monitoring
- Follow Fineract coding standards for consistency
- Document all public APIs with proper Javadoc
- Don't expose sensitive data in API responses
- Don't skip security validation for any endpoint
- Don't use hardcoded values - use configuration properties
- Don't ignore transaction boundaries in service methods
- Don't forget to validate user permissions before operations
- Don't use deprecated APIs - prefer current JAX-RS annotations
- Don't mix concerns - keep services focused on single responsibilities