This SDK empowers developers to leverage the timveroOS platform with flexibility, allowing extensive customization through code. By utilizing these components, teams can create bespoke financial solutions that align perfectly with their business goals.

For a complete working example, see the timvero-example project.

1. Getting Started

This section provides a quick introduction to building applications with the Timvero SDK. Follow this guide to get up and running in minutes and understand the core concepts through practical examples.

1.1. Prerequisites

Before you begin, ensure you have the following installed:

  • Java 21 or later - The platform requires modern Java features

  • Maven 3.8+ - For dependency management and building

  • PostgreSQL 16+ - Primary database for the platform

  • IDE with Spring Boot support - IntelliJ IDEA, Eclipse, or VS Code

1.2. Quick Setup (5 minutes)

Step 1: Clone and Configure
  1. Clone the example project:

    git clone https://github.com/TimveroOS/timvero-example.git
    cd timvero-example
  2. Configure database connection in src/main/resources/application.properties:

    spring.datasource.url=jdbc:postgresql://localhost:5432/your_database
    spring.datasource.username=your_username
    spring.datasource.password=your_password
  3. Run the application:

    mvn spring-boot:run
  4. Access the application:

Step 2: Verify Installation

Once running, you should see:

  • Database tables automatically created via Flyway migrations

  • Admin interface with navigation menu

  • Sample data (if configured)

  • No compilation errors in the console

1.3. Your First Entity: Client Management (15 minutes)

Let’s explore how the Client entity demonstrates the platform’s core patterns. The Client entity is already implemented in the example project, so you can see a complete working example.

Entity Definition

The Client entity demonstrates the platform’s entity structure:

@Entity
@Table
@Audited
@Indexed
public class Client extends AbstractAuditable<UUID> implements NamedEntity, HasDocuments {

    @Embedded
    @Valid
    private IndividualInfo individualInfo;

    @Embedded
    @Valid
    private ContactInfo contactInfo;

    // getters and setters...
}

Key features: * Extends AbstractAuditable: Automatic creation/modification tracking * Implements NamedEntity: Provides display name functionality * Composite structure: Contains IndividualInfo and ContactInfo components * Search integration: @Indexed enables full-text search * Audit support: @Audited tracks all changes

Form Structure

The ClientForm handles user input with validation:

public class ClientForm {

    @Valid
    @NotNull
    private IndividualInfoForm individualInfo;

    @Valid
    @NotNull
    private ContactInfoForm contactInfo;

    // getters and setters...
}

Benefits: * Nested validation: @Valid cascades validation to nested objects * Clean separation: Form objects separate from entities * Type safety: Strongly typed form fields

Controller Implementation

The main controller handles entity management:

@Controller
public class ClientController extends EntityController<UUID, Client, ClientForm> {
    // Inherits all CRUD functionality automatically
}

Actions provide specific operations (buttons in the UI):

@Controller
public class CreateClientAction extends EntityCreateController<UUID, Client, ClientForm> {
    @Override
    protected boolean isOwnPage() {
        return false;
    }
}

@Controller
public class EditClientAction extends EditEntityActionController<UUID, Client, ClientForm> {
    // Handles the edit button functionality
}

What you get automatically: * ✅ Create, Read, Update, Delete operations * ✅ Form validation and error handling * ✅ List view with search and filtering * ✅ Responsive web interface * ✅ Audit logging of all changes

Form Service Layer

The service layer handles business logic and data mapping:

@Service
public class ClientFormService extends EntityFormService<Client, ClientForm, UUID> {
    // Inherits entity-form mapping and persistence operations
}

The service requires a corresponding MapStruct mapper for entity-form conversion:

@Mapper
public interface ClientFormMapper extends EntityToFormMapper<Client, ClientForm> {
    // MapStruct automatically generates implementation for bidirectional mapping
}
Template Integration

The HTML template demonstrates the form component system:

<th:block th:insert="~{/form/components :: text(
    #{client.individualInfo.fullName},
    'individualInfo.fullName',
    'v-required v-name')}" />

<th:block th:insert="~{/form/components :: text(
    #{client.contactInfo.email},
    'contactInfo.email',
    'v-required v-email')}" />

1.4. Essential Concepts (10 minutes)

Entity-Form-Controller Pattern

The platform follows a consistent architectural pattern:

Table 1. Platform Architecture Pattern
Component Purpose Example

Entity

JPA entity with business logic and relationships

Client - stores customer data with audit trail

Form

DTO for user input with validation rules

ClientForm - handles form submission and validation

Controller

Main entity controller providing CRUD operations

ClientController - handles entity management

Actions

Specific operation buttons in the UI

CreateClientAction, EditClientAction - handle specific operations

Service

Business logic and entity-form mapping

ClientFormService - converts between entities and forms

Mapper

Automatic bidirectional object mapping

ClientFormMapper - MapStruct-generated conversions

Automatic Features

Once you create the basic structure following this pattern, the platform automatically provides:

  • CRUD Operations: Complete create, read, update, delete functionality

  • Form Validation: Client-side and server-side validation

  • Database Migrations: Automatic schema generation and versioning

  • Search and Filtering: Full-text search and advanced filtering

  • Audit Logging: Complete change history tracking

  • Responsive UI: Mobile-friendly web interface

  • Security Integration: Authentication and authorization

  • API Endpoints: RESTful API for external integration

Data Flow

Understanding the data flow helps you work effectively with the platform:

User Input → Form Validation → Controller → Service → Mapper → Entity → Database
                     ↓
             Template Rendering ← Form Object ← Mapper ← Entity ← Database Query

1.5. Common Scenarios (20 minutes)

Adding Custom Validation

Enhance the Client form with custom business rules:

public class ClientForm {
    @NotBlank
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String fullName;

    @NotBlank
    @Email(message = "Please provide a valid email address")
    private String email;

    @NotBlank
    @Phone(message = "Please provide a valid phone number")
    private String phone;

    @PastOrPresent(message = "Birth date cannot be in the future")
    private LocalDate dateOfBirth;
}
Implementing Business Logic with Entity Checkers

Create automated workflows that respond to client changes:

@Component
public class ClientWelcomeChecker extends EntityChecker<Client> {

    @Override
    protected void registerListeners(CheckerListenerRegistry<Client> registry) {
        // Trigger when a new client is created
        registry.entityChange().inserted();
    }

    @Override
    protected boolean isAvailable(Client client) {
        // Only for clients with complete contact information
        return client.getContactInfo() != null
            && client.getContactInfo().getEmail() != null;
    }

    @Override
    protected void perform(Client client) {
        // Send welcome email to new clients
        emailService.sendWelcomeEmail(client);
        log.info("Welcome email sent to client: {}", client.getIndividualInfo().getFullName());
    }
}
Adding Document Management

Enable clients to upload required documents:

// 1. Make Client support documents
@Entity
public class Client extends AbstractAuditable<UUID> implements HasDocuments {
    // Existing client implementation
}

// 2. Configure document types
@Configuration
public class ClientDocumentConfiguration {

    public static final EntityDocumentType ID_DOCUMENT = new EntityDocumentType("ID_DOCUMENT");
    public static final EntityDocumentType PROOF_OF_ADDRESS = new EntityDocumentType("PROOF_OF_ADDRESS");

    @Bean
    DocumentTypeAssociation<Client> clientRequiredDocuments() {
        return DocumentTypeAssociation.forEntityClass(Client.class)
            .required(ID_DOCUMENT)
            .required(PROOF_OF_ADDRESS)
            .build();
    }
}

// 3. Add document management tab
@Controller
@Order(1000)
public class ClientDocumentsTab extends EntityDocumentTabController<Client> {

    @Override
    public boolean isVisible(Client client) {
        return true; // Always show documents tab for clients
    }
}
Integrating External Data Sources

Fetch additional data from external APIs:

// 1. Create a data source subject interface
public interface CreditCheckSubject {
    String getNationalId();
    String getFullName();
}

// 2. Implement the interface in your entity
@Entity
public class Client implements CreditCheckSubject {

    @Override
    public String getNationalId() {
        return getIndividualInfo().getNationalId();
    }

    @Override
    public String getFullName() {
        return getIndividualInfo().getFullName();
    }
}

// 3. Create the data source implementation
@Service("creditCheck")
public class CreditCheckDataSource implements MappedDataSource<CreditCheckSubject, CreditReport> {

    @Override
    public Class<CreditReport> getType() {
        return CreditReport.class;
    }

    @Override
    public Content getData(CreditCheckSubject subject) throws Exception {
        // Call external credit check API
        String response = creditCheckApi.checkCredit(
            subject.getNationalId(),
            subject.getFullName()
        );
        return new Content(response.getBytes(), MediaType.APPLICATION_JSON_VALUE);
    }

    @Override
    public CreditReport parseRecord(Content data) throws Exception {
        return objectMapper.readValue(data.getData(), CreditReport.class);
    }
}

1.6. What’s Next?

Explore Advanced Features

Now that you understand the basics, dive deeper into specific areas:

  • Form Classes - Complex validation, nested forms, and custom components

  • Entity Checkers - Business rule automation and workflow triggers

  • Document Management - File uploads, document requirements, and digital signatures

  • DataSource Integration - External API integration and data enrichment

  • Template System - Custom UI components and advanced templating

Real-World Implementation Patterns

Study these complete examples in the project:

  • Client Onboarding: Complete customer registration with validation and document collection

  • Application Processing: Multi-step loan application workflow with automated decision making

  • Participant Management: Complex participant relationships with role-based permissions

  • Document Workflows: Digital signature processes with DocuSign integration

  • Risk Assessment: External data integration for credit scoring and fraud detection

Development Best Practices
  • Start Simple: Begin with basic CRUD operations, add complexity gradually

  • Follow Patterns: Use the established Entity-Form-Controller pattern consistently

  • Leverage Automation: Use Entity Checkers for business rules instead of manual processes

  • Test Thoroughly: The platform provides excellent testing support for all components

  • Monitor Performance: Built-in metrics and logging help optimize your application

Getting Help
  • Documentation: This guide covers all platform features in detail

  • Example Project: Every feature demonstrated with working code

  • Professional Support: Enterprise support available for production deployments

Next Steps Checklist
  • Create your first custom entity following the Client pattern

  • Add custom validation rules to your forms

  • Implement an Entity Checker for business logic automation

  • Set up document management for your entities

  • Integrate with an external data source

  • Customize the UI templates for your specific needs

  • Deploy to a staging environment for testing

You’re now ready to build powerful financial applications with the Timvero platform!

2. Data model setup

This section describes how to set up and manage the data model using SQL file autogeneration and Flyway migrations.

2.1. SQL File Autogeneration

The platform automatically generates SQL files based on your entity definitions. This process creates the necessary database schema files that can be used with Flyway for database migrations.

Automatic Generation Process

After the class is configured, run the application. The system will analyze changes in the data model of Java classes and generate an SQL script with the necessary changes V241012192920.sql in the project’s home directory (application.home=path), in the subdirectory hbm2ddl.

The generation process works as follows:

  1. Entity Analysis: The system scans all JPA entity classes for changes

  2. Schema Comparison: Compares current entity definitions with the existing database schema

  3. SQL Generation: Creates appropriate DDL statements (CREATE, ALTER, DROP) for detected changes

  4. File Creation: Generates timestamped migration files in the hbm2ddl directory

  5. Migration Integration: Files can be moved to Flyway migration directory for deployment

Entity Definition Example

Let’s look at the Participant entity as an example:

@Entity
@Table
@Audited
@Indexed
public class Participant extends AbstractAuditable<UUID> implements NamedEntity, GithubDataSourceSubject, HasDocuments,
    ProcessEntity, DocusignSigner,
    HasPendingDecisions {

    public static final String DECISION_OWNER_TYPE = "PARTICIPANT";

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private ParticipantStatus status = ParticipantStatus.NEW;

    @ElementCollection(fetch = FetchType.EAGER)
    @Enumerated(EnumType.STRING)
    private Set<ParticipantRole> roles;

    @ManyToOne(fetch = EAGER)
    @JoinColumn(nullable = false, updatable = false)
    private Application application;

    @ManyToOne(fetch = EAGER)
    @JoinColumn(nullable = false, updatable = false)
    private Client client;

    @Column(nullable = false)
    @Enumerated(STRING)
    private Employment employment;

    @Column(nullable = false)
    @Enumerated(STRING)
    private Periodicity howOftenIncomeIsPaid;

    @Embedded
    private MonetaryAmount totalAnnualIncome;

    @Embedded
    private MonetaryAmount monthlyOutgoings;

    @Column
    private String githubUsername;
    // getters and setters...
}
Enum Definitions

The entity uses several enums that define the possible values:

ParticipantStatus enum
public enum ParticipantStatus implements InEnum<ParticipantStatus> {
    NEW,
    IN_PROCESS,
    MANUAL_APPROVAL,
    APPROVED,
    DECLINED,
    VOID;

    public boolean isActive() {
        return !this.in(DECLINED, VOID);
    }
}
Employment enum
public enum Employment {

    EMPLOYED,
    HOMEMAKER,
    UNEMPLOYED,
    RETIRED,
    SELF_EMPLOYED

}
Periodicity enum
public enum Periodicity {

    MONTHLY,
    FORTNIGHTLY,
    WEEKLY,
    UNDEFINED

}

2.2. Flyway Migration Integration

Migration File Structure

Flyway migration files are stored in the src/main/resources/db/migration/ directory and follow the naming convention:

V{version}__{description}.sql

For example:

V250530170222__init.sql
V250609220043__participantStatus.sql
Generated SQL Example

Based on the Participant entity definition, the system generates the following SQL:

    create table participant (
        id uuid not null,
        created_at timestamp(6) with time zone not null,
        updated_at timestamp(6) with time zone not null,
        employment varchar(255) not null,
        how_often_income_is_paid varchar(255) not null,
        monthly_outgoings_currency varchar(3),
        monthly_outgoings_number numeric(19,2),
        total_annual_income_currency varchar(3),
        total_annual_income_number numeric(19,2),
        created_by uuid,
        updated_by uuid,
        -- Foreign key to application table
        application_id uuid not null,
        -- Foreign key to client table
        client_id uuid not null,
        primary key (id)
    );

    -- Foreign key constraints for participant table
    -- Links participant to their associated loan application
    alter table if exists participant 
       add constraint FKa8akyngsbkcpy4ev19q53x56h 
       foreign key (application_id) 
       references application;

    -- Links participant to their client profile containing personal information
    alter table if exists participant 
       add constraint FKcmejtugfqk653qthh0jalsx54 
       foreign key (client_id) 
       references client;
