Eclipse Plugin Developer Guide

Plugins let you extend Eclipse platform behaviour at runtime without modifying platform source code. A plugin is a plain JAR uploaded through the Plugin API. The platform hot-loads it, wires CDI injection into it, and calls its handler methods when the relevant events occur.

There are three broad categories of plugin:

CategoryMechanismUse cases
Injection-point plugins@OnNamedInjectionPoint annotated methodsIntercept events, enrich data, override behaviour at named hooks
Implementation pluginsExtend a Pluggable* base classProvide a custom payment gateway, withdrawal gateway, loan gateway, remittance gateway, payment processor, withdrawal processor, fraud handler, or wallet type
Listener pluginsExtend a Pluggable*Listener classReact to service lifecycle events (user creation, payment completion, loan updates, document uploads, etc.)

1. Core Annotations

@PluginDescriptor (required, class-level)

Every plugin JAR must contain exactly one class annotated with @PluginDescriptor. This is how the platform identifies and names the plugin.

@PluginDescriptor(
    name        = "my-plugin",          // unique plugin name (required)
    version     = "1.0",                // version string (optional, default "1.0")
    description = "Does something useful" // human-readable description (optional)
)
public class MyPluginInfo { }

The name field must be unique across all loaded plugins. Uploading a JAR with an existing name is treated as an upgrade.


@OnNamedInjectionPoint (method-level)

Marks a method as a handler for a named platform event.

@OnNamedInjectionPoint(
    injectionPoint = "wallet.postWalletTransfer",  // event name (required)
    priority       = 10                             // higher = runs first (optional, default 0)
)
public void afterTransfer(@Named("from") IWallet from, @Named("to") IWallet to) { ... }

The framework resolves method parameters by type or by name (see section 3). Multiple methods in the same plugin, or across multiple plugins, can handle the same injection point. They run in descending priority order.


2. Plugin Lifecycle and Management

Uploading a plugin

Plugins are managed via the Plugin REST API. To deploy a plugin:

POST /eclipse-conductor/rest/v1/plugins
Content-Type: multipart/form-data

jar=<jarBytes>   (required)
status=ON|OFF    (optional, defaults to OFF)
isGlobal=true    (optional, applies to all tenants)
tenantIds=1,2,3  (optional, specific tenants)

On first upload the platform scans the JAR, discovers all @OnNamedInjectionPoint methods, and auto-populates a config JSON:

{
  "injectionPoints": {
    "com.example.MyPlugin#afterTransfer": true,
    "com.example.MyPlugin#onWalletCreate": true
  }
}

Each key is ClassName#methodName. Set the value to false to disable a specific handler without disabling the whole plugin.

Plugin status

StatusEffect
ONPlugin is active and handlers are called
OFFPlugin is loaded but no handlers are called
DELETEDPlugin is unloaded and garbage collected
PUT /eclipse-conductor/rest/v1/plugins/{pluginId}/status
Body: "ON"

Updating a plugin

Re-upload a JAR with the same @PluginDescriptor name. The platform merges the existing config with the new JAR's injection points — handlers that no longer exist in the new JAR are automatically removed from the config; new handlers are added with enabled: true; existing handlers keep their current enabled state.

PUT /eclipse-conductor/rest/v1/plugins/{pluginId}
Body: { "jar": "<base64>" }

Tenant mapping

A plugin can be global (runs for all tenants) or restricted to specific tenant IDs:

PUT /eclipse-conductor/rest/v1/plugins/{pluginId}/mappings
Body: { "global": false, "mappings": { "tenantIds": [42, 99] } }

3. Injection-Point Plugins

Writing a handler

@PluginDescriptor(name = "transfer-audit", version = "1.0")
@Singleton
public class TransferAuditPlugin {

    @OnNamedInjectionPoint(injectionPoint = "wallet.postWalletTransfer", priority = 5)
    public void auditTransfer(
            @Named("from")    IWallet from,
            @Named("to")      IWallet to,
            @Named("history") List<WalletHistory> history) {

        // 'from' and 'to' are the same IWallet type, so @Named is required to distinguish them
        log.info("Transfer: {} → {} ({} entries)", from.getWalletId(), to.getWalletId(), history.size());
    }
}

CDI injection in plugins

Classes annotated with @Singleton, @ApplicationScoped, @RequestScoped, or @Dependent get CDI wiring. The platform injects the following services automatically:

TypeNotes
CallAsUserJsonRestClientREST client that calls other services as the current user
CallAsSystemJsonRestClientREST client that calls other services as the system
RequestContextCurrent request context (tenant ID, user, session, etc.)
EntityManagerJPA entity manager

