From 5f6bd972aa88f89f5d244551ea166883a4180ab8 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Mon, 15 Jun 2026 09:24:24 -0500 Subject: [PATCH] FINERACT-2455: Func. documentation: Working Capital EIR Accounting --- .../src/docs/en/chapters/features/index.adoc | 2 + .../working-capital-cash-accounting.adoc | 344 ++++++++++++++++++ ...ng-capital-eir-calculation-accounting.adoc | 298 +++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 fineract-doc/src/docs/en/chapters/features/working-capital-cash-accounting.adoc create mode 100644 fineract-doc/src/docs/en/chapters/features/working-capital-eir-calculation-accounting.adoc diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc index bcc3155cc38..57927ce2224 100644 --- a/fineract-doc/src/docs/en/chapters/features/index.adoc +++ b/fineract-doc/src/docs/en/chapters/features/index.adoc @@ -32,3 +32,5 @@ include::working-capital-discount.adoc[leveloffset=+1] include::savings-interest-posting.adoc[leveloffset=+1] include::working-capital-breach-management.adoc[leveloffset=+1] include::working-capital-breach-grace-days.adoc[leveloffset=+1] +include::working-capital-cash-accounting.adoc[leveloffset=+1] +include::working-capital-eir-calculation-accounting.adoc[leveloffset=+1] diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-cash-accounting.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-cash-accounting.adoc new file mode 100644 index 00000000000..74fca323a88 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-cash-accounting.adoc @@ -0,0 +1,344 @@ += Working Capital Loan — Cash-Based Accounting + +== Overview + +Working Capital Loan products support cash-based accounting, where journal entries are posted only when cash actually moves. The accounting rule is selected at the product level via `accountingRule = CASH_BASED`. Each monetary transaction on a Working Capital Loan (repayment, goodwill credit, reversals) drives a balanced set of journal entries against GL accounts mapped on the loan product. + +=== Purpose + +Cash-based accounting allows lenders to record the financial impact of Working Capital Loan transactions in the general ledger at the moment cash is received or disbursed, without recognizing accrued income or receivables in advance. This is the simpler of the two accounting bases and is appropriate when income recognition should track cash flow rather than economic activity. + +=== Scope + +The scope of this document includes: + +* Working Capital Loan product accounting rule configuration +* GL account mappings required for cash-based accounting +* Journal entries posted on repayment (regular and charged-off) +* Journal entries posted on goodwill credit +* Reversal journal entries +* Fund source mapping by payment type +* Read-side mapping retrieval + +EIR-specific discount fee amortization journal entries are documented separately in `working-capital-eir-calculation-accounting.adoc`. + +=== Applicability + +* Working Capital Loan products with `accountingRule = CASH_BASED` +* Working Capital Loans linked to such products + +=== Definitions and Key Concepts + +*Accounting Rule:* Product-level configuration that selects the accounting basis. For Working Capital Loans the supported values are `NONE` (no journal entries posted) and `CASH_BASED`. + +*GL Account Mapping:* Persisted link between a Working Capital Loan product, an accounting placeholder (e.g., `LOAN_PORTFOLIO`), and a concrete `GLAccount` row. + +*Fund Source:* The bank or cash GL account used as the contra side for cash movements. May be overridden per payment type. + +*Charged-Off Loan:* A loan marked as `isChargedOff = true`. After charge-off, repayments post to `INCOME_FROM_RECOVERY` instead of the regular portfolio/fees/penalties accounts. + +== Design Decisions and Considerations + +=== Dedicated Working Capital Accounting Processor + +Working Capital Loans use a dedicated `WorkingCapitalLoanAccountingProcessor` interface with a single `CashBasedAccountingProcessorForWorkingCapitalLoan` implementation. This keeps Working Capital posting logic independent from the standard Loan accounting processors, which avoids coupling the Working Capital allocation model (`WorkingCapitalLoanTransactionAllocation`) to the standard loan data structures. + +=== Accrual Basis Not Supported + +The `WorkingCapitalAccountingRuleType` enum only exposes `NONE` and `CASH_BASED`. Accrual-based accounting is not implemented for Working Capital Loan products. + +== Database Design + +=== Existing Tables + +*m_product_loan_gl_account_mapping*: Stores the mapping between a Working Capital Loan product, a cash account placeholder (`CashAccountsForLoan` value), an optional payment type / charge / code value, and the target `GLAccount`. Working Capital mappings are identified by `product_type = PortfolioProductType.WORKING_CAPITAL_LOAN.value` (4). + +*acc_gl_journal_entry*: Standard journal entry table. Cash-based Working Capital postings are written here with a transaction id prefixed by `WC` (see `AccountingProcessorHelper.WORKING_CAPITAL_LOAN_TRANSACTION_IDENTIFIER`) and `entity_type = WORKING_CAPITAL_LOAN`. + +=== Changes to Existing Tables + +==== m_wc_loan_product + +The product-level accounting rule is persisted in: + +[cols="1,2,1,3",options="header"] +|=== +| Column Name | Type | Constraints | Description +| accounting_type | VARCHAR(20) | not null, default `NONE` | Selected `WorkingCapitalAccountingRuleType` (`NONE` or `CASH_BASED`) +|=== + +== Configuration + +=== Loan Product Configuration + +The accounting rule and GL account mappings are supplied when creating or updating a Working Capital Loan product: + +[source,json] +---- +{ + "accountingRule": "CASH_BASED", + "fundSourceAccountId": 1, + "loanPortfolioAccountId": 2, + "transfersInSuspenseAccountId": 3, + "receivableFeeAccountId": 19, + "receivablePenaltyAccountId": 20, + "incomeFromFeeAccountId": 6, + "incomeFromPenaltyAccountId": 6, + "incomeFromRecoveryAccountId": 7, + "writeOffAccountId": 8, + "overpaymentLiabilityAccountId": 9, + "goodwillCreditAccountId": 16, + "incomeFromGoodwillCreditFeesAccountId": 12, + "incomeFromGoodwillCreditPenaltyAccountId": 13, + "chargeOffExpenseAccountId": 17, + "chargeOffFraudExpenseAccountId": 18, + "incomeFromChargeOffFeesAccountId": 10, + "incomeFromChargeOffPenaltyAccountId": 11, + "paymentChannelToFundSourceMappings": [ + { "paymentTypeId": 1, "fundSourceAccountId": 100 } + ], + "feeToIncomeAccountMappings": [ + { "chargeId": 1, "incomeAccountId": 6 } + ], + "penaltyToIncomeAccountMappings": [ + { "chargeId": 2, "incomeAccountId": 6 } + ], + "chargeOffReasonToExpenseAccountMappings": [ + { "chargeOffReasonCodeValueId": 1, "expenseAccountId": 17 } + ], + "writeOffReasonsToExpenseMappings": [ + { "writeOffReasonCodeValueId": 1, "expenseAccountId": 8 } + ] +} +---- + +=== GL Account Mappings + +When `accountingRule = CASH_BASED`, the platform persists one `ProductToGLAccountMapping` row per supplied mapping. The placeholder enum is `CashAccountsForLoan`: + +* *Fund Source (Asset)*: `fundSourceAccountId` — cash account that is debited on repayment and may be overridden per payment type via `paymentChannelToFundSourceMappings` +* *Loan Portfolio (Asset)*: `loanPortfolioAccountId` — credited for the principal portion of regular repayments +* *Fees Receivable (Asset)*: `receivableFeeAccountId` — credited for the fees portion of regular repayments +* *Penalties Receivable (Asset)*: `receivablePenaltyAccountId` — credited for the penalty portion of regular repayments +* *Overpayment Liability (Liability)*: `overpaymentLiabilityAccountId` — credited for any amount in excess of principal + fees + penalties +* *Income from Recovery (Income)*: `incomeFromRecoveryAccountId` — credited for all repayment portions when the loan is charged off +* *Goodwill Credit (Expense)*: `goodwillCreditAccountId` — debited for principal + overpayment portion of a goodwill credit +* *Income from Goodwill Credit Fees (Income)*: `incomeFromGoodwillCreditFeesAccountId` — debited for fees portion of a goodwill credit +* *Income from Goodwill Credit Penalty (Income)*: `incomeFromGoodwillCreditPenaltyAccountId` — debited for penalty portion of a goodwill credit +* *Charge-off Expense (Expense)* / *Charge-off Fraud Expense (Expense)*: optional, used by charge-off reason mappings + +Additional advanced mappings: + +* *Payment Channel to Fund Source*: per-payment-type override of the default fund source account +* *Fee/Penalty to Income*: per-charge income account override +* *Charge-off Reason to Expense*: per-reason expense account override +* *Write-off Reason to Expense*: per-reason expense account override + +[IMPORTANT] +==== +GL account mappings are validated at product create/update time. Duplicated entries in `paymentChannelToFundSourceMappings`, `feeToIncomeAccountMappings`, `penaltyToIncomeAccountMappings`, `chargeOffReasonToExpenseAccountMappings`, or `writeOffReasonsToExpenseMappings` are rejected with `duplicated.enrty.for.`. +==== + +== API Design + +=== Endpoints + +==== Configure Accounting on a Working Capital Loan Product + +Accounting fields are part of the standard Working Capital Loan product endpoints: + +[source] +---- +POST /v1/working-capital-loan-products +PUT /v1/working-capital-loan-products/{productId} +PUT /v1/working-capital-loan-products/external-id/{externalProductId} +GET /v1/working-capital-loan-products/{productId} +GET /v1/working-capital-loan-products/external-id/{externalProductId} +GET /v1/working-capital-loan-products/template +---- + +The `GET` responses expose: + +* `accountingRule` — current rule as a `StringEnumOptionData` +* `accountingMappings` — map of placeholder name to `GLAccountData` (only populated when the rule is cash-based) +* `accountingMappingOptions` (template) — lookup data for the UI: fund source, payment channels, fee/penalty income, charge-off / write-off reason mappings + +== Validation Rules + +=== General Rules + +* `accountingRule` must be a valid `WorkingCapitalAccountingRuleType` name (`NONE` or `CASH_BASED`). +* When `accountingRule = NONE`, no GL account mappings are persisted; if previously cash-based, the prior mappings are deleted. + +=== Validation Rules for Mappings + +* Each advanced mapping array (`paymentChannelToFundSourceMappings`, `feeToIncomeAccountMappings`, `penaltyToIncomeAccountMappings`, `chargeOffReasonToExpenseAccountMappings`, `writeOffReasonsToExpenseMappings`) must not contain duplicates on its key field. + +== Business Rules + +=== Branch Closure Check + +Before posting any journal entries for a transaction date, the processor calls `helper.checkForBranchClosures(...)`. If the transaction date falls on or before the latest accounting closure for the loan's branch, posting is rejected. + +=== Repayment — Regular + +When a repayment is posted on a non-charged-off Working Capital Loan, the processor reads principal, fees, and penalties portions from the `WorkingCapitalLoanTransactionAllocation`, computes the overpayment portion as the residual of the transaction amount, and posts: + +* a credit per non-zero portion (principal → `LOAN_PORTFOLIO`, fees → `FEES_RECEIVABLE`, penalties → `PENALTIES_RECEIVABLE`, overpayment → `OVERPAYMENT`) +* a single debit to the fund source for the full transaction amount, resolved against `paymentChannelToFundSourceMappings` using `paymentDetail.paymentType.id` when present + +=== Repayment — Charged Off + +When the loan is already charged off, all three credit legs are redirected to `INCOME_FROM_RECOVERY` instead of the regular portfolio/receivable accounts. The fund source debit is unchanged. + +=== Goodwill Credit + +For a goodwill credit on a non-charged-off loan, the processor posts: + +* debit `GOODWILL_CREDIT` for principal + overpayment portion +* debit `INCOME_FROM_GOODWILL_CREDIT_FEES` for fees portion +* debit `INCOME_FROM_GOODWILL_CREDIT_PENALTY` for penalty portion +* credit `LOAN_PORTFOLIO`, `FEES_RECEIVABLE`, `PENALTIES_RECEIVABLE`, `OVERPAYMENT` for the matching portions + +Posting a goodwill credit while the loan is charged off raises `NotImplementedException("Charge off is not implemented yet for Goodwill Credit for Working Capital Loan")`. + +=== Unsupported Transaction Types + +If `postJournalEntries` is invoked with a transaction type other than `REPAYMENT` or `GOODWILL_CREDIT`, it raises `NotImplementedException("Post Journal Entries is not implemented yet for for Working Capital Loan")`. Discount fee amortization and its adjustment are handled by dedicated processor methods (see `working-capital-eir-calculation-accounting.adoc`). + +=== Reversal + +`postReversalJournalEntries` reads existing journal entries for the transaction identifier `WC` and `entityType = WORKING_CAPITAL_LOAN`, then for each one creates a mirror entry with the opposite `JournalEntryType`. The original entries are marked `reversed = true` and linked to their reversal entry. The reversal posting date is `txn.reversedOnDate` when present, otherwise the current business date. + +=== Zero-Amount Skipping + +Each journal leg checks `MathUtil.isGreaterThanZero(amount)` before creating the entry. Zero or null portions produce no journal entry. + +== Accounting Entries + +=== Repayment — Regular + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|Repayment (principal) +|Fund Source (Asset) +|Loan Portfolio (Asset) + +|Repayment (fees) +|Fund Source (Asset) +|Fees Receivable (Asset) + +|Repayment (penalties) +|Fund Source (Asset) +|Penalties Receivable (Asset) + +|Repayment (overpayment) +|Fund Source (Asset) +|Overpayment Liability (Liability) + +|=== + +=== Repayment — Charged Off + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|Repayment (principal) +|Fund Source (Asset) +|Income from Recovery (Income) + +|Repayment (fees) +|Fund Source (Asset) +|Income from Recovery (Income) + +|Repayment (penalties) +|Fund Source (Asset) +|Income from Recovery (Income) + +|Repayment (overpayment) +|Fund Source (Asset) +|Overpayment Liability (Liability) + +|=== + +=== Goodwill Credit (not charged off) + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|Goodwill Credit (principal + overpayment) +|Goodwill Credit (Expense) +|Loan Portfolio (Asset) + +|Goodwill Credit (fees) +|Income from Goodwill Credit Fees (Income) +|Fees Receivable (Asset) + +|Goodwill Credit (penalty) +|Income from Goodwill Credit Penalty (Income) +|Penalties Receivable (Asset) + +|Goodwill Credit (overpayment) +| +|Overpayment Liability (Liability) + +|=== + +=== Reversal + +Every journal entry written for a Working Capital Loan transaction can be reversed. The reversal posts a balanced mirror entry per leg, swapping debit and credit, and marks the original entries `reversed = true`. + +== Example Scenarios + +=== Scenario #1: Regular repayment with fees and overpayment + +**Setup:** + +* Working Capital Loan product with `accountingRule = CASH_BASED` +* Mappings configured for Fund Source, Loan Portfolio, Fees Receivable, Overpayment +* Active loan with outstanding principal 800 and fees 50 + +**Action:** + +A repayment of 1,000 is posted. The allocator records principal = 800, fees = 50, penalties = 0, leaving an overpayment of 150. + +**Expected Behavior:** + +* Debit Fund Source 1,000 +* Credit Loan Portfolio 800 +* Credit Fees Receivable 50 +* Credit Overpayment Liability 150 +* All entries share transaction id `WC` and `entityType = WORKING_CAPITAL_LOAN` + +=== Scenario #2: Repayment after charge-off + +**Setup:** + +* Same product, but the loan was previously charged off (`isChargedOff = true`) +* Mappings include `Income from Recovery` + +**Action:** + +A repayment of 500 is posted with allocation principal = 300, fees = 100, penalties = 100. + +**Expected Behavior:** + +* Debit Fund Source 500 +* Credit Income from Recovery 300 (principal) +* Credit Income from Recovery 100 (fees) +* Credit Income from Recovery 100 (penalties) +* No entries to Loan Portfolio / Fees Receivable / Penalties Receivable + +== Summary + +Working Capital Loan cash-based accounting provides cash-aligned journal entries for repayments and goodwill credits via a dedicated `CashBasedAccountingProcessorForWorkingCapitalLoan`. Key aspects include: + +* Product-level `accountingRule` selecting `NONE` or `CASH_BASED` +* `CashAccountsForLoan`-based GL account mappings, with advanced overrides per payment type, charge, and reason code +* Distinct posting flows for regular vs. charged-off repayments +* Dedicated goodwill credit posting with charge-off explicitly unsupported +* Symmetric reversal that mirrors every leg of the original transaction diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-eir-calculation-accounting.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-eir-calculation-accounting.adoc new file mode 100644 index 00000000000..73dff0cc167 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-eir-calculation-accounting.adoc @@ -0,0 +1,298 @@ += Working Capital Loan — EIR Calculation Accounting + +== Overview + +Working Capital Loans amortize discount fee income across the loan term using an Effective Interest Rate (EIR) schedule. When the COB business step `WC_DISCOUNT_FEE_AMORTIZATION` advances the realized income figure, the accounting processor posts a balanced pair of journal entries that move the newly recognized amount out of the *Deferred Income Liability* placeholder and into income (or, when the loan is charged off, into the charge-off expense placeholder). + +The EIR calculation itself — solver, schedule shape, rate segments — is documented in `working-capital-eir-calculation.adoc`. This document focuses on the journal entries derived from the schedule. + +=== Purpose + +EIR-based income recognition spreads the discount fee over the life of the Working Capital Loan in proportion to its present value at each period, rather than recognizing it at disbursement. The corresponding journal entries keep the general ledger in step with the periodic income figure produced by the amortization schedule. + +=== Scope + +The scope of this document includes: + +* The COB business step that drives EIR income recognition +* Calculation of the periodic amortization amount from the schedule +* Journal entries for `DISCOUNT_FEE_AMORTIZATION` +* Journal entries for `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` +* Charge-off behavior for amortization entries +* Reversal handling + +General Working Capital Loan repayment and goodwill credit accounting are documented in `working-capital-cash-accounting.adoc`. + +=== Applicability + +* Working Capital Loan products with `accountingRule = CASH_BASED` +* Working Capital Loans with `amortizationType = EIR` and a non-zero discount fee, OR with previously recognized discount fee income that needs to be reconciled downward + +=== Definitions and Key Concepts + +*Discount Fee:* The upfront fee charged on a Working Capital Loan, persisted on `loanProductRelatedDetails.discount`. + +*Deferred Income Liability:* The unrecognized portion of the discount fee, sitting as a liability on the lender's balance sheet until it is amortized into income. + +*Realized Income from Discount Fee:* The cumulative amount of discount fee that has already been recognized as income for the loan. Persisted on the loan balance as `realizedIncomeFromDiscountFee`. + +*Schedule Amortization:* The income figure produced by the EIR amortization schedule up to the current business date, accessed via `ProjectedAmortizationScheduleModel.totalActualAmortization()`. + +*Amortization Transaction:* A monetary Working Capital Loan transaction (`DISCOUNT_FEE_AMORTIZATION` or `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT`) that carries the journal-entry amount. + +== Design Decisions and Considerations + +=== Separate Adjustment Path for Downward Reconciliation + +Two transaction types and two processor methods are used: `postJournalEntriesForDiscountFeeAmortization` for the normal increase in realized income and `postJournalEntriesForDiscountFeeAmortizationAdjustment` for downward reconciliation (for example after a discount fee adjustment reduces the discount). The adjustment path swaps debit and credit so the previously recognized income is reversed against the deferred liability rather than recorded as a negative amortization. This avoids posting a debit balance against an income account. + +=== Charged-Off Loans Redirect to Charge-Off Expense + +When the loan is charged off, the income leg of both amortization and adjustment is redirected from `INCOME_FROM_DISCOUNT_FEE` to `CHARGE_OFF_EXPENSE`. This keeps post-charge-off discount fee movement out of the income statement and aligns it with the charge-off expense recognized at the time of charge-off. + +== Database Design + +=== Existing Tables + +*m_wc_loan_balance*: Holds the cumulative realized income for the loan via `realized_income_from_discount_fee`. This value is consulted on every business step run to decide how much new amortization to post and is updated after the journal entries are written. + +*m_wc_loan_amortization_model*: Serialized `ProjectedAmortizationScheduleModel` (JSON CLOB) keyed by loan id. The processor reads this to compute the schedule's `totalActualAmortization` for the current business date. + +*m_wc_loan_transaction*: Stores the `DISCOUNT_FEE_AMORTIZATION` (45) and `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` (47) transactions that carry the journal entry amount. + +*acc_gl_journal_entry*: Receives the balanced debit/credit pair for each amortization or adjustment, with transaction id `WC` and `entityType = WORKING_CAPITAL_LOAN`. + +=== Changes to Existing Tables + +==== m_batch_business_steps + +The COB step that drives EIR income recognition is registered as: + +[cols="1,2,1,3",options="header"] +|=== +| Column Name | Type | Constraints | Description +| job_name | VARCHAR | not null | `WORKING_CAPITAL_LOAN_CLOSE_OF_BUSINESS` +| step_name | VARCHAR | not null | `WC_DISCOUNT_FEE_AMORTIZATION` +| step_order | INT | not null | `6` +|=== + +== Configuration + +=== Loan Product Configuration + +EIR amortization accounting is gated on: + +[source,json] +---- +{ + "amortizationType": "EIR", + "accountingRule": "CASH_BASED", + "deferredIncomeLiabilityAccountId": 4, + "incomeFromDiscountFeeAccountId": 5, + "chargeOffExpenseAccountId": 17 +} +---- + +=== GL Account Mappings + +Required GL account mappings for EIR calculation accounting: + +* *Deferred Income Liability (Liability)*: `deferredIncomeLiabilityAccountId` — the liability that holds unrecognized discount fee income; debited as income is amortized and credited when amortization is reduced +* *Income from Discount Fee (Income)*: `incomeFromDiscountFeeAccountId` — credited as discount fee is recognized into income (or debited on adjustment) when the loan is not charged off +* *Charge-off Expense (Expense)*: `chargeOffExpenseAccountId` — used in place of `Income from Discount Fee` when the loan is charged off + +[IMPORTANT] +==== +The Deferred Income Liability mapping is required whenever `amortizationType = EIR` is used together with `accountingRule = CASH_BASED`. Without it the processor cannot resolve the debit leg for amortization entries. +==== + +== API Design + +EIR amortization journal entries are not exposed through dedicated REST endpoints. They are produced internally by the COB business step. The resulting transactions are visible through the standard Working Capital Loan transaction endpoints: + +[source] +---- +GET /v1/working-capital-loans/{loanId}/transactions/{transactionId} +GET /v1/working-capital-loans/external-id/{loanExternalId}/transactions/external-id/{externalTransactionId} +---- + +== Business Rules + +=== Discount Fee Amortization Business Step + +The `DiscountFeeAmortizationBusinessStep` (COB step `WC_DISCOUNT_FEE_AMORTIZATION`, order 6) runs at end of day. It skips the loan when both of the following are true: + +* `loanProductRelatedDetails.discount` is zero or null, AND +* `loanBalance.realizedIncomeFromDiscountFee` is zero or null + +Otherwise the step delegates to `WorkingCapitalLoanDiscountFeeAmortizationServiceImpl.processDiscountFeeAmortization(loan, businessDate)`. + +=== Amortization Amount Calculation + +The service reads `scheduleAmortization` from the amortization model (`totalActualAmortization`) and `alreadyPosted` from `loanBalance.realizedIncomeFromDiscountFee`. The amount to post is: + +* If the loan is overpaid and `discount > 0`: `amortizationAmount = discount − alreadyPosted` (catch realization up to the full discount on overpayment) +* Otherwise: `amortizationAmount = scheduleAmortization − alreadyPosted` + +If the amortization amount is zero (or both schedule and already-posted are zero on a non-overpaid loan), the step exits without posting. + +=== Posting Direction + +* `amortizationAmount > 0` — create a `DISCOUNT_FEE_AMORTIZATION` transaction for that amount and call `postJournalEntriesForDiscountFeeAmortization`. +* `amortizationAmount < 0` — create a `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` transaction for the absolute value, link it via `m_wc_loan_transaction_relation` (`RELATED`) to the most recent `DISCOUNT_FEE_ADJUSTMENT` transaction, and call `postJournalEntriesForDiscountFeeAmortizationAdjustment`. + +After posting, `loanBalance.realizedIncomeFromDiscountFee` is set to `alreadyPosted + amortizationAmount`. + +=== Branch Closure Check + +Both `postJournalEntriesForDiscountFeeAmortization` and `postJournalEntriesForDiscountFeeAmortizationAdjustment` call `helper.checkForBranchClosures(...)`. If the transaction date falls on or before the latest accounting closure for the loan's branch, posting is rejected. + +=== Zero-Amount Skip + +Both processor methods short-circuit when `MathUtil.isGreaterThanZero(amount)` is false, so a zero-amount transaction never produces journal entries. + +=== Charge-Off Behavior + +When the loan is charged off (`isChargedOff = true`), the income placeholder is replaced by `CHARGE_OFF_EXPENSE` on both the amortization and the adjustment posting. The Deferred Income Liability leg is unchanged. + +=== Reversal + +EIR amortization transactions reverse through the same generic mechanism as other Working Capital Loan transactions: `postReversalJournalEntries` reads the existing entries for transaction id `WC` and writes a mirror entry per leg, then flags the originals as reversed. + +== Accounting Entries + +=== Discount Fee Amortization — Not Charged Off + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|`DISCOUNT_FEE_AMORTIZATION` +|Deferred Income Liability (Liability) +|Income from Discount Fee (Income) + +|=== + +=== Discount Fee Amortization — Charged Off + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|`DISCOUNT_FEE_AMORTIZATION` +|Deferred Income Liability (Liability) +|Charge-off Expense (Expense) + +|=== + +=== Discount Fee Amortization Adjustment — Not Charged Off + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|`DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` +|Income from Discount Fee (Income) +|Deferred Income Liability (Liability) + +|=== + +=== Discount Fee Amortization Adjustment — Charged Off + +[cols="3*"] +|=== +|Transaction Type |Debit |Credit + +|`DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` +|Charge-off Expense (Expense) +|Deferred Income Liability (Liability) + +|=== + +=== Reversal + +Every leg can be reversed; the reversal posts a balanced mirror entry per leg and flags the originals as reversed (see `working-capital-cash-accounting.adoc`). + +== Example Scenarios + +=== Scenario #1: Daily amortization on a non-charged-off loan + +**Setup:** + +* Working Capital Loan product with `amortizationType = EIR` and `accountingRule = CASH_BASED` +* Mappings configured for Deferred Income Liability and Income from Discount Fee +* Discount fee = 1,000, schedule has advanced to `totalActualAmortization = 120` +* `loanBalance.realizedIncomeFromDiscountFee = 100` + +**Action:** + +The `WC_DISCOUNT_FEE_AMORTIZATION` business step runs at end of day. + +**Expected Behavior:** + +* New amortization amount = 120 − 100 = 20 +* A `DISCOUNT_FEE_AMORTIZATION` transaction of 20 is created +* Debit Deferred Income Liability 20, Credit Income from Discount Fee 20 +* `realizedIncomeFromDiscountFee` becomes 120 + +=== Scenario #2: Downward reconciliation after a discount adjustment + +**Setup:** + +* Same product, `realizedIncomeFromDiscountFee = 200` +* A `DISCOUNT_FEE_ADJUSTMENT` has reduced the discount and the schedule now reports `totalActualAmortization = 150` + +**Action:** + +The business step runs. + +**Expected Behavior:** + +* Amortization amount = 150 − 200 = −50, so a `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` of 50 is created +* The adjustment transaction is linked (`RELATED`) to the most recent `DISCOUNT_FEE_ADJUSTMENT` +* Debit Income from Discount Fee 50, Credit Deferred Income Liability 50 +* `realizedIncomeFromDiscountFee` becomes 150 + +=== Scenario #3: Overpaid loan catches up to the full discount + +**Setup:** + +* Same product, loan status is `OVERPAID` +* Discount fee = 1,000, `realizedIncomeFromDiscountFee = 800`, schedule reports `totalActualAmortization = 950` + +**Action:** + +The business step runs. + +**Expected Behavior:** + +* Because the loan is overpaid and `discount > 0`, the amount uses the discount as the target: 1,000 − 800 = 200 +* A `DISCOUNT_FEE_AMORTIZATION` of 200 is created +* Debit Deferred Income Liability 200, Credit Income from Discount Fee 200 +* `realizedIncomeFromDiscountFee` becomes 1,000 + +=== Scenario #4: Amortization on a charged-off loan + +**Setup:** + +* Same product, but the loan was charged off after disbursement +* Mappings include Charge-off Expense + +**Action:** + +The business step posts a 30-unit amortization for the day. + +**Expected Behavior:** + +* A `DISCOUNT_FEE_AMORTIZATION` of 30 is created +* Debit Deferred Income Liability 30, Credit Charge-off Expense 30 (the income placeholder is replaced) +* `realizedIncomeFromDiscountFee` advances by 30 + +== Summary + +EIR calculation accounting recognizes Working Capital Loan discount fee income over time, in proportion to the EIR amortization schedule. Key aspects include: + +* COB step `WC_DISCOUNT_FEE_AMORTIZATION` drives nightly recognition based on the difference between schedule amortization and already-posted income +* `DISCOUNT_FEE_AMORTIZATION` (upward) and `DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT` (downward) transactions carry the journal entry amounts +* Journal entries move value between Deferred Income Liability and Income from Discount Fee, with charged-off loans redirecting the income leg to Charge-off Expense +* The loan balance field `realizedIncomeFromDiscountFee` is the source of truth for what has already been recognized and is updated atomically with the journal entries