Migration Workflow
  1. Entity Definition: Define your entity classes with appropriate JPA annotations

  2. Application Execution: Run the application to trigger the automatic analysis process

  3. SQL Autogeneration: The platform analyzes entity changes and generates SQL scripts in the hbm2ddl subdirectory

  4. Migration File Preparation: Move generated SQL files from hbm2ddl to the Flyway migration directory (src/main/resources/db/migration/)

  5. File Naming: Rename files to follow Flyway convention: V{version}__{description}.sql

  6. Flyway Execution: During application startup, Flyway executes pending migrations in version order

  7. Schema Versioning: Database schema version is tracked automatically in the schema_version table

Best Practices
  • Incremental Changes: Create separate migration files for each schema change

  • Descriptive Names: Use clear, descriptive names for migration files

  • Testing: Test migrations on development environments before production

  • Rollback Strategy: Consider rollback scenarios when designing schema changes

Migration File Example

Here’s an actual migration file that adds participant status functionality:

V250609220043__participantStatus.sql
-- Migration: Add participant status functionality

-- Add status column to audit table (for historical tracking)
alter table if exists aud_participant 
   add column status varchar(255);

-- Add status column to main participant table
alter table if exists participant 
   add column status varchar(255);

-- Set default status for all existing participants
update participant set status = 'NEW';

-- Make status column mandatory after setting default values
alter table if exists participant 
   alter column status set not null;

This approach ensures that your database schema evolves in a controlled, versioned manner while maintaining data integrity throughout the development lifecycle.

3. Form classes setup and usage

This section describes how to set up and manage form classes for data input validation and processing in the application.

3.1. Form Class Architecture

The platform uses form classes to handle user input validation, data binding, and form processing. Form classes serve as DTOs (Data Transfer Objects) that define the structure and validation rules for user interfaces.

Form Class Hierarchy

The application uses a hierarchical form structure:

  • Main Forms: Top-level forms like ClientForm and ApplicationForm

  • Nested Forms: Component forms like IndividualInfoForm and ContactInfoForm

  • Validation: Bean Validation (JSR-303) annotations for field validation

Form Class Examples
ClientForm Structure

The ClientForm class handles client registration and profile management:

    @Valid
    @NotNull
    private IndividualInfoForm individualInfo;

    @Valid
    @NotNull
    private ContactInfoForm contactInfo;
ApplicationForm Structure

The ApplicationForm class manages loan application data:

    @Valid
    private ParticipantForm borrowerParticipant;
Nested Form Components
IndividualInfoForm

Personal information component:

    @NotBlank
    private String nationalId;

    @NotBlank
    private String fullName;

    @PastOrPresent
    @DateTimeFormat(pattern = ValidationUtils.PATTERN_DATEPICKER_FORMAT)
    private LocalDate dateOfBirth;

    @NotNull
    private Country residenceCountry;
ContactInfoForm

Contact information component:

    @NotBlank
    @Email
    private String email;

    @NotBlank
    @Phone
    private String phone;
ParticipantForm

Financial participant information:

    @NotNull
    private Employment employment;

    @NotNull
    private Periodicity howOftenIncomeIsPaid;

    @NotNull
    private MonetaryAmount totalAnnualIncome;

    private MonetaryAmount monthlyOutgoings;
Validation Annotations Used

The form classes use standard Bean Validation (JSR-303) annotations:

Validation Annotations in Use
@NotNull          // Field cannot be null
@NotBlank         // String field cannot be null, empty, or whitespace only
@Email            // Valid email format
@PastOrPresent    // Date must be in the past or present
@Valid            // Cascade validation to nested objects
@Phone            // Custom phone validation (platform-specific)

3.2. Form Processing Architecture

Action Classes

The platform uses generic action classes to handle form operations:

Generic Action Structure
@Controller
public class CreateClientAction extends EntityCreateController<UUID, Client, ClientForm> {

    @Override
    protected boolean isOwnPage() {
        return false;
    }
}
@Controller
public class EditClientAction extends EditEntityActionController<UUID, Client, ClientForm> {

}

These actions are parameterized with: * ID Type: UUID - The entity identifier type * Entity Type: Client - The JPA entity class * Form Type: ClientForm - The form DTO class

Form Service Layer

Actions delegate form processing to specialized service classes:

EntityFormService Usage
@Service
public class ClientFormService extends EntityFormService<Client, ClientForm, UUID> {

The EntityFormService provides: * Entity to Form mapping: Converting entities to form objects for editing * Form to Entity mapping: Converting form submissions to entity objects * Validation integration: Coordinating with Bean Validation * Persistence operations: Saving and updating entities

MapStruct Mappers

Form-to-entity conversion is handled by MapStruct mappers:

Mapper Structure
@Mapper
public interface ClientFormMapper extends EntityToFormMapper<Client, ClientForm> {
@Mapper(uses = ParticipantFormMapper.class)
public interface ApplicationFormMapper extends EntityToFormMapper<Application, ApplicationForm> {
@Mapper
public interface ParticipantFormMapper extends EntityToFormMapper<Participant, ParticipantForm> {

MapStruct automatically generates implementation classes that provide: * Bidirectional mapping: Entity ↔ Form conversion * Nested object mapping: Automatic handling of complex object structures * Type conversion: Automatic conversion between compatible types * Null handling: Safe mapping of optional fields

For detailed information about MapStruct features and configuration, see the official MapStruct documentation.

Processing Flow

The complete form processing flow:

  1. Action Invocation: CreateClientAction or EditClientAction is called

  2. Service Delegation: Action delegates to ClientFormService

  3. Mapper Usage: Service uses ClientFormMapper for conversions

  4. Entity Operations: Service performs database operations

  5. Response Generation: Converted data is returned to the controller

Example Flow for Edit Operation
EditClientAction<UUID, Client, ClientForm>
    ↓
ClientFormService.prepareEditModel(UUID id)
    ↓
ClientFormMapper.entityToForm(Client entity)
    ↓
ClientForm (ready for template rendering)
Example Flow for Save Operation
CreateClientAction<UUID, Client, ClientForm>
    ↓
ClientFormService.save(ClientForm form)
    ↓
ClientFormMapper.formToEntity(ClientForm form)
    ↓
Client entity (persisted to database)

3.3. Template Integration

Form classes integrate with HTML templates using Thymeleaf for rendering user interfaces. The templates use nested field access (dot notation) and reusable form components for consistent styling and validation.

For detailed information about HTML template integration, form components, and Thymeleaf usage, see [_html_template_integration].

4. HTML Template Integration

This section describes how form classes integrate with HTML templates using Thymeleaf for rendering user interfaces.

4.1. Template Structure

The application uses Thymeleaf templates to render forms with automatic data binding and validation integration.

Client Form Template

The client edit form demonstrates nested form structure:

        <h2 class="form-group__title" th:text="#{client.clientInfo}">Personal
            Information</h2>
        <th:block
            th:insert="~{/form/components :: text(#{client.individualInfo.fullName},
                'individualInfo.fullName', 'v-required v-name')}"
            th:with="maxlength = 120" />
        <th:block
            th:insert="~{/form/components :: text(#{client.individualInfo.nationalId},
                'individualInfo.nationalId', 'v-required')}" />
        <th:block
            th:insert="~{/form/components :: date (#{client.individualInfo.birthDate},
                'individualInfo.dateOfBirth', '')}"
            th:with="maxDate = ${#dates.format(#dates.createNow())}" />
        <th:block
            th:insert="~{/form/components :: select(#{client.address.stateOfResidence},
                'individualInfo.residenceCountry', ${countries})}" />

        <h2 class="form-group__title" th:text="#{client.contactInfo}">Contact
            Information</h2>
        <th:block
            th:insert="~{/form/components :: text(#{client.contactInfo.email},
                'contactInfo.email', 'v-required v-email')}" />
        <th:block
            th:insert="~{/form/components :: text(#{client.contactInfo.phone},
                'contactInfo.phone', 'v-required v-phone')}" />

Key features: * Nested field access: Uses dot notation like individualInfo.fullName * Validation classes: CSS classes for client-side validation (v-required, v-email) * Component reuse: Uses Thymeleaf fragments for consistent field rendering

Application Form Template

The application edit form shows financial data handling:

        <h2 th:text="#{application.borrowerInfo}">Borrower
            Information</h2>
        <th:block
            th:insert="~{/form/components :: select(#{participant.employment},
                'borrowerParticipant.employment', ${employmentTypes})}" />
        <th:block
            th:insert="~{/form/components :: select(#{participant.howOftenIncomeIsPaid},
                'borrowerParticipant.howOftenIncomeIsPaid', ${periodicities})}" />

        <h2 class="mt-10" th:text="#{participant.financialInfo}">Financial
            Information</h2>
        <th:block
            th:insert="~{/form/components :: amount(#{participant.totalAnnualIncome},
                'borrowerParticipant.totalAnnualIncome', 'v-required')}" />
        <th:block
            th:insert="~{/form/components :: amount(#{participant.monthlyOutgoings},
                'borrowerParticipant.monthlyOutgoings', '')}" />

Features: * Enum handling: Select dropdowns for employment and periodicities * Monetary amounts: Special amount component for financial fields * Nested participant: Access to borrowerParticipant fields

4.2. Form Component System

The platform uses Thymeleaf fragments for consistent form rendering across all forms. These components are defined in /form/components.html and provide standardized UI elements with built-in validation support.

Available Form Components
Text Input Component
~{/form/components :: text(name, fieldname, inputclass)}
Table 2. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the input field (e.g., #{client.individualInfo.fullName})

fieldname

String

Field path for data binding (e.g., 'individualInfo.fullName')

inputclass

String

CSS validation classes (e.g., 'v-required v-armenian-name')

Additional Variables
  • maxlength - Maximum character limit (default: 256)

  • minlength - Minimum character limit (default: 0)

  • placeholder - Placeholder text for the input field

Usage Example
<th:block th:insert="~{/form/components :: text(
    #{client.individualInfo.fullName},
    'individualInfo.fullName',
    'v-required v-armenian-name')}"
    th:with="maxlength = 120, placeholder = #{placeholder.fullName}" />
Select Dropdown Component
~{/form/components :: select(name, fieldname, values)}
Table 3. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the select field

fieldname

String

Field path for data binding

values

Collection/Map

Options data (Map for key-value pairs, Collection for simple lists)

Usage Example
<th:block th:insert="~{/form/components :: select(
    #{client.address.stateOfResidence},
    'individualInfo.residenceCountry',
    ${countries})}" />
Date Picker Component
~{/form/components :: date(name, fieldname, inputclass)}
Table 4. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the date field

fieldname

String

Field path for data binding

inputclass

String

CSS validation classes (optional)

Additional Variables
  • maxDate - Maximum selectable date

  • minDate - Minimum selectable date

  • startDate - Default selected date

Usage Example
<th:block th:insert="~{/form/components :: date(
    #{client.individualInfo.birthDate},
    'individualInfo.dateOfBirth',
    'v-required')}"
    th:with="maxDate = ${#dates.format(#dates.createNow())}" />
Amount/Currency Component
~{/form/components :: amount(name, fieldname, inputclass)}
Table 5. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the amount field

fieldname

String

Field path for data binding

inputclass

String

CSS validation classes (e.g., 'v-required v-positive')

Additional Variables
  • inputAmountPrefix - Prefix for field IDs (optional)

  • currencies - Available currency options

Usage Example
<th:block th:insert="~{/form/components :: amount(
    #{participant.totalAnnualIncome},
    'borrowerParticipant.totalAnnualIncome',
    'v-required v-armenian-tax-id')}" />
Checkbox Component
~{/form/components :: checkbox(name, fieldname, inputclass)}
Table 6. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the checkbox

fieldname

String

Field path for data binding

inputclass

String

CSS classes for styling/validation

Usage Example
<th:block th:insert="~{/form/components :: checkbox(
    #{client.agreeToTerms},
    'agreeToTerms',
    'v-required')}" />
Textarea Component
~{/form/components :: textarea(name, fieldname, inputclass)}
Table 7. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the textarea

fieldname

String

Field path for data binding

inputclass

String

CSS validation classes

Additional Variables
  • rows - Number of textarea rows (default: 5)

  • maxlength - Maximum character limit

Usage Example
<th:block th:insert="~{/form/components :: textarea(
    #{application.comments},
    'comments',
    'v-required')}"
    th:with="rows = 3, maxlength = 500" />
Radio Button Component
~{/form/components :: radio(name, fieldname, params)}
Table 8. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the radio group

fieldname

String

Field path for data binding

params

Map

Key-value pairs for radio options

Usage Example
<th:block th:insert="~{/form/components :: radio(
    #{client.gender},
    'gender',
    ${genderOptions})}" />
File Upload Component
~{/form/components :: fileInput(name, filelabel, fieldname, inputclass)}
Table 9. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the file input

filelabel

String

Button text for file selection

fieldname

String

Field path for data binding

inputclass

String

CSS classes for styling/validation

Usage Example
<th:block th:insert="~{/form/components :: fileInput(
    #{document.upload},
    #{button.chooseFile},
    'documentFile',
    'v-required')}" />
Read-only Component
~{/form/components :: readonly(name, fieldname, inputclass)}
Table 10. Parameters
Parameter Type Description

name

String (i18n key)

Label text for the read-only field

fieldname

String

Field path for data binding

inputclass

String

CSS classes for styling

Usage Example
<th:block th:insert="~{/form/components :: readonly(
    #{client.id},
    'id',
    '')}" />
Component Benefits

This component system ensures: * Consistency: All forms use the same styling and behavior * Maintainability: Changes to form components affect all forms * Validation Integration: Client-side validation works seamlessly * Accessibility: Standard form components ensure accessibility compliance * Internationalization: Built-in support for i18n message keys * Reusability: Components can be used across different forms and contexts

4.3. Client-Side Validation Classes

The platform provides CSS-based validation classes that integrate with jQuery Validation for client-side form validation:

Standard Validation Classes
Table 11. Built-in Validation Rules
CSS Class Description Usage Example

v-required

Field is mandatory and cannot be empty

'individualInfo.fullName', 'v-required v-name'

v-number

Field must contain a valid number

'amount', 'v-number'

v-digits

Field must contain only digits (0-9)

'quantity', 'v-digits'

v-email

Field must contain a valid email address

'contactInfo.email', 'v-required v-email'

v-url

Field must contain a valid URL

'website', 'v-url'

v-phone

Field must contain a valid phone number

'contactInfo.phone', 'v-required v-phone'

v-positive

Field must contain a positive number (> 0)

'totalAnnualIncome', 'v-required v-positive'

v-name

Field must contain valid name characters (letters, spaces, hyphens, apostrophes), max 256 characters

'individualInfo.fullName', 'v-required v-name'

Custom Validation Methods

The platform extends jQuery Validation with custom validation methods:

Custom Validators
// Armenian name validation (Armenian letters, spaces, hyphens only)
$.validator.addMethod('armenianName', function(value, element) {
  const ARMENIAN_NAME_REGEX = /^[\u0531-\u0556\u0561-\u0587\s\-']+$/;
  return this.optional(element) || ARMENIAN_NAME_REGEX.test(value);
});

// Tax identification number validation (Armenian format)
$.validator.addMethod('armenianTaxId', function(value, element) {
  const TAX_ID_REGEX = /^\d{8}$/;
  return this.optional(element) || TAX_ID_REGEX.test(value);
});

// Armenian postal code validation
$.validator.addMethod('armenianPostal', function(value, element) {
  const POSTAL_REGEX = /^\d{4}$/;
  return this.optional(element) || POSTAL_REGEX.test(value);
});
Validation Class Rules Mapping

The CSS classes are mapped to validation rules using jQuery Validation:

Class Rules Configuration
$.validator.addClassRules({
  'v-armenian-name': {armenianName: true, maxlength: 256},
  'v-armenian-tax-id': {armenianTaxId: true},
  'v-armenian-postal': {armenianPostal: true},
});
Usage in Templates

Validation classes are applied as the third parameter in form component calls:

Template Usage Examples
<!-- Required text field with name validation -->
<th:block th:insert="~{/form/components :: text(
  #{client.individualInfo.fullName},
  'individualInfo.fullName',
  'v-required v-name')}" />

<!-- Required email field -->
<th:block th:insert="~{/form/components :: text(
  #{client.contactInfo.email},
  'contactInfo.email',
  'v-required v-email')}" />

<!-- Required positive amount field -->
<th:block th:insert="~{/form/components :: amount(
  #{participant.totalAnnualIncome},
  'borrowerParticipant.totalAnnualIncome',
  'v-required v-positive')}" />
Combining Validation Classes

Multiple validation classes can be combined using space separation:

  • 'v-required v-email' - Required email field

  • 'v-required v-name' - Required name field with character validation

  • 'v-required v-positive' - Required positive number field

  • 'v-number v-positive' - Optional positive number field

5. DataSource Integration

This section describes how to implement and use DataSource interfaces for integrating external data sources into the Feature Store system.

5.1. DataSource Overview

The DataSource framework provides a standardized way to fetch and process data from external sources. It consists of two main interfaces:

  • DataSource<E> - Basic interface for fetching raw data with getData(E subject) method

  • MappedDataSource<E, T> - Extended interface that adds automatic parsing to typed objects with parseRecord(Content data) and getType() methods

When external data is unavailable or the service returns an error, implementations should throw DataUnavaliableException to indicate the data cannot be retrieved.

5.2. Implementation Example: GitHub DataSource

The GitHub DataSource demonstrates a complete implementation that fetches user data from the GitHub API.

Class Structure
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

@Service(GithubDataSource.DATASOURCE_NAME)
public class GithubDataSource implements MappedDataSource<GithubDataSourceSubject, GithubUser> {
    public static final String DATASOURCE_NAME = "github";