Add them a method parameters to receive them:

@Singleton
public class MyPlugin {

    
    @OnNamedInjectionPoint(injectionPoint = "domainEvent")
    public void onEvent(DomainEvent event, RequestContext ctx, EntityManager em) {
        log.info("Tenant {} fired event {}", ctx.getTenantId(), event);
    }
}

Parameter resolution rules

The framework resolves each method parameter in this order:

  1. If the parameter is annotated with @Named("x"), it looks for a NamedParam whose name matches "x" in the ctx varargs passed to fireInjectionPoint.
  2. Otherwise it resolves by type — exact match first, then assignability, then the global CDI context.

When the same type appears more than once at an injection point all such arguments are wrapped in NamedParam by the platform. Your method must use @Named to tell the framework which one you want, otherwise it throws.

// Correct — two IWallet args must use @Named
public void handle(@Named("from") IWallet from, @Named("to") IWallet to) { ... }

// Wrong — ambiguous, will throw at runtime
public void handle(IWallet from, IWallet to) { ... }

Return values

If a handler returns a non-null value, it is collected by the caller. fireInjectionPoint returns a List<Object> of all non-null return values from all handlers, in priority order. The platform uses this for injection points like convertCurrency and sendTextSms where a plugin is expected to produce a result.


4. All Supported Injection Points

Wallet events

Injection PointWhen firedParameters@Named keys
wallet.preWalletCreateBefore a new wallet is persistedWallet newWallet
wallet.postWalletCreateAfter a new wallet is persistedWallet newWallet
wallet.onWalletUpdateWhen wallet fields are updatedWallet wallet
wallet.preReservationBefore a reservation is createdBigDecimal amount, String sessionId, ZonedDateTime expiryDate, String description, Boolean forcesessionId, description
wallet.preWalletTransferBefore a wallet transfer is executedIWallet from, IWallet to, Boolean bulk, Boolean scheduledfrom, to, bulk, scheduled
wallet.postWalletTransferAfter a wallet transfer completesIWallet from, IWallet to, List<WalletHistory> history, Boolean bulk, Boolean scheduledfrom, to, history, bulk, scheduled
wallet.augmentTransferTo augment/split a transfer before it executesIWallet from, IWallet to, Boolean bulk, Boolean scheduledfrom, to, bulk, scheduled
wallet.doSMSForFailedTransferWhen a transfer fails and the platform wants to send an SMSTransfer transfer, IWallet from, IWallet to, String failedReasonfrom, to
wallet.onTransactionFailureWhen a transaction fails at the ledger levelLong walletId, Long otherWalletId, BigDecimal amount, String currency, String sessionIdwalletId, otherWalletId, currency, sessionId
wallet.postWalletTypeCreateAfter a new wallet type is createdWalletType walletType
wallet.onWalletTypeUpdateWhen a wallet type is updatedWalletType walletType, DbWalletType existing

Exchange rate

Injection PointWhen firedParameters@Named keysExpected return type
convertCurrencyWhen a spot exchange rate is neededString baseCurrency, String toCurrencybaseCurrency, toCurrencyExchangeRate
convertCurrencyHistoricWhen a historic exchange rate is neededString baseCurrency, String toCurrency, Instant datebaseCurrency, toCurrency, dateExchangeRate

If a plugin returns an ExchangeRate, the platform uses it. If no plugin handles the injection point, the built-in rate provider is used.

SMS

Injection PointWhen firedParameters@Named keysExpected return type
sendTextSmsWhen the platform needs to send an SMSString cardBin, String to, String message, String senderId, Object rawObjectcardBin, to, message, senderId, rawObjectString (provider message ID)

HTTP request / response interception

Injection PointWhen firedParameters@Named keys
conductorRequestOn every inbound HTTP request to Eclipse Conductor (before auth and routing)ContainerRequestContext requestContext
conductorResponseOn every outbound HTTP response from Eclipse ConductorContainerRequestContext requestContext, ContainerResponseContext responseContext

Use JaxRsUtils.replyImmediately(rq, response) inside a conductorRequest handler to short-circuit the request and return a custom response immediately.

Domain events

Injection PointWhen firedParameters@Named keys
domainEventWhen any domain event is published via UK.EventsDomainEvent event

Exceptions and alerts

Injection PointWhen firedParameters@Named keys
exceptionWhen an exception is processed by the platform exception handlerThrowable t, List<? extends ExceptionData> edList
alertWhen an alert is sent via UK.AlertsExceptionSeverity severity, String channelName, String message, ZonedDateTime alertDate, Throwable t, String traceIdchannelName, message, traceId

Payment events