    private final RestTemplate restTemplate = new RestTemplate();
Data Retrieval Implementation
    }

    @Override
    public Class<GithubUser> getType() {
        return GithubUser.class;
    }

    @Override
    public Content getData(GithubDataSourceSubject subject) throws Exception {
        try {
            String url = GITHUB_API_BASE_URL + "/users/" + subject.getGithubUsername();
            ResponseEntity<byte[]> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                createHttpEntity(),
Data Parsing Implementation
            );
            return new Content(response.getBody(), MediaType.APPLICATION_JSON_VALUE) ;
        } catch (HttpClientErrorException.NotFound e) {
            throw new DataUnavaliableException("User not found: " + subject.getGithubUsername());
Subject and Target Objects

The subject object defines what data to fetch. In this example, it’s a simple wrapper for the GitHub username:

package com.timvero.example.admin.risk.github;

public interface GithubDataSourceSubject {

    String getGithubUsername();

}

The target object represents the parsed data structure:

package com.timvero.example.admin.risk.github;

import com.fasterxml.jackson.annotation.JsonProperty;

public class GithubUser {
    private String login;
    private String name;
    @JsonProperty("followers")
    private int followersCount;
    @JsonProperty("following")
    private int followingCount;
    @JsonProperty("public_repos")
    private int publicRepos;
    @JsonProperty("avatar_url")
    private String avatarUrl;
In the platform, Participant entity implements the DataSource Subject pattern and can be used directly as a subject for various data sources.

6. Document Management

This section describes how to implement document management functionality for entities in the platform, including document type associations and UI integration.

6.1. Document System Overview

The document management system allows entities to have associated documents with configurable upload and requirement rules. The system consists of:

  • HasDocuments - Interface marking entities that can have documents

  • DocumentTypeAssociation - Configuration for document types per entity

  • EntityDocumentTabController - UI integration for document management tabs

6.2. Document Type Configuration

Document types are configured using DocumentTypeAssociation with a builder pattern that allows defining uploadable and required document types with conditional logic.

Document Type Associations
Required Document Configuration

Documents that must be uploaded based on entity conditions:

    public static final EntityDocumentType OTHER = new EntityDocumentType("OTHER");
    public static final EntityDocumentType ID_SCAN = new EntityDocumentType("ID_SCAN");

    private static final Predicate<Participant> PARTICIPANT_GUARANTOR =
        participant -> participant.getRoles().contains(ParticipantRole.GUARANTOR);
    private static final Predicate<Participant> PARTICIPANT_BORROWER =
        participant -> participant.getRoles().contains(ParticipantRole.BORROWER);

    @Bean
    DocumentTypeAssociation<Participant> idScanDocumentTypeAssociations() {
        return DocumentTypeAssociation.forEntityClass(Participant.class).required(ID_SCAN)

This configuration: * Makes ID_SCAN document required * Only applies when participant status is NEW * Only applies to participants with GUARANTOR or BORROWER roles

Optional Document Configuration

Documents that can be uploaded without restrictions:

            .predicate(PARTICIPANT_GUARANTOR.or(PARTICIPANT_BORROWER)).build();
    }

    @Bean

This allows OTHER document type to be uploaded for any participant without conditions.

Document Type Definitions

Document types are defined as constants in the configuration:

    public static final SignableDocumentType APPLICATION_CONTRACT =
        new SignableDocumentType("APPLICATION_CONTRACT", ApplicationContractDocumentCategory.TYPE);

6.3. UI Integration

Document Tab Implementation

To display document management interface, create a tab controller extending EntityDocumentTabController:

package com.timvero.example.admin.participant.tab;

import com.timvero.example.admin.participant.entity.Participant;
import com.timvero.web.common.tab.EntityDocumentTabController;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Controller;

@Controller
@Order(1500)
public class ParticipantDocumentsTab extends EntityDocumentTabController<Participant> {

    @Override
    public boolean isVisible(Participant entity) {
        return true;
    }
}

Key features: * @Order(1500) - Controls tab display order in the UI * isVisible() - Determines when the tab should be shown * Automatic functionality - Upload, download, and delete operations are handled automatically

Entity Requirements

Entities must implement the HasDocuments interface:

public interface HasDocuments extends Persistable<UUID> {
    // No additional methods required
}

6.4. Builder Pattern Usage

The DocumentTypeAssociation uses a fluent builder pattern:

Available Methods
  • uploadable(EntityDocumentType) - Adds a document type that can be uploaded

  • required(EntityDocumentType) - Adds a document type that must be uploaded

  • predicate(Predicate<E>) - Adds conditional logic for when the association applies

Predicate Chaining

Multiple predicates can be combined:

        new SignableDocumentType("APPLICATION_CONTRACT", ApplicationContractDocumentCategory.TYPE);

    public static final EntityDocumentType OTHER = new EntityDocumentType("OTHER");
    public static final EntityDocumentType ID_SCAN = new EntityDocumentType("ID_SCAN");

Predicates are combined using AND logic - all conditions must be true.

6.5. Complete Implementation Example

To implement document management for an entity:

  1. Entity implements HasDocuments:

@Entity
public class Participant implements HasDocuments {
    // Entity implementation
}
  1. Create document type configuration:

@Configuration
public class ParticipantDocumentTypesConfiguration {

    @Bean
    DocumentTypeAssociation<Participant> requiredDocuments() {
        return DocumentTypeAssociation.forEntityClass(Participant.class)
            .required(ID_SCAN)
            .predicate(participant -> participant.getStatus() == ParticipantStatus.NEW)
            .build();
    }

    @Bean
    DocumentTypeAssociation<Participant> optionalDocuments() {
        return DocumentTypeAssociation.forEntityClass(Participant.class)
            .uploadable(OTHER)
            .build();
    }
}
  1. Create document tab controller:

@Controller
@Order(1500)
public class ParticipantDocumentsTab extends EntityDocumentTabController<Participant> {

    @Override
    public boolean isVisible(Participant entity) {
        return true; // Always show documents tab
    }
}

This provides a complete document management system with conditional requirements and integrated UI.

7. Entity Checkers setup and usage

This section describes how to set up and manage Entity Checkers for automated business rule validation and state management in the application.

7.1. Checker System Architecture

The platform uses Entity Checkers to implement event-driven business logic that automatically responds to entity changes. Checkers serve as reactive components that monitor database changes and execute business rules when specific conditions are met.

What are Entity Checkers?

Entity Checkers are specialized components that:

  • Monitor Entity Changes: Automatically detect when entities are created, updated, or deleted

  • Apply Business Rules: Execute predefined logic when specific conditions are satisfied

  • Maintain Data Consistency: Ensure related entities remain synchronized

  • Automate Workflows: Trigger next steps in business processes without manual intervention

Checker Class Hierarchy

The application uses a structured checker architecture based on framework components:

  • Base Checker Classes: Framework-provided abstract classes (EntityChecker) that handle the infrastructure

  • Custom Entity Checkers: Application-specific implementations:

    • BorrowerStartTreeChecker - manages borrower workflow initiation

Each checker consists of three main parts:

  • Listener Registration (registerListeners method) - defines what entity changes to monitor

  • Availability Check (isAvailable method) - determines when the checker should execute

  • Business Logic (perform method) - implements the actual business rule

7.2. Listener Registration

The registerListeners method is the core mechanism for defining what entity changes a checker should monitor. This method uses the CheckerListenerRegistry to configure event listeners that will trigger the checker’s business logic.

CheckerListenerRegistry

CheckerListenerRegistry<E> is a registry for configuring event listeners for entity changes. The generic type E represents the target entity type that the checker operates on (e.g., Application, Participant).

Key Methods
Method Parameters Description

entityChange

Class<T> entityClass - Type of entity being monitored
Function<T, E> mapper - Function to convert changed entity to target entity

Creates a listener for entity change events. See usage examples.

entityChange

(no parameters)

Monitors the same entity type as the checker (shorthand). See usage examples.

updated

String…​ fields - Names of fields to monitor for changes

Filters to field update events only.

inserted

(no parameters)

Filters to entity insertion events only.

and

Predicate<T> predicate - Custom condition that must be satisfied

Adds filtering conditions. Chain multiple conditions together.

Using entityChange Method

The entityChange method provides flexible entity-to-target mapping capabilities to solve different monitoring scenarios in checker implementations.

Direct Entity Monitoring

Problem: Your checker needs to monitor changes to the same entity type it operates on.

For example, an ApplicationChecker that monitors Application entity changes directly.

Solution:

registry.entityChange().updated("status")

This monitors changes to the same entity type as the checker operates on.

When to Use
  • Simple checkers where trigger entity = target entity

  • Direct field monitoring without complex relationships

  • Most straightforward monitoring scenario

Related Entity Monitoring

Problem: Your checker needs to monitor changes to different entity types and map them to the target entity through relationships.

For example, an ApplicationChecker that triggers when related Participant entities change, but needs to operate on the Application.

Solution:

registry.entityChange(Participant.class, Participant::getApplication).updated("status")
  • Participant.class - the entity type being monitored for changes

  • Participant::getApplication - function that converts the changed Participant to the target Application

When to Use
  • Monitoring entities connected through direct JPA relationships

  • One-to-one or many-to-one relationships with getter methods

  • Related entities share the same transaction context

Complex Repository-Based Mapping

Problem: Your checker needs to monitor entities where the relationship requires repository lookup to resolve the target entity.

For example, monitoring SignableDocument changes but needing to operate on the associated Participant through a complex relationship.

Solution:

registry.entityChange(SignableDocument.class, d -> participantRepository.getReferenceById(d.getOwnerId()))
    .updated("status")
  • SignableDocument.class - the entity type being monitored

  • d → participantRepository.getReferenceById(d.getOwnerId()) - repository-based mapping function

When to Use
  • Complex relationships not modeled as direct JPA associations

  • Cross-context entity lookups

  • Dynamic relationship resolution based on entity state

Performance Consideration: Repository-based mapping introduces additional database queries. Use this approach only when direct relationship mapping is not possible.

7.3. Checker Implementation Examples

BorrowerStartTreeChecker

The BorrowerStartTreeChecker manages borrower workflow initiation when participants complete required documentation.

Table 12. Checker Overview
Component Description

Target Entity

Participant - The entity this checker operates on

Purpose

Automatically start decision tree process when borrower completes all requirements

Triggers

Document signatures and uploads for required participant documents

Business Logic

Update participant status to IN_PROCESS and start automated decision tree

Listener 1: Application Form Signature Monitor

Monitors when application forms are signed and triggers workflow initiation.

Purpose: Track completion of application form signatures
Trigger: SignableDocument.status changes to SIGNED for application forms
Target Resolution: Maps document changes to owning participant

        registry.entityChange(SignableDocument.class, d -> participantRepository.getReferenceById(d.getOwnerId()))
            .updated(SignableDocument_.STATUS)
            .and(d -> d.getStatus() == SignatureStatus.SIGNED && d.getDocumentType() == ParticipantDocumentTypesConfiguration.APPLICATION_FORM);
Listener 2: Required Document Upload Monitor

Tracks when required documents are uploaded to the system.

Purpose: Monitor completion of required document uploads
Trigger: New EntityDocument insertions for required document types
Target Resolution: Maps document uploads to owning participant

        registry.entityChange(EntityDocument.class, d -> participantRepository.getReferenceById(d.getOwnerId()))
            .inserted().and(d -> {
                Participant participant = participantRepository.getReferenceById(d.getOwnerId());
                return documentService.getRequiredDocumentTypes(participant).contains(d.getDocumentType());
            });
Availability Check

Determines when the checker should execute its business logic.

    @Override
    protected boolean isAvailable(Participant participant) {
        return needSignature(participant) && hasSignature(participant);
    }
Availability Conditions
  • Participant must be a BORROWER

  • Participant status must be NEW

  • Application form must be signed

  • All required documents must be uploaded

Business Logic

Updates participant status and initiates the decision tree process.

    @Override
    protected void perform(Participant participant) {
        participant.setStatus(ParticipantStatus.IN_PROCESS);
        decisionProcessStarter.start(PARTICIPANT_TREE, participant.getId());
    }
Business Operations
  1. Status Update: Change participant status from NEW to IN_PROCESS

  2. Process Initiation: Start the automated decision tree workflow

  3. Transaction Safety: Both operations occur within the same transaction

8. Credit Management System

This section describes how to implement and manage credit entities using the loan servicing framework. The ExampleCredit implementation demonstrates a complete loan management system, but the underlying loan module provides flexible components for implementing various lending products.

8.1. Credit System Architecture

The credit management system is built on the loan servicing framework, which provides core components for any type of lending product. The ExampleCredit serves as a reference implementation, but you can create different credit types for various lending scenarios.

Core Components

The loan module (com.timvero.servicing) provides the foundation:

  • Credit - Base entity class for all lending products

  • CreditSnapshot - Point-in-time credit state management

  • CreditOperation - Base class for all credit operations (payments, charges, etc.)

  • Debt - Flexible debt structure with account-based balances

  • CreditCalculationService - Core calculation engine

  • CreditPaymentService - Payment processing infrastructure

ExampleCredit Implementation

The ExampleCredit demonstrates a complete consumer loan implementation:

@Entity
@DiscriminatorValue("1")
public class ExampleCredit extends Credit implements NamedEntity {

    @NotNull
    @OneToOne(fetch = FetchType.EAGER)
    @JoinColumn(nullable = false)
    private Application application;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "condition")
    @Fetch(FetchMode.JOIN)
    private ExampleCreditCondition condition;

    public Application getApplication() {
        return application;
    }

    public void setApplication(Application application) {
        this.application = application;
    }

    public ExampleCreditCondition getCondition() {
        return condition;
    }

    public void setCondition(ExampleCreditCondition condition) {
        this.condition = condition;
    }

    @Override
    public String getDisplayedName() {
        return "Loan for " + getApplication().getBorrowerParticipant().getDisplayedName();
    }

    @Transient
    public LocalDate getMaturityDate() {
        return getStartDate().plus(getCondition().getPeriod().multipliedBy(getCondition().getTerm()));
    }

Key features: * Discriminator Value: "1" identifies this credit type in the database * Application Integration: Links to loan application and borrower * Condition Management: Contains loan terms (principal, term, interest rate) * Maturity Calculation: Automatic calculation based on start date and terms

8.2. Credit Entity Setup

Creating Custom Credit Types

To implement different lending products, extend the base Credit class:

@Entity
@DiscriminatorValue("2")  // Unique identifier for this credit type
public class MortgageCredit extends Credit implements NamedEntity {

    @OneToOne(fetch = FetchType.EAGER)
    @JoinColumn(nullable = false)
    private PropertyApplication application;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "mortgage_condition")
    private MortgageCondition condition;

    @Override
    public String getDisplayedName() {
        return "Mortgage for " + getApplication().getPropertyAddress();
    }

    @Transient
    public LocalDate getMaturityDate() {
        return getStartDate().plus(getCondition().getTerm());
    }
}
Credit Condition Configuration

Define loan terms and conditions specific to your credit type:

@Entity
public class ExampleCreditCondition extends BasePersistable<UUID> {

    @Embedded
    @CompositeType(MonetaryAmountType.class)
    private MonetaryAmount principal;

    @Column(nullable = false)
    private Period period;  // Payment frequency (monthly, weekly, etc.)

    @Column(nullable = false)
    private Integer term;   // Number of payment periods

    @Column(nullable = false)
    private BigDecimal interestRate;

    // getters and setters...
}
Database Schema Generation

The platform automatically generates SQL migrations for credit entities:

-- Generated migration for ExampleCredit
CREATE TABLE credit (
    id UUID PRIMARY KEY,
    credit_type INTEGER NOT NULL,  -- Discriminator column
    start_date DATE NOT NULL,
    calculation_date DATE,
    actual_snapshot BIGINT,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    -- ExampleCredit specific columns
    application UUID NOT NULL,
    condition UUID
);

8.3. Credit Operations Framework

Built-in Operation Types

The loan module provides standard operation types that work with any credit implementation:

Payment Operations
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
public class ExampleCreditPayment extends CreditPayment {

    public static Integer TYPE = 200;

    public ExampleCreditPayment(LocalDate date, MonetaryAmount amount) {
        super(TYPE, date, OperationStatus.APPROVED, amount);
    }

    protected ExampleCreditPayment() {
    }

    @Override
    public Integer getType() {
        return TYPE;
    }

    @Override
    public int getOrder() {
        return 200;
    }

Custom payment types can be created by extending CreditPayment:

@Entity
@DiscriminatorValue("201")
public class MortgagePayment extends CreditPayment {

    public static Integer TYPE = 201;

    public MortgagePayment(LocalDate date, MonetaryAmount amount) {
        super(TYPE, date, OperationStatus.APPROVED, amount);
    }

    @Override
    public Integer getType() {
        return TYPE;
    }
}
Charge Operations

Implement fees and charges specific to your credit type:

@Entity
@DiscriminatorValue("301")
public class OriginationFeeCharge extends CreditOperation {

    public static Integer TYPE = 301;

    @Embedded
    @CompositeType(MonetaryAmountType.class)
    private MonetaryAmount amount;

    public OriginationFeeCharge(LocalDate date, MonetaryAmount amount) {
        super(TYPE, date, OperationStatus.APPROVED);
        this.amount = amount;
    }

    @Override
    public boolean isEndDayOperation() {
        return false;
    }
}
Accrual Operations

Interest and fee accruals are handled by the calculation engine:

@Entity
@DiscriminatorValue("401")
public class InterestAccrual extends CreditOperation {

    @Embedded
    @CompositeType(MonetaryAmountType.class)
    private MonetaryAmount accruedAmount;

    @Column(nullable = false)
    private String accountType;  // INTEREST, LATE_FEE, etc.

    @Override
    public boolean isEndDayOperation() {
        return true;  // Accruals typically run at end of day
    }
}
Credit Action Controllers

Actions provide user interface operations for credit management:

Payment Registration
@Controller
@RequestMapping("/register-payment")
public class RegisterPaymentAction extends EntityActionController<UUID, ExampleCredit, ManualTransferForm> {

    @Autowired
    private BorrowerTransactionService borrowerTransactionService;


    public static final Long OTHER = 0L;

    @Override
    protected EntityAction<? super ExampleCredit, ManualTransferForm> action() {
        return when(c -> c.getActualSnapshot() != null && c.getActualSnapshot().getStatus().equals(ACTIVE))
            .then((c, f, u) -> {
                LiquidityClientPaymentMethod paymentMethod =
                    new LiquidityClientPaymentMethod(f.getProcessedDate(), f.getAmount(), TransactionType.INCOMING,
                        c.getApplication().getBorrowerParticipant().getClient().getIndividualInfo().getFullName());
                borrowerTransactionService.proceedCustom(c, TransactionType.INCOMING, paymentMethod,
                    paymentMethod.getAmount(), true, f.getDescription());
            });

The action uses a form to collect payment details:

    public static class ManualTransferForm {

        @NotNull
        @Positive
        private MonetaryAmount amount;

        @NotNull
        @DateTimeFormat(pattern = PATTERN_DATEPICKER_FORMAT)
        private LocalDate processedDate;

        private String description;

        public MonetaryAmount getAmount() {
            return amount;
        }

        public void setAmount(MonetaryAmount amount) {
            this.amount = amount;
        }

        public LocalDate getProcessedDate() {
            return processedDate;
        }

        public void setProcessedDate(LocalDate processedDate) {
            this.processedDate = processedDate;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }
    }
Disbursement Processing
@Controller
@RequestMapping("/register-disbursement")
public class RegisterDisbursementAction extends EntityActionController<UUID, ExampleCredit, DisbursementForm> {

    @Override
    protected EntityAction<? super ExampleCredit, DisbursementForm> action() {
        return when(c -> c.getActualSnapshot().getStatus().equals(APPROVED))
            .then((c, f, u) -> {
                // Create disbursement transaction
                disbursementService.processDisbursement(c, f.getAmount(), f.getMethod());
            });
    }
}
Transaction Processing Integration

The credit system integrates with the transaction processing framework:

    @Autowired
    private PaymentTransactionService transactionService;
    @Autowired
    private CoreCreditRepository creditRepository;
    @Autowired
    private PaymentTransactionRepository transactionRepository;
    @Autowired
    private BorrowerTransactionRepository borrowerTransactionRepository;
    @Autowired
    private CreditPaymentService paymentService;
    @Autowired
    private ChargeOperationService chargeOperationService;

    @Transactional(propagation = Propagation.MANDATORY)
    public void proceedCustom(ExampleCredit credit, TransactionType type, PaymentMethod paymentMethod,
        MonetaryAmount amount, boolean sync, String description) {
        BorrowerTransaction transaction = new BorrowerTransaction(type, amount, paymentMethod, credit);

        transaction.setStatus(TransactionStatus.READY_FOR_EXECUTION);
        transaction.setDescription(description);

Transaction processing flow: 1. Transaction Creation: BorrowerTransaction created with payment details 2. Payment Gateway: Transaction sent to payment processor 3. Success Handling: On success, creates CreditPayment operation 4. Calculation Trigger: Credit calculation engine updates balances 5. Snapshot Update: New credit state snapshot created

8.4. Credit Calculation Engine

Debt Structure

The loan module uses a flexible debt structure with named accounts:

// Account type constants for ExampleCredit
public static final String PRINCIPAL = "PRINCIPAL";
public static final String INTEREST = "INTEREST";
public static final String LATE_FEE = "LATE_FEE";
public static final String PAST_DUE_PRINCIPAL = "PAST_DUE_PRINCIPAL";
public static final String PAST_DUE_INTEREST = "PAST_DUE_INTEREST";

For different credit types, define appropriate account structures:

// Mortgage-specific accounts
public static final String PRINCIPAL = "PRINCIPAL";
public static final String INTEREST = "INTEREST";
public static final String ESCROW = "ESCROW";
public static final String PMI = "PMI";
public static final String PROPERTY_TAX = "PROPERTY_TAX";
Calculation Service Integration

The calculation engine automatically processes credit operations:

// Triggered after payment registration
creditCalculationService.calculate(creditId, paymentDate, currentCalculationDate);

This recalculates: * Debt Balances: Updates account balances based on payment distribution * Interest Accruals: Calculates daily interest charges * Past Due Amounts: Moves overdue balances to past due accounts * Credit Status: Updates credit status based on payment history

Snapshot Management

Credit snapshots provide point-in-time state tracking:

// Credit snapshot contains:
public class CreditSnapshot {
    private LocalDate date;           // Snapshot date
    private CreditStatus status;      // Credit status at this date
    private Debt debt;               // Account balances at this date
}

8.5. Credit User Interface

Controller Implementation
@RequestMapping(value = ExampleCreditController.PATH)
@MenuItem(order = 5_300, name = "credit")
public class ExampleCreditController extends ViewableFilterController<UUID, ExampleCredit, ExampleCreditFilter> {

    public static final String PATH = "/credit";

    @Override
    protected String getHeaderPage() {
        return "/credit/header";

The controller provides: * List View: Paginated credit listing with filtering * Detail View: Comprehensive credit information display * Action Buttons: Context-sensitive operations based on credit status

Credit Filtering
public class ExampleCreditFilter extends ListFilter {

    @Field(restriction = Restriction.IN, value = ExampleCredit_.ACTUAL_SNAPSHOT + "." + CreditSnapshot_.STATUS)
    private CreditStatus[] status;

    public CreditStatus[] getStatus() {
        return status;
    }

Filtering capabilities: * Status Filtering: Filter by credit status (ACTIVE, CLOSED, etc.) * Date Ranges: Filter by creation date, maturity date, etc. * Amount Ranges: Filter by principal amount, current balance * Custom Filters: Add business-specific filtering criteria

Tab-Based Interface

Credit details are organized into tabs for better user experience:

// Credit data tab showing balances and payment history
@Controller
@Order(1000)
public class CreditDataTab extends EntityTabController<UUID, ExampleCredit> {

    @Override
    protected String getTabTemplate(UUID id, Model model) throws Exception {
        ExampleCredit credit = loadEntity(id);

        // Add credit summary data
        model.addAttribute("currentBalance", credit.getActualSnapshot().getDebt());
        model.addAttribute("paymentHistory", getPaymentHistory(credit));
        model.addAttribute("nextPaymentDate", calculateNextPaymentDate(credit));

        return super.getTabTemplate(id, model);
    }
}

Available tabs: * Credit Data: Current balances, payment status, maturity information * Payments: Payment history and upcoming payment schedule * Transactions: All financial transactions related to the credit * Calculations: Detailed calculation history and interest computations * Documents: Credit-related documents and contracts

Form Components for Credit Operations

Credit forms use the platform’s form component system:

<!-- Payment amount input -->
<th:block th:insert="~{/form/components :: amount(
    #{credit.payment.amount},
    'amount',
    'v-required v-positive')}"
    th:with="currencies = ${currencies}" />

<!-- Payment date picker -->
<th:block th:insert="~{/form/components :: date(
    #{credit.payment.date},
    'processedDate',
    'v-required')}"
    th:with="minDate = ${minDate}" />

<!-- Payment description -->
<th:block th:insert="~{/form/components :: textarea(
    #{credit.payment.description},
    'description',
    '')}"
    th:with="rows = 3" />

8.6. Credit Business Rules with Entity Checkers

Automated Credit Monitoring

Entity checkers can implement automated credit monitoring:

@Component
public class CreditPaymentChecker extends EntityChecker<ExampleCredit> {

    @Override
    protected void registerListeners(CheckerListenerRegistry<ExampleCredit> registry) {
        // Monitor payment operations
        registry.entityChange(CreditPayment.class, payment ->
            creditRepository.findByOperationsIn(payment))
            .inserted();
    }

    @Override
    protected boolean isAvailable(ExampleCredit credit) {
        return credit.getActualSnapshot().getStatus().equals(ACTIVE);
    }

    @Override
    protected void perform(ExampleCredit credit) {
        // Check if credit is now current after payment
        if (isPaidCurrent(credit)) {
            updateCreditStatus(credit, CURRENT);
            sendPaymentConfirmation(credit);
        }
    }
}
Past Due Processing

Automated past due detection and processing:

@Component
public class PastDueChecker extends EntityChecker<ExampleCredit> {

    @Override
    protected void registerListeners(CheckerListenerRegistry<ExampleCredit> registry) {
        // Monitor daily calculation updates
        registry.entityChange().updated("calculationDate");
    }

    @Override
    protected boolean isAvailable(ExampleCredit credit) {
        return isPastDue(credit) && !isAlreadyMarkedPastDue(credit);
    }

    @Override
    protected void perform(ExampleCredit credit) {
        // Create past due operation
        PastDueOperation pastDue = new PastDueOperation(
            LocalDate.now(),
            calculatePastDueAmount(credit)
        );

        credit.getOperations().add(pastDue);

        // Send past due notification
        notificationService.sendPastDueNotice(credit);
    }
}

8.7. Extending the Credit System

Creating New Credit Types

To implement different lending products:

  1. Extend Credit Entity: Create new entity with specific discriminator value

  2. Define Condition Structure: Create condition entity with product-specific terms

  3. Implement Operations: Create custom payment and charge operation types

  4. Configure Account Types: Define debt account structure for the product

  5. Create Controllers: Implement UI controllers and actions

  6. Add Business Rules: Implement entity checkers for automated processing

Integration with External Systems

The credit system supports integration with external services:

  • Payment Gateways: Transaction processing through payment providers

  • Credit Bureaus: Credit reporting and monitoring

  • Core Banking: Integration with banking systems

  • Document Management: Contract and document storage

  • Notification Services: Email and SMS notifications

Customization Examples
Auto Loan Implementation
@Entity
@DiscriminatorValue("3")
public class AutoLoan extends Credit {

    @OneToOne
    private VehicleApplication application;

    @OneToOne
    private AutoLoanCondition condition;

    @Column
    private String vehicleVin;

    @Column
    private MonetaryAmount vehicleValue;
}
Credit Card Implementation
@Entity
@DiscriminatorValue("4")
public class CreditCard extends Credit {

    @Column
    private MonetaryAmount creditLimit;

    @Column
    private BigDecimal aprRate;

    @Column
    private MonetaryAmount availableCredit;

    @Column
    private LocalDate statementDate;
}

8.8. Best Practices

Credit Design Principles
  • Separation of Concerns: Keep business logic in services, not entities

  • Event-Driven Architecture: Use entity checkers for automated processing

  • Flexible Debt Structure: Design account types to accommodate future requirements

  • Audit Trail: Leverage built-in auditing for compliance requirements

  • Transaction Safety: Ensure operations are atomic and consistent

Performance Considerations
  • Calculation Optimization: Batch credit calculations during off-peak hours

  • Snapshot Management: Archive old snapshots to maintain performance

  • Index Strategy: Create appropriate database indexes for credit queries

  • Lazy Loading: Use lazy loading for large collections and related entities

Security and Compliance
  • Data Protection: Implement field-level encryption for sensitive data

  • Access Control: Use role-based security for credit operations

  • Audit Logging: Maintain comprehensive audit trails for regulatory compliance

  • Data Retention: Implement data retention policies for closed credits

The credit management system provides a robust foundation for implementing various lending products while maintaining consistency, auditability, and extensibility across different credit types.

9. Credit Operations Framework

The Credit Operations Framework provides a powerful, extensible system for managing financial operations throughout the credit lifecycle. This framework handles everything from loan disbursements and payments to interest accruals and account closures, with full audit trails and automated processing capabilities.

The example project demonstrates one possible implementation of credit operations. Real-world implementations may differ significantly based on business requirements, regulatory needs, and the types of financial products being offered.

9.1. Operations Architecture Overview

The operations framework is built on a flexible, event-driven architecture that separates operation definitions from their processing logic, enabling customization for various lending products and business models.

Core Components

The loan servicing module (com.timvero.servicing) provides the foundation:

  • CreditOperation - Base entity for all credit operations

  • CreditOperationHandler<O> - Interface for operation processing logic

  • Snapshot - Point-in-time credit state representation

  • AccrualEngine - Interface for time-based calculations

  • PreCalculateSynchronizer - Interface for operation synchronization

  • CreditCalculationService - Core calculation engine that processes operations

9.2. Operation Processing Flow

The credit calculation system processes operations through a sophisticated pipeline that ensures correct chronological execution and state management.

The Calculation Pipeline

When CreditCalculationService.calculate() is called, the system follows this flow:

  1. Synchronization Phase: PreCalculateSynchronizer implementations (like AccrualOperationService) ensure all necessary operations exist

  2. Date Range Processing: The system processes each date from the start date to the target date

  3. Daily Operation Processing: For each date, operations are sorted by their getOrder() value and processed sequentially

  4. Snapshot Creation: After processing all operations for a date, a CreditSnapshot is created and stored

  5. State Updates: The credit’s actual snapshot and calculation date are updated

Operation Processing Order

Operations are processed in a specific order determined by their getOrder() method. Lower numbers execute first:

Order Operation Type Example Type Code Purpose

101

Charge Operations

901

Add fees, penalties, or other charges to the credit

111

Accrual Operations

950

Calculate and apply interest, late fees, and other time-based charges

200

Payment Operations

200

Process borrower payments and apply to debt balances

900

Past Due Operations

900

Move current debt to past due status when payments are missed

995

Void Operations

995

Cancel or void credit operations

999

Close Operations

999

Close and finalize credit accounts

End-of-Day vs Intraday Operations

Each operation implements isEndDayOperation() to control when during the day it should be processed:

  • isEndDayOperation() = false: Intraday operations - processed immediately when encountered

  • isEndDayOperation() = true: End-of-day operations - processed only when the date is "closed"

This distinction is crucial for business logic:

Intraday Operations (Most Operations)
@Override
public boolean isEndDayOperation() {
    return false; // Process immediately
}
  • Charges - Applied immediately when created

  • Payments - Processed as soon as received

  • Accruals - Applied when calculation runs

End-of-Day Operations (Special Cases)
@Override
public boolean isEndDayOperation() {
    return true; // Process only at end of day
}
  • Past Due Operations - Only processed when the day is "closed" to ensure all payments for that day have been received

This design prevents premature past due processing if a payment arrives later in the same business day.

Operation Handler Execution

Within each date, the OperationProcessor handles individual operations:

  1. Handler Discovery: The system finds the appropriate CreditOperationHandler for each operation type

  2. Snapshot Application: Each handler modifies the current Snapshot to reflect the operation’s effect

  3. Debt Tracking: The system tracks how each operation changes the debt balances

  4. State Management: Operations can modify both debt balances and credit status

Transaction and Locking

The calculation service uses sophisticated transaction management:

  • Pessimistic Locking: Credits are locked during calculation to prevent concurrent modifications

  • Separate Transactions: Synchronization and calculation run in separate transactions

  • Event Publishing: Status changes and snapshot updates trigger events for other system components

9.3. Operation Entity Structure

Base Operation Entity

All credit operations extend the base CreditOperation class:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "operation_type", discriminatorType = DiscriminatorType.INTEGER)
public abstract class CreditOperation extends AbstractAuditable<UUID> {

    @Column(nullable = false)
    private Integer type;

    @Column(nullable = false)
    private LocalDate date;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OperationStatus status;

    // Abstract methods that subclasses must implement
    public abstract boolean isEndDayOperation();
    public abstract int getOrder();
}

Key features: * Single Table Inheritance: All operations stored in one table with discriminator * Type Identification: Each operation type has a unique integer identifier * Date-based Processing: Operations are tied to specific business dates * Status Management: Operations can be APPROVED, CANCELED, DECLINED, or PENDING * Audit Trail: Full change history via AbstractAuditable

Operation Status Lifecycle

Operations follow a defined status lifecycle:

PENDING → APPROVED → (Applied to Credit)
    ↓
DECLINED → (Rejected, not applied)
    ↓
CANCELED → (Removed from processing)

9.4. Implementing Custom Operations

To implement custom operations, follow the same patterns demonstrated in the example project. You’ll need to:

  1. Create the Operation Entity - Extend CreditOperation with your specific fields and behavior

  2. Implement the Operation Handler - Create a service that implements CreditOperationHandler<YourOperation>

  3. Configure the Handler - Add the handler as a Spring bean in your configuration

  4. Define Processing Order - Set appropriate getOrder() and isEndDayOperation() values

The example project’s ChargeOperation and ChargeOperationService demonstrate the simplest implementation pattern, while AccrualOperation and AccrualOperationService show more complex synchronization behavior.

9.5. Standard Operation Types

The example project demonstrates key operation types that represent the core concepts of credit operations.

Charge Operations

Charge operations represent the simplest operation type - they add a monetary amount to a credit account.

@Entity
@DiscriminatorValue("901")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
public class ChargeOperation extends CreditOperation {

    public static Integer TYPE = 901;

    @Embedded
    @NotNull
    private MonetaryAmount amount;

    protected ChargeOperation() {
        super();
    }

    public ChargeOperation(LocalDate date, MonetaryAmount amount) {
        super(TYPE, date, OperationStatus.APPROVED);
        this.amount = amount;
    }

    public MonetaryAmount getAmount() {
        return amount;
    }

    @Override
    public boolean isEndDayOperation() {
        return false;
    }

    @Override
    public int getOrder() {
        return 101;
    }
}

Key characteristics: * Type Code: 901 - Unique identifier for database discrimination * Order: 101 - Early processing order to apply charges before other operations * Monetary Amount: Embedded amount to be added to the debt

Payment Operations

Payment operations process borrower payments and distribute them across debt accounts according to business rules.

@Entity
@DiscriminatorValue("200")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
public class ExampleCreditPayment extends CreditPayment {

    public static Integer TYPE = 200;

    public ExampleCreditPayment(LocalDate date, MonetaryAmount amount) {
        super(TYPE, date, OperationStatus.APPROVED, amount);
    }

    protected ExampleCreditPayment() {
    }

    @Override
    public Integer getType() {
        return TYPE;
    }

    @Override
    public int getOrder() {
        return 200;
    }
}

The payment distribution order is configured in CreditCalculationConfiguration:

    @Bean
    CreditPaymentOperationHandler<ExampleCreditPayment> creditPaymentOperationHandler() {
        return new CreditPaymentOperationHandler<>(OVERPAYMENT, List.of(PAST_DUE_PRINCIPAL, PAST_DUE_INTEREST,
            LATE_FEE, INTEREST, PRINCIPAL)) {};
    }

This defines the payment waterfall: past due amounts first, then fees, current interest, and finally principal. Overpayments are credited to the OVERPAYMENT account.

Accrual Operations

Accrual operations represent a sophisticated concept for calculating time-based charges like interest and late fees. Unlike other operations that are created manually, accrual operations are automatically synchronized with significant credit events.

The Accrual Concept

The key insight behind accrual operations is that interest and fees need to be calculated whenever the debt balance changes. The system automatically creates accrual operations on dates when:

  • Payments are made - Interest must be calculated up to the payment date

  • Past due events occur - Balances move to past due status, affecting future calculations

This synchronization is handled by the AccrualOperationService which implements PreCalculateSynchronizer. It scans for payment and past due operations and ensures corresponding accrual operations exist for those dates.

Accrual Engines

The actual calculation logic is delegated to specialized accrual engines:

  • InterestAccrualEngine - Calculates interest on the PRINCIPAL balance using the credit’s interest rate

  • LateFeeAccrualEngine - Calculates late fees on past due principal and interest using the late fee rate

These engines extend BasisAccrualEngine which provides sophisticated day-count calculations, handling rate changes over time, and pro-rated calculations between significant dates.

How It Works
  1. Event Detection: When payments or past due operations are processed, the synchronizer identifies dates needing accrual calculations

  2. Accrual Creation: AccrualOperation entities are automatically created for these dates

  3. Engine Calculation: During credit calculation, accrual engines compute the exact amounts based on outstanding balances, rates, and time periods

  4. Balance Application: The calculated accruals are added to the appropriate debt accounts (INTEREST, LATE_FEE, etc.)

This design ensures that time-based charges are accurately calculated and applied, even when payments are made on irregular dates or when credit terms change over time.

Past Due Operations

Past due operations handle one of the most critical business processes in lending - managing overdue debt. When borrowers miss scheduled payments, the system must reorganize debt balances to reflect the new risk profile and enable different treatment of overdue amounts.

The Past Due Concept

The fundamental principle is that overdue debt behaves differently from current debt:

  • Late fees accrue only on past due balances, not current balances

  • Collection processes target past due amounts with different strategies

  • Reporting and risk assessment treat past due debt as higher risk

  • Payment distribution prioritizes past due amounts over current debt

When a scheduled payment date passes without sufficient payment, current debt must be "moved" to past due accounts to enable this differentiated treatment.

Scheduled Payment Detection

Past due operations are triggered by the credit’s payment schedule, which is defined in the credit condition. The system monitors for:

  • Regular payment dates - monthly, weekly, or other periodic payments based on the credit terms

  • Maturity date - the final payment date when the entire remaining balance becomes due

  • Missed payment amounts - comparing expected payments against actual payments received

Debt Movement Logic

When a past due event occurs, the operation performs account transfers:

  • INTEREST balance → PAST_DUE_INTEREST account

  • PRINCIPAL balance → PAST_DUE_PRINCIPAL account

This reorganization enables the accrual engines to calculate late fees specifically on past due amounts, while new interest continues to accrue on any remaining current principal.

Maturity vs Regular Past Due

The maturity flag in the operation distinguishes between two scenarios:

  • Regular Past Due: Borrower missed a scheduled payment but loan hasn’t matured - partial amounts may move to past due

  • Maturity Past Due: Loan has reached its final payment date - typically the entire remaining balance becomes past due

This distinction allows for different business rules, such as accelerated collection procedures or different late fee calculations for matured loans.

Integration with Credit Lifecycle

Past due operations integrate with other parts of the system:

  • Accrual Operations: Automatically created to calculate late fees on the newly past due amounts

  • Credit Labels: Display indicators like PastDueLabel to show past due status without changing the core credit status

  • Notification Systems: Often trigger automated borrower communications

  • Collection Workflows: May initiate collection processes or escalation procedures

This design ensures that the transition from current to past due debt is handled consistently and triggers all necessary downstream processes.

9.6. Account Structure and Debt Management

The operations framework uses a flexible account-based debt structure defined in the configuration.

Account Type Constants

The example project defines these account types in CreditCalculationConfiguration:

    public static final String PRINCIPAL = "PRINCIPAL";
    public static final String INTEREST = "INTEREST";
    public static final String PAST_DUE_PRINCIPAL = "PD_PRINCIPAL";
    public static final String PAST_DUE_INTEREST = "PD_INTEREST";
    public static final String LATE_FEE = "LATE_FEE";
    public static final String OVERPAYMENT = "OVERPAYMENT";

These accounts represent different types of debt: * PRINCIPAL - Outstanding loan principal amount * INTEREST - Accrued interest charges * PAST_DUE_PRINCIPAL - Overdue principal amounts * PAST_DUE_INTEREST - Overdue interest amounts * LATE_FEE - Late payment penalties * OVERPAYMENT - Credit balance from overpayments

9.7. Operation Configuration

The operations framework requires careful configuration to define how operations behave, how payments are distributed, and how the overall credit system operates. The example project demonstrates a complete configuration approach.

Configuration Architecture

All operation-related configuration is centralized in CreditCalculationConfiguration, which serves as the single source of truth for:

  • Credit Status Definitions - Available credit states and their properties

  • Account Structure - Debt account types and their relationships

  • Operation Handlers - Services that process each operation type

  • Payment Distribution - Rules for how payments are applied to debt

  • Accrual Engines - Time-based calculation components

  • Credit View Options - UI display configuration

Credit Status Configuration

The framework defines credit statuses with specific properties:

public static final CreditStatus PENDING = new CreditStatus("PENDING", 1000, false);
public static final CreditStatus ACTIVE = new CreditStatus("ACTIVE", 1100, false);
public static final CreditStatus CLOSED = new CreditStatus("CLOSED", 2000, true);
public static final CreditStatus VOID = new CreditStatus("VOID", 2100, true);

Each status includes: * Name: Human-readable identifier * Order: Numeric value for status progression logic * Ending Flag: Whether this status represents a terminal state

Account Structure Configuration

The debt account structure is defined as constants:

    public static final String PRINCIPAL = "PRINCIPAL";
    public static final String INTEREST = "INTEREST";
    public static final String PAST_DUE_PRINCIPAL = "PD_PRINCIPAL";
    public static final String PAST_DUE_INTEREST = "PD_INTEREST";
    public static final String LATE_FEE = "LATE_FEE";
    public static final String OVERPAYMENT = "OVERPAYMENT";

These accounts represent different types of debt and credit balances: * Current Debt: PRINCIPAL, INTEREST - active loan balances * Past Due Debt: PAST_DUE_PRINCIPAL, PAST_DUE_INTEREST - overdue amounts * Penalties: LATE_FEE - fees for late payments * Credits: OVERPAYMENT - borrower credit balances

Operation Handler Configuration

Each operation type requires a corresponding handler bean:

Charge Operation Handler
@Bean
ChargeOperationService chargeOperationService() {
    return new ChargeOperationService();
}

The ChargeOperationService implements CreditOperationHandler<ChargeOperation> and defines how charge operations affect debt balances.

Payment Operation Handler
    @Bean
    CreditPaymentOperationHandler<ExampleCreditPayment> creditPaymentOperationHandler() {
        return new CreditPaymentOperationHandler<>(OVERPAYMENT, List.of(PAST_DUE_PRINCIPAL, PAST_DUE_INTEREST,
            LATE_FEE, INTEREST, PRINCIPAL)) {};
    }

The payment handler configuration is critical as it defines: * Overpayment Account: Where excess payments are credited (OVERPAYMENT) * Payment Waterfall: The order in which payments are applied to debt accounts

Past Due Operation Handler
@Bean
PastDueOperationService pastDueOperationService() {
    LinkedHashMap<String, String> map = new LinkedHashMap<>();
    map.put(INTEREST, PAST_DUE_INTEREST);
    map.put(PRINCIPAL, PAST_DUE_PRINCIPAL);
    return new PastDueOperationService(map);
}

The mapping defines how current debt accounts are transferred to past due accounts when payments are missed.

Accrual Operation Handler
@Bean
AccrualOperationService accrualOperationService() {
    return new AccrualOperationService();
}

The accrual service coordinates with accrual engines to calculate time-based charges.

Accrual Engine Configuration

Accrual engines handle specific types of time-based calculations:

Interest Accrual Engine
@Bean
InterestAccrualEngine interestAccrualEngine() {
    return new InterestAccrualEngine();
}

Calculates interest on the PRINCIPAL balance and adds it to the INTEREST account.

Late Fee Accrual Engine
@Bean
LateFeeAccrualEngine lateFeeAccrualEngine() {
    return new LateFeeAccrualEngine();
}

Calculates late fees on past due balances and adds them to the LATE_FEE account.

Loan Engine Configuration

The loan engine orchestrates the overall calculation process:

@Bean
LoanEngine loanEngine() {
    return new BasicLoanEngine(PENDING);
}

The BasicLoanEngine is initialized with the default credit status (PENDING) for new credits.

Credit View Configuration

The view configuration determines which accounts are displayed in the user interface:

@Bean
CreditViewOptions creditViewOptions() {
    return new CreditViewOptions(PRINCIPAL, INTEREST, PAST_DUE_PRINCIPAL, PAST_DUE_INTEREST, LATE_FEE);
}

This configuration excludes the OVERPAYMENT account from standard credit displays, as it represents a credit balance rather than debt.

Customizing Configuration for Different Credit Products

Different credit products require different configurations. Common customization patterns include:

Custom Account Structures and Payment Distribution
// Credit card configuration
public static final String PURCHASES = "PURCHASES";
public static final String CASH_ADVANCES = "CASH_ADVANCES";
public static final String FEES = "FEES";

// Payment order: fees first, then cash advances, then purchases
new CreditPaymentOperationHandler<>("CREDIT_BALANCE",
    List.of("FEES", "CASH_ADVANCES", "PURCHASES"));

// Mortgage configuration with escrow
public static final String ESCROW = "ESCROW";
public static final String PMI = "PMI";

// Payment order: fees, past due, escrow, current debt
new CreditPaymentOperationHandler<>("ESCROW_SURPLUS",
    List.of("LATE_FEE", "PAST_DUE_PRINCIPAL", "ESCROW", "PRINCIPAL"));
Configuration Validation

The framework automatically validates configuration consistency:

  • Handler Registration: Ensures all operation types have corresponding handlers

  • Account References: Validates that payment distribution references valid account types

  • Engine Registration: Confirms accrual engines are properly registered

  • Status Consistency: Checks that status definitions are logically consistent

Configuration Best Practices
  1. Centralized Configuration: Keep all operation configuration in a single class for maintainability

  2. Meaningful Constants: Use descriptive names for account types and statuses

  3. Documented Relationships: Clearly document how accounts relate to each other

  4. Environment-Specific Beans: Use Spring profiles or conditions for product-specific configurations

  5. Validation: Implement configuration validation to catch setup errors early

  6. Consistent Naming: Use consistent naming patterns across account types and operation handlers

The configuration approach in the example project provides a flexible foundation that can be adapted for various lending products while maintaining consistency and clarity.

9.8. Operation Synchronization

The PreCalculateSynchronizer interface enables operations to maintain consistency with related events.

Synchronization Example

The AccrualOperationService demonstrates how synchronization works by automatically creating accrual operations when payments or past due events occur. The service implements PreCalculateSynchronizer and scans for trigger operations, ensuring that accrual operations exist for dates when debt balances change.

You can examine the complete implementation in the source code: AccrualOperationService.java.

9.9. Real-world Usage Patterns

Understanding how operations work together in practice is essential for implementing robust credit systems. The example project’s test cases demonstrate several real-world scenarios that show the complete operation flow.

Scenario 1: Loan Disbursement and Interest Accrual

This scenario demonstrates the basic credit lifecycle from disbursement through interest calculation.

The Flow
  1. Credit Creation: A new credit is created with defined terms (principal amount, interest rate, payment schedule)

  2. Principal Disbursement: A ChargeOperation adds the loan principal to the PRINCIPAL account

  3. Time Progression: As time passes, the calculation engine processes each day

  4. Interest Accrual: AccrualOperationService automatically creates AccrualOperation entities for interest calculation

  5. Balance Updates: Interest is calculated and added to the INTEREST account

Key Insights
  • Automatic Synchronization: Accrual operations are created automatically when needed

  • Daily Processing: The calculation engine processes operations chronologically

  • Time-based Calculations: Interest accrues based on outstanding principal and elapsed time

From the Test Case

The chargeOperation() test in CalculationTest demonstrates this pattern:

Credit Start → Charge Principal → Calculate to Today → Verify Interest Accrued

The test verifies that interest is correctly calculated using the formula: principal × (rate / 100) × (days / 360)

Scenario 2: Payment Processing and Distribution

This scenario shows how borrower payments are processed and distributed across debt accounts.

The Flow
  1. Outstanding Debt: Credit has balances in multiple accounts (principal, interest, fees)

  2. Payment Received: An ExampleCreditPayment operation is created

  3. Payment Distribution: The payment handler applies the payment according to the configured waterfall

  4. Balance Updates: Debt accounts are reduced according to priority order

  5. Continued Accruals: Interest continues to accrue on remaining balances

Payment Waterfall Logic

The example project uses this payment priority: 1. PAST_DUE_PRINCIPAL - Overdue principal first 2. PAST_DUE_INTEREST - Overdue interest second 3. LATE_FEE - Late fees third 4. INTEREST - Current interest fourth 5. PRINCIPAL - Current principal last

From the Test Cases

The paymentOperation1() and paymentOperation2() tests demonstrate:

Partial Payment Scenario:

Charge Principal → Partial Payment → Verify Interest Paid → Verify Principal Unchanged

Full Payment Scenario:

Charge Principal → Full Payment → Verify Interest Paid → Verify Principal Reduced
Scenario 3: Past Due Processing and Late Fees

This scenario illustrates the complex process of handling missed payments and calculating late fees.

The Flow
  1. Payment Due Date: A scheduled payment date arrives

  2. Insufficient Payment: Borrower makes no payment or insufficient payment

  3. Past Due Operation: System automatically creates PastDueOperation

  4. Account Transfers: Current debt moves to past due accounts

  5. Late Fee Accrual: LateFeeAccrualEngine begins calculating fees on past due amounts

  6. Payment Priority Change: Future payments prioritize past due amounts

Account Movement Logic

When past due occurs: * INTEREST balance → PAST_DUE_INTEREST account * PRINCIPAL balance → PAST_DUE_PRINCIPAL account

From the Test Case

The pastDue1() test demonstrates this complex scenario:

Charge Principal → Insufficient Payment → Calculate Past Due Date → Verify Account Transfers → Verify Late Fees

The test shows how the system: - Moves unpaid amounts to past due accounts - Calculates late fees on the past due balances - Maintains accurate balance tracking across account types

Scenario 4: End-of-Day vs Intraday Processing

This scenario highlights the importance of operation timing in business logic.

The Concept
  • Intraday Operations: Charges, payments, and accruals process immediately

  • End-of-Day Operations: Past due operations wait until the business day is "closed"

Business Rationale

Past due operations use isEndDayOperation() = true to prevent premature past due status if a payment arrives later in the same business day.

Example Timeline
9:00 AM  - Payment due date arrives
10:00 AM - Borrower payment received (processed immediately)
11:00 AM - Another payment received (processed immediately)
End of Day - Past due operation processes only if still insufficient payment

This prevents false past due situations when payments arrive throughout the day.

Scenario 5: Operation Synchronization in Action

This scenario demonstrates how the synchronization system maintains data consistency.

The Challenge

Accrual operations must exist for every date when: - Payments are made (to calculate interest up to payment date) - Past due events occur (to recalculate accruals on new account structure)

The Solution

AccrualOperationService implements PreCalculateSynchronizer:

  1. Scan for Trigger Events: Finds all payment and past due operations

  2. Identify Required Dates: Determines which dates need accrual calculations

  3. Create Missing Operations: Adds AccrualOperation entities for missing dates

  4. Cancel Unnecessary Operations: Removes accrual operations for dates without triggers

Result

The system ensures accurate interest calculations even when: - Payments are made on irregular dates - Past due events change the debt structure - Operations are added or modified after the fact

Key Patterns from Real Usage
1. Event-Driven Architecture

Operations trigger other operations automatically: - Payments trigger accrual calculations - Past due events trigger late fee calculations - Each operation maintains referential integrity

2. Chronological Processing

The calculation engine processes operations in strict date order: - Earlier operations affect later operations - Processing order within a date matters (getOrder() values) - State changes are cumulative and consistent

3. Business Rule Flexibility

The framework accommodates complex business rules: - Different payment priorities for different products - Time-sensitive processing (end-of-day vs intraday) - Automatic synchronization of dependent operations

4. Audit and Traceability

Every operation maintains complete audit trails: - Who created the operation and when - What the operation changed (debt account effects) - Why the operation was created (business event triggers)

Testing Real-World Scenarios

The example project’s CalculationTest demonstrates how to test these scenarios:

  1. Setup Realistic Conditions: Create credits with proper terms and schedules

  2. Apply Operations in Sequence: Mirror real-world event timing

  3. Verify All Effects: Check not just primary changes but secondary effects

  4. Test Edge Cases: Include scenarios like overpayments and zero balances

These patterns provide a foundation for implementing robust credit operations that handle the complexity of real-world lending scenarios while maintaining accuracy and auditability.

9.10. Best Practices

Operation Design Principles
  • Immutability: Operations should be immutable once created and approved

  • Idempotency: Operations should produce the same result when applied multiple times

  • Atomicity: Each operation should represent a single, atomic business transaction

  • Auditability: All operations must be fully auditable with complete change history

  • Extensibility: Design operations to be easily extended for new business requirements

Performance Optimization
  • Batch Processing: Group related operations for efficient processing

  • Lazy Loading: Use lazy loading for operation collections to avoid N+1 queries

  • Indexing: Create appropriate database indexes for operation queries

  • Caching: Cache frequently accessed operation handlers and calculation results

Error Handling
  • Validation: Validate operations before processing to catch errors early

  • Retry Logic: Implement retry mechanisms for transient failures

  • Compensation: Design compensation operations for failed transactions

  • Monitoring: Monitor operation processing for performance and error patterns

Security Considerations
  • Authorization: Ensure proper authorization for operation creation and modification

  • Data Protection: Protect sensitive operation data with encryption

  • Audit Logging: Maintain comprehensive audit logs for regulatory compliance

  • Access Control: Implement role-based access control for operation management

9.11. Testing Operations

The operations framework provides comprehensive testing capabilities that allow you to verify operation behavior, calculation accuracy, and integration between different operation types.

Integration Testing Approach

The example project demonstrates a complete integration testing strategy using CalculationTest that tests the entire operation processing pipeline.

Test Configuration

The test uses a comprehensive Spring configuration that mirrors the production setup:

@DataJpaTest
@AutoConfigureEmbeddedDatabase(provider = DatabaseProvider.ZONKY)
@ContextConfiguration(classes = {CreditCalculationConfiguration.class, CalculationTest.CalculationTestConfig.class})

Key testing components: * Embedded Database: Uses Zonky for isolated database testing * Transaction Management: TransactionTemplateBuilder for proper transaction handling * Real Services: Uses actual CreditCalculationService and AccrualService instances * Complete Configuration: Includes all operation handlers and accrual engines

Testing Patterns

The example project demonstrates several essential testing patterns:

1. Credit Lifecycle Testing

Testing the complete credit lifecycle from creation through operations:

// Create credit with realistic conditions
UUID creditId = initCredit(startDate, TODAY);

// Apply operations
charge(creditId, chargeDate, principalAmount);
registerPayment(creditId, paymentDate, paymentAmount);

// Trigger calculation
calculate(creditId, startDate, TODAY);

// Verify results
ExampleCredit credit = entityManager.find(ExampleCredit.class, creditId);
Assertions.assertEquals(expectedBalance, credit.getActualSnapshot().getDebt().getAccount(PRINCIPAL).get());
2. Operation Interaction Testing

Testing how different operations interact with each other:

@Test
public void paymentOperation1() {
    // Setup: Create credit and charge principal
    charge(creditId, chargeDate, principal);

    // Test: Make partial payment
    registerPayment(creditId, paymentDate, partialPayment);
    calculate(creditId, startDate, TODAY);

    // Verify: Check payment distribution and remaining balances
    ExampleCreditPayment payment = credit.getOperations(ExampleCreditPayment.class, APPROVED).findAny().get();
    assertEquals(expectedInterestPayment, payment.getFinalDebt().get().getAccount(INTEREST).get());
    assertEquals(expectedRemainingInterest, credit.getActualSnapshot().getDebt().getAccount(INTEREST).get());
}
3. Accrual Calculation Testing

Testing time-based calculations and accrual accuracy:

@Test
public void chargeOperation() {
    charge(creditId, chargeDate, principal);
    calculate(creditId, startDate, TODAY);

    // Verify accrued interest calculation
    MonetaryAmount expectedInterest = principal.multiply(
        (INTEREST_RATE.doubleValue() / 100d) * (daysBetween / 360d)
    );

    Debt accruals = accrualService.calculateCurrentAccurals(credit);
    assertEquals(expectedInterest, accruals.getAccount(INTEREST).get());
}
4. Past Due Processing Testing

Testing complex past due scenarios:

@Test
public void pastDue1() {
    // Setup credit with insufficient payment
    charge(creditId, chargeDate, principal);
    registerPayment(creditId, paymentDate, insufficientPayment);

    // Calculate beyond payment due date
    calculate(creditId, startDate, TODAY.plusMonths(1));

    // Verify past due balances and late fee accruals
    assertEquals(expectedPastDueInterest, credit.getActualSnapshot().getDebt().getAccount(PAST_DUE_INTEREST).get());
    assertEquals(expectedLateFee, accrualService.calculateCurrentAccurals(credit).getAccount(LATE_FEE).get());
}
Testing Utilities

The test class provides reusable utility methods for common testing scenarios:

Credit Initialization
public UUID initCredit(LocalDate startDate, LocalDate today) {
    return transactionTemplateBuilder.requiresNew().execute(s -> {
        // Create complete credit structure: product, condition, application, credit
        // Return credit ID for use in tests
    });
}
Operation Creation
public void charge(UUID creditId, LocalDate operationDate, MonetaryAmount amount) {
    transactionTemplateBuilder.requiresNew().executeWithoutResult(status -> {
        chargeOperationService.createOperation(creditId, operationDate, amount);
    });
}

public void registerPayment(UUID creditId, LocalDate paymentDate, MonetaryAmount amount) {
    transactionTemplateBuilder.requiresNew().executeWithoutResult(status -> {
        entityManager.find(ExampleCredit.class, creditId).getOperations()
            .add(new ExampleCreditPayment(paymentDate, amount));
    });
}
Calculation Execution
public void calculate(UUID creditId, LocalDate from, LocalDate today) {
    transactionTemplateBuilder.requiresNew().executeWithoutResult(status -> {
        calculationService.calculate(creditId, from, today);
    });
}
Test Data Management

The tests use realistic financial data and calculations:

private static final BigDecimal INTEREST_RATE = BigDecimal.valueOf(12); // 12% annual
private static final BigDecimal LATE_FEE_RATE = BigDecimal.valueOf(24); // 24% annual
private static final BigDecimal PRINCIPAL = BigDecimal.valueOf(2_000_000); // 2M ZWL

Interest calculations use proper day-count methods:

MonetaryAmount interest = principal.multiply(
    (INTEREST_RATE.doubleValue() / 100d) * (ChronoUnit.DAYS.between(chargeDate, TODAY) / 360d)
);
Assertion Strategies

The tests demonstrate comprehensive assertion patterns:

Balance Verification
assertEquals(expectedAmount, credit.getActualSnapshot().getDebt().getAccount(PRINCIPAL).get());
Operation Effect Verification
ExampleCreditPayment payment = credit.getOperations(ExampleCreditPayment.class, APPROVED).findAny().get();
assertEquals(paymentAmount.negate(), payment.getFinalDebt().get().getAccount(INTEREST).get());
Accrual Verification
Debt accruals = accrualService.calculateCurrentAccurals(credit);
assertEquals(expectedLateFee, accruals.getAccount(LATE_FEE).get());
assertEquals(2, accruals.getAccounts().keySet().size()); // Verify account count
State Verification
assertEquals(expectedDate, credit.getActualSnapshot().getDate());
assertEquals(calculationDate, credit.getCalculationDate());
assertTrue(credit.getActualSnapshot().getDebt().getTotal().isEmpty()); // For zero balance
Best Practices for Operation Testing
  1. Use Realistic Data: Test with actual financial amounts and rates that reflect real-world scenarios

  2. Test Operation Sequences: Verify that operations work correctly when applied in different orders

  3. Verify Time-based Calculations: Ensure accruals calculate correctly across different time periods

  4. Test Edge Cases: Include scenarios like overpayments, zero balances, and boundary conditions

  5. Use Proper Transactions: Each operation should be in its own transaction to mirror production behavior

  6. Verify All Accounts: Check not just the primary effects but also secondary account impacts

  7. Test Calculation Accuracy: Use precise mathematical calculations to verify financial accuracy

The testing approach in the example project provides a solid foundation for ensuring operation correctness and can be extended for custom operation types and business scenarios.

The Credit Operations Framework provides a solid foundation for building sophisticated financial applications while maintaining flexibility for customization and extension. By following these patterns and best practices, you can create robust, scalable operation processing systems that meet your specific business requirements.

10. Payment Transactions Framework

The Payment Transactions Framework connects real-world payments to credit operations. When someone makes a payment, it creates a transaction record, processes it through a payment gateway, and then creates the corresponding credit operation. This ensures every payment operation can be traced back to an actual payment.

10.1. The Basic Concept

Think of payment transactions as a receipt system:

  1. Customer pays → Create transaction record (like writing a receipt)

  2. Process payment → Send to bank/payment processor

  3. Payment succeeds → Create credit operation (update the loan balance)

  4. Keep records → Maintain complete audit trail

This two-step process (transaction → operation) ensures every balance change has a real-world payment behind it.

10.2. Architecture Overview

The system has three main layers that work together:

Transaction Layer

What it does: Records and processes real payments

BorrowerTransaction → PaymentGateway → Bank/Card Processor
Bridge Layer

What it does: Converts successful payments into credit operations

BorrowerTransactionService.handle() → Creates CreditOperation
Credit Layer

What it does: Updates loan balances and calculates interest

ExampleCreditPayment → Credit Calculation → Updated Balances
Core Components

The payment transaction system includes:

  • PaymentTransaction - Base record for all payment attempts

  • BorrowerTransaction - Example project’s payment transaction type

  • PaymentGateway - Interface for connecting to payment processors

  • PaymentMethod - How customer wants to pay (bank account, card, etc.)

  • BorrowerTransactionService - Converts successful payments to credit operations

10.3. Real-World Basis Principle

All operations in the credit system must have verifiable real-world foundations:

Payment Operations Foundation

Payment operations must originate from actual payment transactions:

Real Payment → PaymentTransaction → Success Handler → CreditOperation → Balance Update
Other Operations Foundations

While payment operations require transactions, other operations have their own real-world basis:

  • Accrual Operations → Contract terms and offer conditions (interest rates, payment schedules)

  • Past Due Operations → Payment schedule agreements (contractual due dates)

  • Charge Operations → Should be based on disbursements or outgoing transactions

The example project may have a flaw with charge operations - they should typically be tied to disbursement transactions, outgoing transactions, or regulatory events rather than being manually created without clear business justification.

This principle ensures: * Regulatory Compliance - Every financial change can be traced to a real event * Audit Trail Integrity - Complete documentation of why changes occurred * Business Logic Accuracy - Operations reflect actual business events * Fraud Prevention - Prevents unauthorized or fictitious transactions

10.4. Transaction Entity Structure

Every payment attempt creates a PaymentTransaction record that tracks the payment from start to finish.

BorrowerTransaction Example

The example project uses BorrowerTransaction for loan payments:

@Entity
@DiscriminatorValue("BORROWER")
public class BorrowerTransaction extends PaymentTransaction {

    @ManyToOne(fetch = FetchType.LAZY)
    private ExampleCredit credit;

    @ManyToOne(fetch = FetchType.LAZY)
    @NotAudited
    private CreditOperation operation;

    public BorrowerTransaction(TransactionType type, MonetaryAmount amount,
                              PaymentMethod paymentMethod, ExampleCredit credit) {
        super(type, amount);
        this.credit = credit;
        setPaymentMethod(paymentMethod);
    }

    public ExampleCredit getCredit() { return credit; }
    public CreditOperation getOperation() { return operation; }
    public void setOperation(CreditOperation operation) { this.operation = operation; }
}

Key fields: * credit - Which loan this payment is for * operation - The credit operation created when payment succeeds * amount - How much money (uses MonetaryAmount for currency handling) * type - INCOMING (customer pays) or OUTGOING (refund/disbursement) * status - Current state of the payment * paymentMethod - How the customer is paying (bank account, card, etc.)

Transaction Status Lifecycle

Transactions go through these states:

DRAFT → READY_FOR_EXECUTION → IN_PROGRESS → SUCCEED
  ↓              ↓                 ↓           ↓
CANCELLED    CANCELLED         FAILED    (Create Operation)
                                 ↓
                            UNAVAILABLE
                                 ↓
                          (Manual Review)
Understanding Transaction Status

The TransactionStatus enum has three important properties:

public enum TransactionStatus {
    SUCCEED(800, true, true),    // successful=true, complete=true
    FAILED(700, false, true),    // successful=false, complete=true
    IN_PROGRESS(400, false, false); // successful=false, complete=false

    private boolean successful; // Did the payment work?
    private boolean complete;   // Is processing finished?
}

Status meanings: * DRAFT - Transaction created, not yet sent to payment processor * IN_PROGRESS - Sent to payment processor, waiting for response * SUCCEED - Payment processor approved the payment * FAILED - Payment processor declined the payment * UNAVAILABLE - System error or payment processor is down * CHARGEBACK - Bank reversed a previously successful payment

Transaction Processing Flow

The complete transaction processing follows this pattern:

  1. Transaction Creation: BorrowerTransaction entity created with payment details

  2. Gateway Submission: PaymentTransactionService submits to appropriate gateway

  3. Async Processing: Transaction processed asynchronously to avoid blocking

  4. Status Updates: Transaction status updated based on gateway response

  5. Success Handling: BorrowerTransactionService.handle() creates credit operation

  6. Error Handling: Failed transactions trigger appropriate error responses

10.5. Payment Gateways

Payment gateways connect your system to banks and payment processors. Think of them as translators that convert your payment requests into the specific format each processor expects.

The Gateway Interface

All payment gateways implement the same interface:

public interface PaymentGateway {
    String getMethodType();  // "ACH", "CARD", etc.
    String getName();        // "Stripe", "Bank_ACH", etc.
    boolean verify(PaymentMethod method) throws IOException;
    TransactionResult proceedIncoming(String orderId, PaymentMethod method, MonetaryAmount amount);
    TransactionResult proceedOutgoing(String orderId, PaymentMethod method, MonetaryAmount amount);
}

What each method does: * verify() - Validate payment method before processing (called by PaymentTransactionService.verify()) * proceedIncoming() - Process customer payments (money coming in) * proceedOutgoing() - Process refunds and disbursements (money going out)

Gateway Implementation Patterns

Payment gateways can be implemented following these patterns:

Real-Time API Gateway Pattern

For immediate credit/debit card processing:

@Service
public class CardPaymentGateway implements PaymentGateway {

    public TransactionResult proceedIncoming(String orderId, PaymentMethod method, MonetaryAmount amount) {
        // 1. Extract payment method details (tokenized)
        // 2. Build API request with transaction data
        // 3. Submit to payment processor via HTTPS
        // 4. Parse response and map to TransactionResult
        // 5. Return standardized result with gateway reference
    }
}

Key characteristics: * Immediate Processing - Real-time API calls with instant responses * Token-Based Security - Uses tokenized payment methods for PCI compliance * Structured Response - JSON/XML responses parsed into standard result format * Error Detection - Handles duplicate transactions and various error conditions

SOAP Web Service Gateway Pattern

For traditional banking integration:

@Service
public class ACHGateway implements PaymentGateway {

    public TransactionResult proceedOutgoing(String orderId, PaymentMethod method, MonetaryAmount amount) {
        // 1. Build SOAP command with ACH details
        // 2. Add merchant credentials and security headers
        // 3. Submit via SOAP web service
        // 4. Handle asynchronous ACH processing status
        // 5. Return result with settlement timing information
    }
}

Key characteristics: * SOAP Integration - XML-based web service communication * Asynchronous Processing - ACH transactions require settlement time * Comprehensive Logging - Full request/response logging for audit * Credential Management - Secure handling of merchant credentials

Batch File Gateway Pattern

For bulk ACH processing via NACHA files:

@Service
public class NACHABatchGateway implements PaymentGateway {

    @Scheduled(fixedRate = 3600000) // Hourly batch processing
    public void processBatch() {
        // 1. Find transactions ready for batch processing
        // 2. Create NACHA-compliant file format
        // 3. Add each transaction to appropriate batch
        // 4. Generate file and transmit via SFTP
        // 5. Update transaction statuses
    }
}

Key characteristics: * Batch Processing - Multiple transactions in single file * File-Based Transport - SFTP or similar file delivery * NACHA Compliance - Proper ACH file format generation * Delayed Settlement - Transactions marked successful when file sent, not when settled

Gateway Configuration

Different gateways can be configured for different payment types:

@Service
public class ACHGateway implements PaymentGateway {
    public String getMethodType() { return "ACH"; }
    public String getName() { return "Bank_ACH"; }
}

@Service
public class CardGateway implements PaymentGateway {
    public String getMethodType() { return "CARD"; }
    public String getName() { return "Stripe"; }
}

The system selects the appropriate gateway based on the payment method type.

10.6. Payment Methods

Payment methods represent how customers want to pay - bank account, credit card, etc. Each payment method stores the necessary information to process payments through the appropriate gateway.

Example: LiquidityClientPaymentMethod

The example project includes a simple payment method for testing:

@Entity
@DiscriminatorValue(LiquidityClientPaymentMethod.TYPE)
public class LiquidityClientPaymentMethod extends PaymentMethod {

    public static final String TYPE = LiquidityPaymentGateway.GATEWAY_TYPE;

    @Column(name = "processed_date")
    private LocalDate processedDate;

    @Embedded
    private MonetaryAmount amount;

    @Column(name = "name")
    private String ownerName;

    public LiquidityClientPaymentMethod(LocalDate processedDate, MonetaryAmount amount,
                                       TransactionType transactionType, String ownerName) {
        super(TYPE);
        this.processedDate = processedDate;
        this.amount = amount;
        this.transactionType = transactionType;
        this.ownerName = ownerName;
    }

    public LocalDate getProcessedDate() { return processedDate; }
    public MonetaryAmount getAmount() { return amount; }
    public String getOwnerName() { return ownerName; }
}

This payment method: * Stores an amount - For testing, it has a fixed amount * Has a processed date - When the "payment" was processed * Works with gateways - Can be used by payment gateways that support this type

Payment Method Types

Different payment method types serve different use cases:

Type Use Case Processing Pattern Security Model

ACH

Bank account transfers

Batch or real-time

Account number encryption

Debit/Credit Cards

Card payments

Real-time API

PCI tokenization

Digital Wallets

Mobile payments

Real-time API

OAuth tokens

Wire Transfers

Large amounts

Manual processing

Bank verification

Payment Method Implementation Patterns

When implementing new payment method types:

ACH Payment Method Pattern
@Entity
@DiscriminatorValue("ACH")
public class ACHPaymentMethod extends PaymentMethod {

    // Encrypted bank account details
    private String ownerName;
    private String accountNumber;    // Encrypted
    private String routingNumber;
    private AccountType accountType; // CHECKING, SAVINGS

    // Validation and security methods
    public boolean isValid() {
        return validateRoutingNumber() && validateAccountNumber();
    }
}

Key features: * Bank Account Details - Routing and account numbers for ACH processing * Account Type Classification - Checking vs savings account handling * Validation Logic - Routing number format and account number validation * Encryption - Sensitive account data encrypted at rest

Card Payment Method Pattern
@Entity
@DiscriminatorValue("CARD")
public class CardPaymentMethod extends PaymentMethod {

    // Tokenized card data - no sensitive information stored
    private String token;           // From payment processor
    private String lastFourDigits;  // For display only
    private String expiryMonth;
    private String expiryYear;

    public boolean isExpired() {
        return LocalDate.now().isAfter(getExpiryDate());
    }
}

Key characteristics: * Tokenization - Card numbers replaced with secure tokens from payment processor * PCI Compliance - No sensitive card data stored in application database * Display Information - Only last four digits stored for user interface * Expiry Validation - Built-in expiration checking

Payment Method Security

The framework implements comprehensive security patterns:

Data Protection
// Sensitive data encrypted at rest
@Convert(converter = EncryptedStringConverter.class)
private String accountNumber;

// Tokens from external processors
private String processorToken;

// Display-only information
private String maskedAccountNumber; // "****1234"
Validation and Verification
public interface PaymentMethodValidator {
    boolean validate(PaymentMethod method);
    ValidationResult verify(PaymentMethod method) throws IOException;
}

// Gateway-specific validation
@Override
public boolean verify(PaymentMethod method) throws IOException {
    // Real-time validation with payment processor
    return gateway.validatePaymentMethod(method);
}
Access Control
@PreAuthorize("hasPermission(#method, 'USE')")
public TransactionResult processPayment(PaymentMethod method, MonetaryAmount amount) {
    // Role-based access control for payment method usage
}

10.7. How Transactions Become Operations

When a payment succeeds, the system needs to update the loan balance. This happens in BorrowerTransactionService.handle().

The Conversion Process

Here’s what happens when a payment succeeds:

@Override
public void handle(PaymentTransaction t) {
    BorrowerTransaction transaction = (BorrowerTransaction) t;

    if (transaction.getStatus() == TransactionStatus.SUCCEED) {
        ExampleCredit credit = transaction.getCredit();
        LocalDate date = getProcessedDate(transaction);

        // Create the right type of operation
        CreditOperation operation = switch (transaction.getType()) {
            case INCOMING -> handleIncoming(credit, transaction, date);  // Customer payment
            case OUTGOING -> handleOutgoing(credit, transaction, date);  // Refund/disbursement
        };

        // Link them together for audit trail
        transaction.setOperation(operation);
    }
}
Customer Payments (INCOMING)

When a customer makes a payment:

private CreditPayment handleIncoming(ExampleCredit credit, BorrowerTransaction transaction, LocalDate date) {
    // Create payment operation
    CreditPayment payment = new ExampleCreditPayment(date, transaction.getAmount());

    // Register with credit system
    return paymentService.registerPayment(credit, payment);
}

This creates an ExampleCreditPayment operation that reduces the loan balance.

Refunds and Disbursements (OUTGOING)

When money goes to the customer:

private ChargeOperation handleOutgoing(ExampleCredit credit, BorrowerTransaction transaction, LocalDate date) {
    // Create charge operation (increases balance)
    return chargeOperationService.createOperation(credit.getId(), date, transaction.getAmount());
}

This creates a ChargeOperation that increases the loan balance (for disbursements) or reverses payments (for refunds).

The Audit Trail

The system maintains complete traceability:

  1. Transaction Record - Shows the real-world payment attempt

  2. Gateway Response - Stored in transaction.trace field

  3. Operation Link - transaction.operation points to the credit operation

  4. Credit Update - Operation appears in credit’s operation list

This means you can always trace a balance change back to the original payment.

10.8. Processing Payments Asynchronously

Payment processing happens in the background so users don’t have to wait. When someone submits a payment, the system:

  1. Creates transaction record - Saves it immediately

  2. Returns to user - Shows "processing" message

  3. Processes in background - Calls payment gateway

  4. Updates status - Success or failure

  5. Creates operation - If payment succeeded

Why Async Processing?
  • Faster user experience - Don’t wait for slow payment processors

  • Better error handling - Can retry failed payments

  • Scalability - Handle many payments at once

Error Handling

When processing payments, three things can happen:

Payment Declined

// Gateway says "insufficient funds" or "invalid card"
transaction.setStatus(TransactionStatus.FAILED);
transaction.addTrace("Gateway declined: " + result.getMessage());

System Error

// Code bug or unexpected error
transaction.setStatus(TransactionStatus.UNAVAILABLE);
transaction.addTrace("System error: " + e.getMessage());

Gateway Down

// Payment processor is unavailable
transaction.setStatus(TransactionStatus.UNAVAILABLE);
transaction.addTrace("Gateway unavailable: " + e.getMessage());
// Can retry later
Monitoring

Since processing happens in background, you need to monitor: * Failed transactions - Show in admin dashboard for investigation * Stuck transactions - Alert if too many stay "IN_PROGRESS" * Gateway errors - Monitor payment processor uptime

10.9. Transaction Types and Patterns

Different transaction types serve different business purposes and follow specific processing patterns.

Incoming Payment Transactions

Borrower payments to reduce credit balances:

BorrowerTransaction payment = new BorrowerTransaction(
    TransactionType.INCOMING,
    credit,
    paymentMethod,
    paymentAmount,
    "Borrower payment"
);

Processing flow: 1. User Initiates - Borrower submits payment through portal 2. Transaction Created - BorrowerTransaction entity persisted 3. Gateway Processing - Payment method charged via appropriate gateway 4. Success Handling - ExampleCreditPayment operation created 5. Balance Update - Credit calculation applies payment to debt accounts

Outgoing Payment Transactions

Disbursements or refunds to borrowers:

BorrowerTransaction disbursement = new BorrowerTransaction(
    TransactionType.OUTGOING,
    credit,
    paymentMethod,
    disbursementAmount,
    "Loan disbursement"
);

Processing flow: 1. System Initiates - Loan approval triggers disbursement 2. Transaction Created - Outgoing transaction entity 3. Gateway Processing - Funds sent to borrower account 4. Success Handling - Disbursement operation created 5. Balance Update - Principal balance increased

Chargeback Transactions

Handling payment reversals:

// Original payment is reversed
originalTransaction.setStatus(TransactionStatus.CHARGEBACK);

// Chargeback operation created to reverse the payment
ChargebackOperation chargeback = new ChargebackOperation(
    originalPayment.getAmount().negate(),
    "Chargeback: " + originalTransaction.getOrderId()
);
Retry Patterns

Failed transactions may be retried based on failure type:

if (canRetry(transaction, result)) {
    scheduleRetry(transaction, calculateBackoffDelay(transaction.getRetryCount()));
} else {
    markPermanentFailure(transaction, result);
}

Retry logic considers: * Failure Type - Network errors retryable, declines usually not * Retry Count - Exponential backoff with maximum attempts * Time Limits - Don’t retry indefinitely old transactions

10.10. Testing Payment Transactions

Testing payment transactions requires careful consideration of external dependencies and asynchronous processing.

Test Gateway Implementation

For testing, implement a controllable test gateway:

@Service
public class TestPaymentGateway implements PaymentGateway {

    @Override
    public TransactionResult proceedIncoming(String orderId, PaymentMethod method, MonetaryAmount amount) {
        // Simulate different scenarios based on test data
        if (amount.getNumber().doubleValue() == 999.99) {
            return new TransactionResult(orderId, amount, Status.FAIL, false, "Test decline");
        }

        if ("ERROR_TOKEN".equals(method.getToken())) {
            throw new RuntimeException("Test gateway error");
        }

        return new TransactionResult(orderId, amount, Status.SUCCESS, false, "Test success");
    }
}
Integration Testing Patterns

Test the complete transaction-to-operation flow:

@Test
@Transactional
public void testSuccessfulPayment() {
    // Setup: Create credit and payment method
    UUID creditId = createTestCredit();
    PaymentMethod paymentMethod = createTestPaymentMethod();

    // Execute: Process payment transaction
    BorrowerTransaction transaction = new BorrowerTransaction(
        TransactionType.INCOMING, credit, paymentMethod,
        MonetaryAmount.of(500, "USD"), "Test payment"
    );

    paymentTransactionService.processTransaction(transaction.getId());

    // Wait for async processing
    await().atMost(5, SECONDS).until(() ->
        transactionRepository.findById(transaction.getId()).getStatus() == TransactionStatus.SUCCEED
    );

    // Verify: Check operation was created and credit updated
    ExampleCredit updatedCredit = creditRepository.findById(creditId);
    assertThat(updatedCredit.getOperations(ExampleCreditPayment.class)).hasSize(1);

    ExampleCreditPayment payment = updatedCredit.getOperations(ExampleCreditPayment.class).iterator().next();
    assertThat(payment.getAmount()).isEqualTo(MonetaryAmount.of(500, "USD"));
    assertThat(payment.getTransaction()).isEqualTo(transaction);
}
Mocking External Dependencies

For unit tests, mock gateway dependencies:

@MockBean
PaymentGateway mockGateway;

@Test
public void testGatewayFailure() {
    // Setup: Mock gateway to return failure
    when(mockGateway.proceedDebit(any(), any(), any()))
        .thenReturn(new TransactionResult("123", amount, Status.FAIL, false, "Declined"));

    // Execute: Process transaction
    paymentTransactionService.processTransaction(transactionId);

    // Verify: Transaction marked as failed
    BorrowerTransaction transaction = transactionRepository.findById(transactionId);
    assertThat(transaction.getStatus()).isEqualTo(TransactionStatus.FAILED);

    // Verify: No operation created
    assertThat(credit.getOperations()).isEmpty();
}

10.11. Security and Compliance

Payment transaction processing requires adherence to strict security and compliance standards.

PCI DSS Compliance

For card payments:

  • No Card Storage - Card numbers never stored in application database

  • Tokenization - Sensitive data replaced with non-sensitive tokens

  • Secure Transmission - All payment data encrypted in transit

  • Access Controls - Role-based access to payment functionality

Bank Security Standards

For ACH payments:

  • Encryption at Rest - Bank account data encrypted in database

  • Secure APIs - TLS encryption for all gateway communication

  • Credential Management - Secure storage of gateway credentials

  • Audit Logging - Complete transaction audit trails

Regulatory Compliance

Financial regulations require:

  • Transaction Traceability - Complete audit trail from user action to balance change

  • Data Retention - Transaction records maintained for required periods

  • Reporting - Transaction data available for regulatory reporting

  • Error Handling - Proper handling and reporting of failed transactions

10.12. Best Practices

Transaction Design Principles
  • Idempotency - Transactions should produce same result when retried

  • Atomicity - Each transaction represents a single business event

  • Traceability - Complete audit trail from initiation to completion

  • Error Recovery - Graceful handling of all failure scenarios

Gateway Integration Best Practices
  • Timeout Handling - Appropriate timeouts for gateway calls

  • Retry Logic - Intelligent retry strategies for transient failures

  • Rate Limiting - Respect gateway rate limits and quotas

  • Monitoring - Comprehensive monitoring of gateway performance

Security Best Practices
  • Token Management - Secure handling of payment method tokens

  • Credential Security - Proper storage and rotation of gateway credentials

  • Data Minimization - Store only necessary payment data

  • Audit Logging - Complete logging of all payment activities

Performance Optimization
  • Async Processing - Non-blocking transaction processing

  • Connection Pooling - Efficient gateway connection management

  • Caching - Cache gateway configuration and metadata

  • Batch Processing - Group transactions where possible (NACHA)

10.13. Summary

The Payment Transactions Framework ensures every credit operation has a real-world basis:

The Flow:

Customer Payment → BorrowerTransaction → PaymentGateway → Bank/Processor
     ↓
Payment Succeeds → BorrowerTransactionService.handle() → ExampleCreditPayment → Updated Loan Balance

Key Benefits: * Complete audit trail - Every balance change traces to a real payment * Async processing - Fast user experience, reliable background processing * Multiple gateways - Support different payment processors * Error handling - Graceful handling of declined payments and system errors * Regulatory compliance - Full documentation for audits

For Developers: * Extend PaymentTransaction for your transaction types * Implement PaymentGateway for new payment processors * Use PaymentTransactionHandler to convert transactions to operations * Always maintain the transaction → operation link for audit trails

This foundation supports any type of payment processing while ensuring complete traceability and regulatory compliance.