Injection PointWhen firedParameters@Named keys
payment.preCreatePaymentBefore a new payment is persistedNewPayment newPayment
payment.postCreatePaymentAfter a new payment is persistedPayment payment
payment.preCreateWithdrawalBefore a new withdrawal is persistedWithdrawal newWithdrawal
payment.postCreateWithdrawalAfter a new withdrawal is persistedWithdrawal withdrawal
payment.postPaymentCompleteAfter a payment reaches a terminal completed statePayment payment
payment.postWithdrawalCompleteAfter a withdrawal reaches a terminal completed stateWithdrawal withdrawal
payment.prePaymentAcquiringBefore payment acquiring is attemptedDbPayment payment

Loan events

Injection PointWhen firedParameters@Named keys
loan.preCreateLoanBefore a new loan is persistedNewLoan newLoan
loan.postCreateLoanAfter a new loan is persistedLoan loan
loan.preLoanUpdateBefore a loan is updatedLoan loan, DbLoan existingLoan
loan.postLoanUpdateAfter a loan is updatedLoan loan

Remittance events

Injection PointWhen firedParameters@Named keys
remittance.preCreateRemittanceBefore a new remittance is persistedNewRemittance newRemittance
remittance.postCreateRemittanceAfter a new remittance is persistedRemittance remittance
remittance.preRemittanceUpdateBefore a remittance is updatedRemittance remittance, DbRemittance existingRemittance
remittance.postRemittanceUpdateAfter a remittance is updatedRemittance remittance
remittance.postRemittanceCompleteAfter a remittance reaches a terminal completed stateRemittance remittance
remittance.preRemittanceAcquiringBefore remittance acquiring is attemptedRemittance remittance

User and organisation events

Injection PointWhen firedParameters@Named keys
user.onUserCreateBefore a new user is createdUser newUser
user.onUserCreatedAfter a new user is persistedUser user
user.onUserUpdateBefore a user is updatedUser changes, DbUser existing
user.onUserUpdatedAfter a user is updatedUser user
user.onUserDeleteWhen a user is deletedUser user
user.onOrganisationCreateBefore a new organisation is createdOrganisation organisation
user.onOrganisationCreatedAfter a new organisation is persistedOrganisation organisation
user.onOrganisationUpdateBefore an organisation is updatedOrganisation changes, DbOrganisation existing
user.onOrganisationUpdatedAfter an organisation is updatedOrganisation organisation
user.onOrganisationDeleteWhen an organisation is deletedOrganisation organisation
user.onUserIdentityCreateAfter a user identity is createdUserIdentity userIdentity (secretOrKey and info stripped)
user.onUserIdentityDeleteWhen a user identity is deletedUserIdentity userIdentity
user.preAuthBefore authentication is attemptedAuthCredentials authData
user.onAuthSuccessAfter a successful authenticationAuthCredentials authData (password stripped), User user
user.onAuthFailureAfter a failed authenticationAuthCredentials authData (password stripped), DbUser dbUser
user.onDocumentCreateAfter a document is createdDocument document (binary data stripped)
user.onDocumentUpdateAfter a document is updatedDocument document (binary data stripped)
user.onDocumentDeleteWhen a document is deletedDocument document (binary data stripped)
user.onAddressCreateAfter an address is createdAddress address
user.onAddressUpdateBefore an address is updatedAddress address, DbAddress existing
user.onAddressUpdatedAfter an address is updatedAddress address
user.onAddressDeleteWhen an address is deletedAddress address
user.onUserContactCreateAfter a user contact is createdUserContact userContact
user.onUserContactUpdateBefore a user contact is updatedUserContact userContact, DbUserContact existing
user.onUserContactUpdatedAfter a user contact is updatedUserContact userContact
user.onUserContactDeleteWhen a user contact is deletedUserContact userContact
user.onUserPositionCreateAfter a user position record is createdUserPosition userPosition
user.onUserPositionDeleteWhen a user position record is deletedUserPosition userPosition
user.onUserRoleCreateAfter a user role is assignedUserRole userRole
user.onUserRoleDeleteWhen a user role is removedUserRole userRole
user.onUserRatifiedAfter a user KYC ratification result is storedRatifyResult result
user.onOrganisationRatifiedAfter an organisation KYC ratification result is storedRatifyResult result
user.onManualRatifyWhen a manual ratify result is submittedString ratifyResultData
user.postRatifyBatchCompletionAfter a ratify batch job completesString queueName, RatifyBatchType type, Long batchId, String checks
user.onPasswordUpdateWhen a user password is changedUserIdentityPasswordChange passwordChange
user.onUserIdentityPasswordChangeInitWhen a password-change flow is initiatedPasswordChangeInit initiatePasswordChange

Credential fields (password, secretOrKey, info) are always stripped before these events fire so that plugin code cannot read or log credentials.

Attachment events

Injection PointWhen firedParameters@Named keys
attachment.onGetAttachmentsWhen a list of attachments is retrievedList<Attachment> attachments, String fields

Property events

Injection PointWhen firedParameters@Named keys
property.postPropertyCreateAfter a new property is createdProperty property
property.onPropertyUpdateWhen a property is updatedProperty property
property.onPropertyDeleteWhen a property is deletedProperty property
property.onPropertyGetWhen properties are retrievedList<DbProperty> properties

Property is a global platform resource with no tenant concept. All property injection points fire with tenantId = null.


5. Implementation Plugins

Implementation plugins provide a complete custom implementation of a platform interface. The plugin class extends a Pluggable* base class and is registered by class name in configuration. When the platform needs an instance of that interface it calls UK.Runtime.getBean(className), which returns the plugin's CDI-managed instance.

5.1 Fraud Event Handler

Use case: Provide a custom fraud decision engine. The platform calls handle() for each fraud event and uses the returned result.

Base class: com.ukheshe.services.fraud.publish.pluggable.PluggableFraudEventPublishHandler

Interface method:

FraudEventPublishResponse handle(FraudEventPublishEvent fraudPublishEvent)

Plugin example:

@PluginDescriptor(name = "my-fraud-engine", version = "1.0")
@Singleton
public class MyFraudPlugin extends PluggableFraudEventPublishHandler {

    @Override
    public FraudEventPublishResponse handle(FraudEventPublishEvent event) {
        FraudEventPublishResponse response = new FraudEventPublishResponse();
        // Inspect event.getMessage(), event.getMessageType(), event.getTenantId() etc.
        response.setFraudResult(FraudEventPublishResult.NOT_FRAUD);
        return response;
    }
}

Configuration property:

fraud.event.handlers.<handlerType> = com.example.MyFraudPlugin

Where <handlerType> matches the handlerType field on the FraudEventPublishEvent sent by the transaction pipeline (e.g. MYENGINE).

Field mapping utilities:

FraudEventPublishHandler provides static helpers for reading tenant-specific field mappings from properties:

Map<String, String> requestFields  = FraudEventPublishHandler.getRequestConfig(event);
Map<String, String> responseFields = FraudEventPublishHandler.getResponseConfig(event);

Property key format:

fraud.event.handler.<handlerType>.field-mappings.request.<messageTypeRegex>           # global
fraud.event.handler.<handlerType>.field-mappings.request.<messageTypeRegex>.<tenantId> # tenant override

5.2 Wallet Type

Use case: Provide a custom wallet type with overridden balance calculation, custom charging logic, or bespoke limit behaviour. The platform instantiates one wallet object per wallet access.

Base class: com.ukheshe.services.wallet.pluggable.PluggableWallet

Key base class method:

protected void setSuperClass(BiDirectionalDelegatingWallet superclass)

Call setSuperClass in @PostConstruct, passing an injected BiDirectionalDelegatingLimitCheckingWallet. This wires the bidirectional delegation that makes super.* calls work correctly through managed CDI beans (see note below).

Plugin example:

@PluginDescriptor(name = "high-balance-wallet", version = "1.0")
@Dependent
public class HighBalanceWallet extends PluggableWallet {

    @PostConstruct
    public void init() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingLimitCheckingWallet.class));
    }

    @Override
    public BigDecimal getCurrentBalance() {
        // override — calls the real implementation via super
        return super.getCurrentBalance().add(BONUS);
    }
}

Configuration:

Set the implementationClass field on the wallet type DB record to the fully qualified class name of your plugin class (e.g. com.example.HighBalanceWallet). The WalletFactory uses this field to look up and instantiate the correct implementation for each wallet.

Why setSuperClass? Quarkus generates CDI proxy classes at build time. A plugin loaded at runtime cannot directly extend a managed bean; if it tried, calling super.method() would hit an unmanaged instance where all @Inject fields are null. PluggableWallet + BiDirectionalDelegatingLimitCheckingWallet solve this: the plugin extends the plain PluggableWallet POJO, which delegates to the managed bean via a bidirectional reference. When the managed bean calls this.method() internally, it is redirected to the plugin subclass so standard Java polymorphism works as expected.


5.3 Payment Gateway

Use case: Implement a custom payment gateway (e.g. a new card acquirer, mobile money provider, or voucher system).

Base class: com.ukheshe.services.payment.gateway.pluggable.PluggablePaymentGateway

Interface: com.ukheshe.services.payment.gateway.IPaymentGateway

Bidirectional delegate interface: com.ukheshe.services.payment.gateway.pluggable.BiDirectionalDelegatingPaymentGateway

Call setSuperClass(BiDirectionalDelegatingPaymentGateway) in @PostConstruct to wire bidirectional delegation. The concrete bean to inject is BasePaymentGateway, which implements this interface.

Key methods the plugin must implement:

MethodPurpose
void init(DbPayment dbPayment)Called first with the payment record; call super.init() to delegate to the base gateway
void initiatePayment(Object gatewaySpecificData)Initiate payment with the external provider
void paymentModified(Object gatewaySpecificData)Handle an async update to an existing payment
void sync()Poll / reconcile payment state
void cancel()Cancel a pending payment
void reverse(Object gatewaySpecificData)Reverse a completed payment
void expire()Mark a timed-out payment as expired
void refund(DbRefund refund)Process a refund
boolean supportsPreProcessing()Whether the gateway supports pre-auth holds
boolean supportsReverse()Whether the gateway supports reversals
boolean supportsHold()Whether the gateway supports authorisation holds
List<PaymentProof> getPaymentProofs(DbPayment, String fields)Return payment proof documents

Plugin example:

@PluginDescriptor(name = "my-payment-gateway", version = "1.0")
@ApplicationScoped
public class MyPaymentGateway extends PluggablePaymentGateway {

    @PostConstruct
    public void setup() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingPaymentGateway.class));
    }

    @Override
    public void initiatePayment(Object data) {
        // call external provider
    }

    @Override
    public boolean supportsReverse() { return false; }
    // ... implement remaining interface methods
}

Registration: The plugin class must be registered in the payment gateway factory. Contact the platform team to add a new gateway type string mapping to your class, or use an existing type string with a property override.


5.4 Withdrawal Gateway

Use case: Implement a custom withdrawal gateway (e.g. a new bank EFT rail, mobile wallet disbursement, or ATM network).

Base class: com.ukheshe.services.payment.gateway.pluggable.PluggableWithdrawalGateway

Interface: com.ukheshe.services.payment.gateway.IWithdrawalGateway

Bidirectional delegate interface: com.ukheshe.services.payment.gateway.pluggable.BiDirectionalDelegatingWithdrawalGateway

Call setSuperClass(BiDirectionalDelegatingWithdrawalGateway) in @PostConstruct. The concrete bean to inject is BaseWithdrawalGateway, which implements this interface.

Key methods:

MethodPurpose
void init(DbWithdrawal dbWithdrawal)Called first with the withdrawal record
void processWithdrawal()Send the withdrawal to the external provider
void withdrawalModified(Object gatewaySpecificData)Handle an async update
WithdrawalQuote withdrawalQuote(double amount)Return a fee/FX quote for the amount
void cancel()Cancel a pending withdrawal
void expireWithdrawal()Mark a timed-out withdrawal expired
void sync()Poll / reconcile withdrawal state
boolean supportsPreProcessing()Whether the gateway supports pre-processing
WithdrawalLimits getWithdrawalLimits(Long tenantId, String currency, String gateway, WithdrawalType, WithdrawalSubType)Return configured limits

Plugin example:

@PluginDescriptor(name = "my-withdrawal-gateway", version = "1.0")
@ApplicationScoped
public class MyWithdrawalGateway extends PluggableWithdrawalGateway {

    @PostConstruct
    public void setup() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingWithdrawalGateway.class));
    }

    @Override
    public void processWithdrawal() {
        DbWithdrawal wd = getDbWithdrawal();
        // call external provider
    }
    // ... implement remaining methods
}

Registration: Same as payment gateway — register the type string in the withdrawal gateway factory.


5.5 Loan Gateway

Use case: Implement a custom loan gateway to connect to an external lending provider (credit bureau, BNPL platform, or in-house loan engine).

Base class: com.ukheshe.services.loan.gateway.pluggable.PluggableLoanGateway

Interface: com.ukheshe.services.loan.gateway.ILoanGateway

Bidirectional delegate interface: com.ukheshe.services.loan.gateway.pluggable.BiDirectionalDelegatingLoanGateway

Concrete delegate bean: com.ukheshe.services.loan.gateway.pluggable.BiDirectionalDelegatingBaseLoanGateway (@Dependent )

Call setSuperClass(BiDirectionalDelegatingLoanGateway) in @PostConstruct, injecting the BiDirectionalDelegatingBaseLoanGateway bean.

Key methods:

MethodPurposeBase impl?
void init(DbLoan dbLoan)Initialise the gateway with the loan recordYes — call super.init()
void initiateLoan(Object gatewaySpecificData)Submit the loan to the external providerNo — must override
LoanOptIn loanOptIn(NewLoanOptIn newLoanOptIn)Enrol a customer into a loan productNo — must override
void loanOptOut(NewLoanOptOut loanOptOut)Remove a customer from a loan productYes
void loanRePayment(DbLoan existingLoan, NewLoanRePayment loanRePayment)Record a loan repaymentYes
void updateLoan(Object gatewaySpecificData)Handle an async update to an existing loanNo — must override
LoanLimit loanLimit(String customerId, String productId)Return current loan limits for a customerNo — must override
Loan getLoan(DbLoan dbLoan)Fetch current loan state from the providerNo — must override
List<Loan> getLoans(DbLoan filter, int offset, int limit)List loans matching a filterNo — must override
boolean supportsPreProcessing()Whether the gateway supports pre-processingYes
boolean supportsReverse()Whether the gateway supports reversalsYes
List<LoanTransaction> getLoanTransactions(long tenantId, long loanId, Long walletId)Return transaction history for a loanYes

Plugin example:

@PluginDescriptor(name = "my-loan-gateway", version = "1.0")
@ApplicationScoped
public class MyLoanGateway extends PluggableLoanGateway {

    @Inject BiDirectionalDelegatingBaseLoanGateway baseGateway;

    @PostConstruct
    public void setup() {
        setSuperClass(baseGateway);
    }

    @Override
    public void initiateLoan(Object data) {
        // call external lending provider
    }

    @Override
    public LoanOptIn loanOptIn(NewLoanOptIn newLoanOptIn) {
        // enrol customer
        return new LoanOptIn();
    }
    // ... implement remaining no-base-impl methods
}

Registration: Contact the platform team to register the gateway class for the relevant loan type.


5.6 Remittance Gateway

Use case: Implement a custom remittance gateway (cross-border money transfer, mobile money payout, or FX provider).

Base class: com.ukheshe.services.remittance.gateway.pluggable.PluggableRemittanceGateway

Interface: com.ukheshe.services.remittance.gateway.IRemittanceGateway

Bidirectional delegate interface: com.ukheshe.services.remittance.gateway.pluggable.BiDirectionalDelegatingRemittanceGateway

Concrete delegate bean: com.ukheshe.services.remittance.gateway.pluggable.BiDirectionalDelegatingBaseRemittanceGateway (`@Dependent)

Call setSuperClass(BiDirectionalDelegatingRemittanceGateway) in @PostConstruct, injecting the BiDirectionalDelegatingBaseRemittanceGateway bean.

Key methods:

MethodPurposeBase impl?
void init(DbRemittance dbRemittance)Initialise the gateway with the remittance recordYes — call super.init()
void initiateRemittance(Object gatewaySpecificData)Submit the remittance to the external providerNo — must override
void getRemittance(DbRemittance dbRemittance)Fetch current remittance state from the providerNo — must override
void remittanceModified(Object gatewaySpecificData)Handle an async update to an existing remittanceYes
void cancel()Cancel a pending remittanceYes
void reverse(Object gatewaySpecificData)Reverse a completed remittanceYes
void userEnrollment(DbRemittanceUserEnrollment enrollment)Enrol a user with the remittance providerYes
void getUserEnrollmentStatus(DbRemittanceUserEnrollment enrollment)Check a user's enrolment statusYes
void updateUserEnrollment(DbRemittanceUserEnrollment enrollment)Update a user's enrolment recordYes
boolean supportsPreProcessing()Whether the gateway supports pre-processingYes
boolean supportsReverse()Whether the gateway supports reversalsYes
void sync()Poll / reconcile remittance stateYes
DbRemittance getDbRemittance()Return the current DB recordYes
RemittanceQuickQuoteResponse getQuickQuotes(RemittanceQuickQuoteRequest request)Return indicative quotesYes
CompletionStage<List<ConfirmationQuotesResponse>> getConfirmationQuotes(DbRemittance dbRemittance)Return confirmation quotesYes

Plugin example:

@PluginDescriptor(name = "my-remittance-gateway", version = "1.0")
@ApplicationScoped
public class MyRemittanceGateway extends PluggableRemittanceGateway {

    @Inject BiDirectionalDelegatingBaseRemittanceGateway baseGateway;

    @PostConstruct
    public void setup() {
        setSuperClass(baseGateway);
    }

    @Override
    public void initiateRemittance(Object data) {
        // call external remittance provider
    }

    @Override
    public void getRemittance(DbRemittance dbRemittance) {
        // fetch current state
    }
    // ... override other methods as needed
}

Registration: Contact the platform team to register the gateway class for the relevant remittance type.


5.7 Payment Processor

Use case: Customise how a payment is enriched before creation, how its result is interpreted, or how completion is attempted. Processors sit between the Conductor API layer and the payment service.

Base class: com.ukheshe.eclipse.conductor.service.payments.PluggablePaymentProcessor

Interface: com.ukheshe.eclipse.conductor.service.payments.IPaymentProcessor

Key methods:

MethodPurpose
void enrichForCreation(NewEclipsePayment, NewPayment)Populate payment-service fields from the conductor request
void enrichWithUpdates(UpdatedPaymentTransactionData, Payment)Apply async update data to the payment record
PaymentResult enrichResult(PaymentResult, Payment)Post-process the result returned to the API caller
Payment attemptPaymentCompletion(long tenantId, Payment)Try to complete a deferred payment
Reservation enrichForRefund(Payment, EclipsePaymentRefund, NewPaymentRefund)Prepare a refund reservation
void validatePaymentReceivingWallet(NewPayment)Validate that the destination wallet can receive this payment

Plugin example:

@PluginDescriptor(name = "my-payment-processor", version = "1.0")
@ApplicationScoped
public class MyPaymentProcessor extends PluggablePaymentProcessor {

    @PostConstruct
    public void setup() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingMyBaseProcessor.class));
    }

    @Override
    public void enrichForCreation(NewEclipsePayment eclipsePayment, NewPayment newPayment) {
        super.enrichForCreation(eclipsePayment, newPayment); // delegates to base
        // additional enrichment
    }
}

Registration: The processor factory selects a processor based on payment type. Contact the platform team to wire a new payment type to your plugin class.


5.8 Withdrawal Processor

Use case: Customise enrichment, fee calculation, or limit enforcement for a withdrawal flow.

Base class: com.ukheshe.eclipse.conductor.service.withdrawals.PluggableWithdrawalProcessor

Interface: com.ukheshe.eclipse.conductor.service.withdrawals.IWithdrawalProcessor

Key methods:

MethodPurpose
Long enrichForCreation(Wallet, NewEclipseWalletWithdrawal, Withdrawal, Long destinationWalletId)Enrich the withdrawal before creation; return destination wallet ID
void enrichWithUpdates(Wallet, UpdatedWalletWithdrawal, Withdrawal)Apply an async update to the withdrawal record
WalletWithdrawalResult enrichResult(Wallet, WalletWithdrawalResult, Withdrawal)Post-process the result
Fee getWithdrawalFee(long tenantId, Wallet, String type, BigDecimal amount)Return the fee for this withdrawal
WithdrawalLimits getWithdrawalLimit(long tenantId, Wallet, String type)Return applicable limits

Plugin example:

@PluginDescriptor(name = "my-withdrawal-processor", version = "1.0")
@ApplicationScoped
public class MyWithdrawalProcessor extends PluggableWithdrawalProcessor {

    @PostConstruct
    public void setup() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingMyBaseWithdrawalProcessor.class));
    }

    @Override
    public Fee getWithdrawalFee(long tenantId, Wallet wallet, String type, BigDecimal amount) {
        // custom fee logic
        return new Fee(amount.multiply(BigDecimal.valueOf(0.01)));
    }
}

Registration: Contact the platform team to register your processor class for the relevant withdrawal type.


6. Listener Plugins

Listener plugins let you react to service lifecycle events across any microservice. Each service exposes a Pluggable*Listener class that you extend. Overriding a method gives you a pre-post hook around that lifecycle event, with access to full CDI injection.

Listener chain

Every service has a four-layer listener chain:

SilentXxxListener          (no-op base — defines the interface)
    └── PublishingXxxListener    (publishes domain events via UK.Events)
            └── PluggableXxxListener  (fires injection points via UK.Plugins)
                    └── EclipseXxxListener  (platform overrides; extend this one)

Your plugin should extend EclipseXxxListener (or a service-specific subclass of it provided by the platform). When you call super.method(...), the call propagates up through PluggableXxxListener (firing the injection point) and then PublishingXxxListener (publishing the domain event). Both always run unless you intentionally skip the super call.

Available Pluggable*Listener classes

ServiceClass
Walletcom.ukheshe.services.wallet.listener.PluggableWalletListener
Paymentcom.ukheshe.services.payment.listener.PluggablePaymentListener
Loancom.ukheshe.services.loan.listener.PluggableLoanListener
Remittancecom.ukheshe.services.remittance.listener.PluggableRemittanceListener
Usercom.ukheshe.services.user.listener.PluggableUserListener
Attachmentcom.ukheshe.services.attachment.listener.PluggableAttachmentListener
Propertycom.ukheshe.services.property.listener.PluggablePropertyListener

Plugin example

@PluginDescriptor(name = "payment-audit", version = "1.0")
@ApplicationScoped
public class PaymentAuditListener extends EclipsePaymentListener {

    @Inject AuditService audit;

    @Override
    public void postCreatePayment(Payment payment) {
        audit.record(payment.getTenantId(), "payment.create", payment.getPaymentId());
        super.postCreatePayment(payment); // fires injection point + publishes domain event
    }

    @Override
    public void postPaymentComplete(Payment payment) {
        audit.record(payment.getTenantId(), "payment.complete", payment.getPaymentId());
        super.postPaymentComplete(payment);
    }
}

Registration: Contact the platform team to configure the listener class for the relevant service. The platform instantiates one listener per service via CDI and calls it for every lifecycle event.


7. Building a Plugin JAR

A plugin is a regular JAR with no main class. It must:

  1. Declare one @PluginDescriptor-annotated class — used for identification and versioning.
  2. Implement handler methods or extend a Pluggable* base class (or both).
  3. Not bundle platform classes — classes from com.ukheshe.* are available at runtime via the parent class loader and must not be included in the plugin JAR.

The plugin-sdk.jar at the root of the repository contains the platform API classes needed for compilation. Reference it as a provided-scope dependency.

<dependency>
    <groupId>com.ukheshe</groupId>
    <artifactId>plugin-sdk</artifactId>
    <version>${project.version}</version>
    <scope>provided</scope>
</dependency>

Build a fat JAR that includes your own classes and any third-party libraries not already on the platform classpath.


8. Quick Reference

Minimal injection-point plugin

@PluginDescriptor(name = "my-plugin", version = "1.0", description = "Example plugin")
public class MyPluginDescriptor { }

@Singleton
public class MyHandlers {

    @OnNamedInjectionPoint(injectionPoint = "wallet.postWalletCreate", priority = 1)
    public void onWalletCreated(Wallet wallet) {
        // runs after every wallet is created
    }
}

Minimal fraud handler plugin

@PluginDescriptor(name = "my-fraud-plugin", version = "1.0")
public class MyFraudDescriptor { }

@Singleton
public class MyFraudHandler extends PluggableFraudEventPublishHandler {

    @Override
    public FraudEventPublishResponse handle(FraudEventPublishEvent event) {
        FraudEventPublishResponse res = new FraudEventPublishResponse();
        res.setFraudResult(FraudEventPublishResult.NOT_FRAUD);
        return res;
    }
}

Property: fraud.event.handlers.MYENGINE = com.example.MyFraudHandler

Minimal wallet type plugin

@PluginDescriptor(name = "my-wallet", version = "1.0")
public class MyWalletDescriptor { }

@Dependent
public class MyWallet extends PluggableWallet {

    @PostConstruct
    public void init() {
        setSuperClass(UK.Runtime.getBean(BiDirectionalDelegatingLimitCheckingWallet.class));
    }

    @Override
    public BigDecimal getCurrentBalance() {
        return super.getCurrentBalance(); // add custom logic here
    }
}

Wallet type DB record: implementationClass = com.example.MyWallet

Minimal loan gateway plugin

@PluginDescriptor(name = "my-loan-gateway", version = "1.0")
public class MyLoanDescriptor { }

@ApplicationScoped
public class MyLoanGateway extends PluggableLoanGateway {

    @Inject BiDirectionalDelegatingBaseLoanGateway baseGateway;

    @PostConstruct
    public void init() {
        setSuperClass(baseGateway);
    }

    @Override
    public void initiateLoan(Object data) { /* call provider */ }

    @Override
    public LoanOptIn loanOptIn(NewLoanOptIn req) { return new LoanOptIn(); }

    @Override
    public void updateLoan(Object data) { }

    @Override
    public LoanLimit loanLimit(String customerId, String productId) { return null; }

    @Override
    public Loan getLoan(DbLoan dbLoan) { return null; }

    @Override
    public List<Loan> getLoans(DbLoan filter, int offset, int limit) { return List.of(); }
}

Minimal listener plugin

@PluginDescriptor(name = "my-listener", version = "1.0")
public class MyListenerDescriptor { }

@ApplicationScoped
public class MyListener extends EclipseUserListener {

    @Override
    public void onUserCreated(User user) {
        // react to new user
        super.onUserCreated(user); // always call super to fire injection point + domain event
    }
}