diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc new file mode 100644 index 00000000000..9096e788453 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc @@ -0,0 +1,79 @@ +{ + "name": "WorkingCapitalBreachDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "breachFrequency", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "breachFrequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "breachAmountCalculationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "breachAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "breachGraceDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "breachStartDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "nearBreach", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalNearBreachDataV1" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc new file mode 100644 index 00000000000..916e692c3e0 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc @@ -0,0 +1,411 @@ +{ + "name": "WorkingCapitalLoanAccountDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "accountNo", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "externalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "status", + "type": [ + "null", + "org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1" + ] + }, + { + "default": null, + "name": "clientId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "clientAccountNo", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "clientName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "clientOfficeId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "clientExternalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "loanProductId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "loanProductName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "loanProductDescription", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "fundId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "fundName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "principal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "approvedPrincipal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "proposedPrincipal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "netDisbursalAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "numberOfRepayments", + "doc": "Loan term in days, not a count of repayments. Null until the amortization schedule is generated.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "repaymentEvery", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "repaymentFrequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "amortizationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "graceOnArrearsAgeing", + "doc": "Grace days before a period counts as delinquent.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "timeline", + "type": [ + "null", + "org.apache.fineract.avro.loan.v1.LoanApplicationTimelineDataV1" + ] + }, + { + "default": null, + "name": "summary", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1" + ] + }, + { + "default": null, + "name": "charges", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1" + } + ] + }, + { + "default": null, + "name": "disbursementDetails", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1" + } + ] + }, + { + "default": null, + "name": "totalOverpaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "loanProductCounter", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquent", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1" + ] + }, + { + "default": null, + "name": "lastClosedBusinessDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargedOff", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "enableInstallmentLevelDelinquency", + "doc": "True when the loan has a delinquency bucket configured. Working capital classifies delinquency per period, not per installment.", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "originators", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1" + } + ] + }, + { + "default": null, + "name": "breach", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1" + ] + }, + { + "default": null, + "name": "paymentRate", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "discountFee", + "doc": "Effective discount fee, set at disbursement.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "proposedDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "approvedDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "periodPaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "dailyEir", + "doc": "Daily effective interest rate derived from discount-fee pricing, not a lending interest rate.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "calculatedAnnualEir", + "doc": "Annualised dailyEir.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "npvDayCount", + "doc": "Day count used by the amortization schedule.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "totalPaymentVolume", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "delinquency", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyDataV1" + ] + }, + { + "default": null, + "name": "overpaidOnDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "actualNoTerm", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "customData", + "type": [ + "null", + { + "values": "bytes", + "type": "map" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc new file mode 100644 index 00000000000..15439afe6fc --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc @@ -0,0 +1,179 @@ +{ + "name": "WorkingCapitalLoanChargeDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "chargeId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeTimeType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "dueDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeCalculationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "currency", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + ] + }, + { + "default": null, + "name": "amount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountAccrued", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountUnrecognized", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penalty", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "chargePaymentMode", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "paid", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "loanId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "externalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "externalLoanId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "submittedOnDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "customData", + "type": [ + "null", + { + "values": "bytes", + "type": "map" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc new file mode 100644 index 00000000000..9fd134a7f12 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc @@ -0,0 +1,98 @@ +{ + "name": "WorkingCapitalLoanCollectionDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "delinquentDays", + "doc": "Days since the oldest active delinquency tag.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquentDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "delinquentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "lastPaymentDate", + "doc": "Date of the last non-reversed repayment-like transaction (repayment, payout refund, goodwill credit, charge adjustment).", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "lastPaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "lastRepaymentDate", + "doc": "Date of the last non-reversed repayment transaction.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "lastRepaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "delinquencyPausePeriods", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.DelinquencyPausePeriodV1" + } + ] + }, + { + "default": null, + "name": "delinquencySchedule", + "doc": "Current delinquency schedule periods.", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1" + } + ] + }, + { + "default": null, + "name": "totalDelinquentAmount", + "doc": "Aggregated outstanding of the delinquency schedule periods; always equal to delinquentAmount.", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc new file mode 100644 index 00000000000..2bcb20622d0 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc @@ -0,0 +1,24 @@ +{ + "name": "WorkingCapitalLoanDelinquencyDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "delinquencyStartType", + "doc": "What starts the delinquency clock.", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "delinquencyStartDate", + "type": [ + "null", + "string" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc new file mode 100644 index 00000000000..9ec0007da07 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc @@ -0,0 +1,79 @@ +{ + "name": "WorkingCapitalLoanDelinquencySchedulePeriodDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "periodNumber", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "fromDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "toDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "expectedAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "paidAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "minPaymentCriteriaMet", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "delinquentDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquentAmount", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc new file mode 100644 index 00000000000..cec630873f8 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc @@ -0,0 +1,48 @@ +{ + "name": "WorkingCapitalLoanDisbursementDetailDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "expectedDisbursementDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "actualDisbursementDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "principal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "netDisbursalAmount", + "doc": "Net amount disbursed to the client after up-front deductions (from the amortization model).", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc new file mode 100644 index 00000000000..3ffae5d0e92 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc @@ -0,0 +1,322 @@ +{ + "name": "WorkingCapitalLoanSummaryDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "currency", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + ] + }, + { + "default": null, + "name": "principalDisbursed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalAdjustments", + "doc": "Always null: working capital has no chargeback, the only source of principal adjustments.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesCharged", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesCharged", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalExpectedRepayment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepayment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRecovered", + "doc": "Always null: working capital does not yet implement recovery.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "writeoffReasonId", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "writeoffReason", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeOffReasonId", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "chargeOffReason", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "totalPayoutRefund", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalPayoutRefundReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalGoodwillCredit", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalGoodwillCreditReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalChargeAdjustment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalChargeAdjustmentReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCreditBalanceRefund", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCreditBalanceRefundReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepaymentTransaction", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepaymentTransactionReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalPrincipal", + "doc": "Disbursed amount plus the discount fee.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "realizedIncomeFromDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "unrealizedIncomeFromDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalDiscountFeeAdjustment", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc new file mode 100644 index 00000000000..01aaf251677 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc @@ -0,0 +1,47 @@ +{ + "name": "WorkingCapitalNearBreachDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "frequency", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "frequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "threshold", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java index 90cf8dd9ffa..c237c6b0e66 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java @@ -27,6 +27,7 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.avro.client.v1.ClientDataV1; @@ -37,6 +38,10 @@ import org.apache.fineract.avro.loan.v1.LoanOwnershipTransferDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanTransactionDataV1; import org.apache.fineract.client.feign.FineractFeignClient; import org.apache.fineract.client.models.ExternalTransferData; @@ -44,14 +49,17 @@ import org.apache.fineract.client.models.GetLoansLoanIdDelinquencyPausePeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.GetWorkingCapitalLoanSummary; import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionIdResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionsResponse; +import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse; import org.apache.fineract.client.models.GlobalConfigurationPropertyData; import org.apache.fineract.client.models.PageExternalTransferData; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.WorkingCapitalCollection; import org.apache.fineract.test.data.AssetExternalizationTransferStatus; import org.apache.fineract.test.data.AssetExternalizationTransferStatusReason; import org.apache.fineract.test.data.TransactionType; @@ -83,11 +91,23 @@ import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMerchantIssuedRefundPostEvent; import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionPayoutRefundPostEvent; import org.apache.fineract.test.messaging.event.loan.transaction.LoanUndoContractTerminationBusinessEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.AbstractWorkingCapitalLoanEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanApplicationModifiedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanApprovedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanBalanceChangedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanCreatedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanDelinquencyRangeChangeEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanDisbursalEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanRejectedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanStatusChangedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanUndoApprovalEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanUndoDisbursalEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -736,4 +756,123 @@ public void approveLoanEventCheck(PostLoansLoanIdResponse loanApproveResponse) { .isEqualTo(body.getCurrency().getCode());// } + public void workingCapitalLoanCreatedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanCreatedEvent.class, loanId); + } + + public void workingCapitalLoanApplicationModifiedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanApplicationModifiedEvent.class, loanId); + } + + public void workingCapitalLoanApprovedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanApprovedEvent.class, loanId); + } + + public void workingCapitalLoanUndoApprovalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanUndoApprovalEvent.class, loanId); + } + + public void workingCapitalLoanRejectedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanRejectedEvent.class, loanId); + } + + public void workingCapitalLoanDisbursalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanDisbursalEvent.class, loanId); + } + + public void workingCapitalLoanUndoDisbursalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanUndoDisbursalEvent.class, loanId); + } + + public void workingCapitalLoanStatusChangedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanStatusChangedEvent.class, loanId); + } + + public void workingCapitalLoanBalanceChangedEventCheck(final Long loanId) { + waitForTransactionCommit(); + final GetWorkingCapitalLoansLoanIdResponse body = ok( + () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + + eventAssertion.assertEvent(WorkingCapitalLoanBalanceChangedEvent.class, loanId).extractingData(event -> { + assertWorkingCapitalLoanAccountData(event, body); + + final WorkingCapitalLoanSummaryDataV1 eventSummary = event.getSummary(); + final GetWorkingCapitalLoanSummary bodySummary = body.getSummary(); + assertThat(eventSummary).isNotNull(); + assertThat(bodySummary).isNotNull(); + assertThat(areBigDecimalValuesEqual(eventSummary.getPrincipalPaid(), bodySummary.getPrincipalPaid())).isTrue(); + assertThat(areBigDecimalValuesEqual(eventSummary.getPrincipalOutstanding(), bodySummary.getPrincipalOutstanding())).isTrue(); + assertThat(areBigDecimalValuesEqual(eventSummary.getTotalOutstanding(), bodySummary.getTotalOutstanding())).isTrue(); + assertThat(areBigDecimalValuesEqual(event.getTotalOverpaid(), bodySummary.getOverpayment())).isTrue(); + return null; + }); + } + + public void workingCapitalLoanBalanceChangedEventChargesCheck(final Long loanId, final List> expectedCharges) { + waitForTransactionCommit(); + eventAssertion.assertEvent(WorkingCapitalLoanBalanceChangedEvent.class, loanId).extractingData(event -> { + final List eventCharges = event.getCharges(); + assertThat(eventCharges).isNotNull().hasSize(expectedCharges.size()); + IntStream.range(0, expectedCharges.size()).forEach(i -> { + final Map expected = expectedCharges.get(i); + final WorkingCapitalLoanChargeDataV1 actual = eventCharges.get(i); + assertThat(actual.getAmount()).isNotNull(); + assertThat(actual.getAmountAccrued()).isNotNull(); + assertThat(actual.getAmountUnrecognized()).isNotNull(); + assertThat(areBigDecimalValuesEqual(actual.getAmount(), new BigDecimal(expected.get("amount")))).isTrue(); + assertThat(areBigDecimalValuesEqual(actual.getAmountAccrued(), new BigDecimal(expected.get("amountAccrued")))).isTrue(); + assertThat(areBigDecimalValuesEqual(actual.getAmountUnrecognized(), new BigDecimal(expected.get("amountUnrecognized")))) + .isTrue(); + }); + return null; + }); + } + + public void workingCapitalLoanDelinquencyRangeChangeEventCheck(final Long loanId) { + waitForTransactionCommit(); + final GetWorkingCapitalLoansLoanIdResponse body = ok( + () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + + eventAssertion.assertEvent(WorkingCapitalLoanDelinquencyRangeChangeEvent.class, loanId).extractingData(event -> { + assertWorkingCapitalLoanAccountData(event, body); + + final WorkingCapitalLoanCollectionDataV1 eventDelinquent = event.getDelinquent(); + final WorkingCapitalCollection bodyDelinquent = body.getDelinquent(); + assertThat(eventDelinquent).isNotNull(); + assertThat(bodyDelinquent).isNotNull(); + assertThat(areBigDecimalValuesEqual(eventDelinquent.getDelinquentAmount(), bodyDelinquent.getDelinquentAmount())).isTrue(); + assertThat(areBigDecimalValuesEqual(eventDelinquent.getTotalDelinquentAmount(), bodyDelinquent.getDelinquentPrincipal())) + .isTrue(); + assertThat(eventDelinquent.getDelinquencySchedule()).isNotEmpty(); + return null; + }); + } + + private void workingCapitalLoanAccountDataV1Check(final Class eventClazz, + final Long loanId) { + waitForTransactionCommit(); + final GetWorkingCapitalLoansLoanIdResponse body = ok( + () -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + + eventAssertion.assertEvent(eventClazz, loanId).extractingData(event -> { + assertWorkingCapitalLoanAccountData(event, body); + return null; + }); + } + + private void assertWorkingCapitalLoanAccountData(final WorkingCapitalLoanAccountDataV1 event, + final GetWorkingCapitalLoansLoanIdResponse body) { + Assertions.assertNotNull(body.getStatus()); + assertThat(event.getStatus().getId().longValue()).isEqualTo(body.getStatus().getId()); + + assertThat(event.getDelinquent()).isNotNull(); + assertThat(event.getDelinquent().getDelinquencySchedule()).isNotNull(); + + if (event.getDisbursementDetails() != null && !event.getDisbursementDetails().isEmpty() && body.getNetDisbursalAmount() != null) { + event.getDisbursementDetails() + .forEach(tranche -> assertThat(areBigDecimalValuesEqual(tranche.getNetDisbursalAmount(), body.getNetDisbursalAmount())) + .isTrue()); + } + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java new file mode 100644 index 00000000000..d4d9d48d041 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +import java.util.function.Function; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.test.messaging.event.Event; + +public abstract class AbstractWorkingCapitalLoanEvent implements Event { + + @Override + public Class getDataClass() { + return WorkingCapitalLoanAccountDataV1.class; + } + + @Override + public Function getIdExtractor() { + return WorkingCapitalLoanAccountDataV1::getId; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java new file mode 100644 index 00000000000..914baf500bb --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanApplicationModifiedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanApplicationModifiedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java new file mode 100644 index 00000000000..289ce9dccf1 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanApprovedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanApprovedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java new file mode 100644 index 00000000000..1005fbd82fb --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanBalanceChangedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanBalanceChangedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java new file mode 100644 index 00000000000..6b3817dc820 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanCreatedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanCreatedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java new file mode 100644 index 00000000000..219ef798af6 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanDelinquencyRangeChangeEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java new file mode 100644 index 00000000000..99ed3ad59a0 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanDisbursalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanDisbursalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java new file mode 100644 index 00000000000..f64208516c9 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanRejectedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanRejectedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java new file mode 100644 index 00000000000..bcf4a4baecd --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanStatusChangedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanStatusChangedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java new file mode 100644 index 00000000000..f008572405c --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanUndoApprovalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanUndoApprovalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java new file mode 100644 index 00000000000..ff2b0802441 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanUndoDisbursalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanUndoDisbursalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 70269a9ad64..e86f9a940e1 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -1413,6 +1413,61 @@ public void checkWorkingCapitalDiscountFeeAdjustmentBusinessEventIsRaised(final new BigDecimal(amount), transactionDate); } + @Then("a Working Capital Loan Created business event is raised") + public void aWorkingCapitalLoanCreatedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanCreatedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Application Modified business event is raised") + public void aWorkingCapitalLoanApplicationModifiedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanApplicationModifiedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Approved business event is raised") + public void aWorkingCapitalLoanApprovedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanApprovedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Undo Approval business event is raised") + public void aWorkingCapitalLoanUndoApprovalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanUndoApprovalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Rejected business event is raised") + public void aWorkingCapitalLoanRejectedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanRejectedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Disbursal business event is raised") + public void aWorkingCapitalLoanDisbursalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanDisbursalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Undo Disbursal business event is raised") + public void aWorkingCapitalLoanUndoDisbursalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanUndoDisbursalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Status Changed business event is raised") + public void aWorkingCapitalLoanStatusChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanStatusChangedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanBalanceChangedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised with charges:") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaisedWithCharges(final DataTable table) { + eventCheckHelper.workingCapitalLoanBalanceChangedEventChargesCheck(getCreatedLoanId(), table.asMaps()); + } + + @Then("a Working Capital Loan Delinquency Range Change business event is raised") + public void aWorkingCapitalLoanDelinquencyRangeChangeBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanDelinquencyRangeChangeEventCheck(getCreatedLoanId()); + } + @Then("Admin fails to disburse the Working Capital loan on {string} with {string} EUR transaction amount because of not approved") public void disburseWCLoanFailureWithNotApproved(String actualDisbursementDate, String transactionAmount) { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature new file mode 100644 index 00000000000..f5396866f43 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature @@ -0,0 +1,159 @@ +@WorkingCapital +@WorkingCapitalLoanAccountBusinessEventsFeature +Feature: Working Capital Loan Account Business Events + + Scenario: Working Capital loan raises Created, Application Modified, Approved, Undo Approval, Disbursal and Undo Disbursal business events across its lifecycle + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100.0 | 100.0 | 1.0 | 0.0 | + Then Working capital loan creation was successful + And a Working Capital Loan Created business event is raised + When Admin modifies the working capital loan with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | | | 80.0 | | | | + Then a Working Capital Loan Application Modified business event is raised + When Admin successfully approves the working capital loan on "01 January 2026" with "80" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And a Working Capital Loan Approved business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin makes undo approval on the working capital loan + Then Working capital loan undo approval was successful + And a Working Capital Loan Undo Approval business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin successfully approves the working capital loan on "01 January 2026" with "80" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And a Working Capital Loan Approved business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "80" EUR transaction amount + Then a Working Capital Loan Disbursal business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin successfully undo Working Capital disbursal + Then a Working Capital Loan Undo Disbursal business event is raised + And a Working Capital Loan Status Changed business event is raised + + Scenario: Rejected Working Capital loan raises Rejected and Status Changed business events + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100.0 | 100.0 | 1.0 | 0.0 | + Then Working capital loan creation was successful + When Admin rejects the working capital loan on "01 January 2026" + Then Working capital loan rejection was successful + And a Working Capital Loan Rejected business event is raised + And a Working Capital Loan Status Changed business event is raised + + Scenario: Working Capital loan raises Balance Changed on partial repayment and Status Changed on full repayment + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes repayment on "10 January 2026" with 4000.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "20 January 2026" + And Customer makes repayment on "20 January 2026" with 5000.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + + Scenario: Overpaid Working Capital loan raises Balance Changed and Status Changed business events through credit balance refund + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "20 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes repayment on "20 January 2026" with 9200.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "OVERPAID" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Customer makes credit balance refund on "20 January 2026" with 200.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + + Scenario: Delinquent Working Capital loan raises a Delinquency Range Change business event + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "15 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then a Working Capital Loan Delinquency Range Change business event is raised + When Admin closes the Working Capital loan with a full repayment on "15 February 2026" + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + + Scenario: Working Capital loan raises Balance Changed on discount fee, discount fee adjustment and undo of the adjustment + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee with "12" amount on Working Capital loan account for last disbursement + Then a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee adjustment with "5" amount on Working Capital loan account for last discount + Then a Working Capital Loan Balance Changed business event is raised + When Admin undo the last Discount fee adjustment on Working Capital loan account + Then a Working Capital Loan Balance Changed business event is raised + When Admin closes the Working Capital loan with a full repayment on "01 January 2026" + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + + @SerialChargeAccrualConfig + Scenario: Working Capital loan Balance Changed event exposes charge amountAccrued and amountUnrecognized before and after accrual + Given Admin sets the business date to "01 January 2028" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 January 2028 | 01 January 2028 | 9000 | 100000 | 18 | 0 | + And a Working Capital Loan Balance Changed business event is raised + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "05 January 2028" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2028" due date and 50.0 transaction amount + And Admin sets the business date to "10 January 2028" + And Customer makes repayment on "10 January 2028" with 100.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with charges: + | amount | amountAccrued | amountUnrecognized | + | 50.0 | 0.0 | 50.0 | + When Admin sets the business date to "16 January 2028" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Admin closes the Working Capital loan with a full repayment on "16 January 2028" + And a Working Capital Loan Balance Changed business event is raised with charges: + | amount | amountAccrued | amountUnrecognized | + | 50.0 | 50.0 | 0.0 | diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 78673e840f1..4e6d1679f5b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -117,16 +117,21 @@ public void givenAllConfigurationWhenValidatedThenValidationSuccessful() throws "WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent", "WorkingCapitalLoanRepaymentTransactionBusinessEvent", "WorkingCapitalLoanDiscountFeeTransactionBusinessEvent", "WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent", "WorkingCapitalLoanChargeAdjustmentPreBusinessEvent", - "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent", "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent"); + "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent", "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent", + "WorkingCapitalLoanCreatedBusinessEvent", "WorkingCapitalLoanApplicationModifiedBusinessEvent", + "WorkingCapitalLoanApprovedBusinessEvent", "WorkingCapitalLoanUndoApprovalBusinessEvent", + "WorkingCapitalLoanRejectedBusinessEvent", "WorkingCapitalLoanDisbursalBusinessEvent", + "WorkingCapitalLoanUndoDisbursalBusinessEvent", "WorkingCapitalLoanStatusChangedBusinessEvent", + "WorkingCapitalLoanBalanceChangedBusinessEvent", "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"); - List tenants = Arrays - .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); + List tenants = List + .of(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); JdbcTemplate jdbcTemplate = Mockito.mock(JdbcTemplate.class); when(tenantDetailsService.findAllTenants()).thenReturn(tenants); when(jdbcTemplateFactory.create(any())).thenReturn(jdbcTemplate); when(jdbcTemplate.queryForList(anyString(), eq(String.class))).thenReturn(configurations); - List sourcePackage = Arrays.asList("org.apache.fineract"); + List sourcePackage = List.of("org.apache.fineract"); when(externalEventSourceService.getSourcePackages()).thenReturn(sourcePackage); // when underTest.afterPropertiesSet(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java new file mode 100644 index 00000000000..137338c1823 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanApplicationModifiedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanApplicationModifiedBusinessEvent"; + + public WorkingCapitalLoanApplicationModifiedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java new file mode 100644 index 00000000000..09dda04b015 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanApprovedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanApprovedBusinessEvent"; + + public WorkingCapitalLoanApprovedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java new file mode 100644 index 00000000000..3042f352d9a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanBalanceChangedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanBalanceChangedBusinessEvent"; + + public WorkingCapitalLoanBalanceChangedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java new file mode 100644 index 00000000000..cb43af49c2d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public abstract class WorkingCapitalLoanBusinessEvent extends AbstractBusinessEvent { + + private static final String CATEGORY = "WorkingCapitalLoan"; + + public WorkingCapitalLoanBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Long getAggregateRootId() { + return get().getId(); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java new file mode 100644 index 00000000000..ec3b61b8d8c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanCreatedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanCreatedBusinessEvent"; + + public WorkingCapitalLoanCreatedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java new file mode 100644 index 00000000000..823a960f076 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"; + + public WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java new file mode 100644 index 00000000000..ddbcbe70ccd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanDisbursalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanDisbursalBusinessEvent"; + + public WorkingCapitalLoanDisbursalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java new file mode 100644 index 00000000000..6699dabeb2f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanRejectedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanRejectedBusinessEvent"; + + public WorkingCapitalLoanRejectedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java new file mode 100644 index 00000000000..9a6119b0fed --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanStatusChangedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanStatusChangedBusinessEvent"; + + public WorkingCapitalLoanStatusChangedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java new file mode 100644 index 00000000000..73a6626b1a6 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanUndoApprovalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanUndoApprovalBusinessEvent"; + + public WorkingCapitalLoanUndoApprovalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java new file mode 100644 index 00000000000..f19eedda7cd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanUndoDisbursalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanUndoDisbursalBusinessEvent"; + + public WorkingCapitalLoanUndoDisbursalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java new file mode 100644 index 00000000000..45e32505b6b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java @@ -0,0 +1,24 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; + +public record ChargeIdAndAmountHolder(Long chargeId, BigDecimal amount) { +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java new file mode 100644 index 00000000000..cd3521b7196 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java @@ -0,0 +1,25 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + +public record TransactionTypeTotalHolder(LoanTransactionType transactionType, boolean reversed, BigDecimal total) { +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java index 32929b76b36..4b5b8009f44 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java @@ -22,8 +22,11 @@ import java.util.List; import java.util.Optional; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.data.ChargeIdAndAmountHolder; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -38,4 +41,16 @@ List findAllByToTransactionAndFromTransac List findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType( WorkingCapitalLoanCharge toCharge, boolean reversed, LoanTransactionType transactionType); + + @Query(""" + SELECT r.toCharge.id, SUM(r.fromTransaction.transactionAmount) + FROM WorkingCapitalLoanTransactionRelation r + WHERE r.fromTransaction.wcLoan.id = :wcLoanId + AND r.fromTransaction.reversed = FALSE + AND r.fromTransaction.transactionType = :transactionType + AND r.toCharge IS NOT NULL + GROUP BY r.toCharge.id + """) + List fetchTransactionAmountPerCharge(@Param("wcLoanId") Long wcLoanId, + @Param("transactionType") LoanTransactionType transactionType); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java index 5d5a416469f..b6dc3fc7c46 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java @@ -35,6 +35,9 @@ public interface WorkingCapitalLoanDelinquencyActionRepository extends JpaReposi List findByWorkingCapitalLoanIdAndActionOrderByIdDesc(Long workingCapitalLoanId, DelinquencyAction action); + List findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(Long workingCapitalLoanId, + DelinquencyAction action); + @Query(""" select case when count(action) > 0 then true else false end from WorkingCapitalLoanDelinquencyAction action where action.action = org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.DISABLE diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java index c6637529c0e..828601ff3cc 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionDateAndAmountHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionTypeTotalHolder; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -88,4 +89,24 @@ SELECT t.transactionDate, SUM(t.transactionAmount) """) List fetchTransactionDateAndAmount(@Param("wcLoanId") Long wcLoanId, @Param("transactionTypes") List transactionTypes); + + @Query(""" + SELECT t.transactionType, t.reversed, SUM(t.transactionAmount) + FROM WorkingCapitalLoanTransaction t + WHERE t.wcLoan.id = :wcLoanId + AND t.transactionType in :transactionTypes + GROUP BY t.transactionType, t.reversed + """) + List fetchTotalsPerTypeAndReversed(@Param("wcLoanId") Long wcLoanId, + @Param("transactionTypes") List transactionTypes); + + @Query(""" + SELECT t FROM WorkingCapitalLoanTransaction t + WHERE t.wcLoan.id = :wcLoanId + AND t.reversed = FALSE + AND t.transactionType in :transactionTypes + ORDER BY t.transactionDate DESC, t.id DESC + """) + List findActiveByTypesOrderByDateDesc(@Param("wcLoanId") Long wcLoanId, + @Param("transactionTypes") List transactionTypes); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java new file mode 100644 index 00000000000..f00232e6d4a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java @@ -0,0 +1,122 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.serialization.mapper; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroMapperConfig; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanChargeData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDisbursementDetailData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanSummaryData; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = AvroMapperConfig.class) +public interface WorkingCapitalLoanAccountDataMapper { + + int AVRO_DECIMAL_SCALE = 8; + + @Mapping(source = "delinquencyGraceDays", target = "graceOnArrearsAgeing") + @Mapping(source = "summary.overpayment", target = "totalOverpaid") + @Mapping(source = "dailyEir", target = "dailyEir", qualifiedByName = "toAvroDecimalScale") + @Mapping(source = "calculatedAnnualEir", target = "calculatedAnnualEir", qualifiedByName = "toAvroDecimalScale") + @Mapping(source = "numberOfRepayments", target = "actualNoTerm") + @Mapping(source = "delinquencyStartType", target = "delinquency.delinquencyStartType") + @Mapping(source = "delinquencyStartDate", target = "delinquency.delinquencyStartDate") + @Mapping(source = "breach.id", target = "breach.id") + @Mapping(source = "breach.name", target = "breach.name") + @Mapping(source = "breach.breachFrequency", target = "breach.breachFrequency") + @Mapping(source = "breach.breachFrequencyType", target = "breach.breachFrequencyType") + @Mapping(source = "breach.breachAmountCalculationType", target = "breach.breachAmountCalculationType") + @Mapping(source = "breach.breachAmount", target = "breach.breachAmount") + @Mapping(source = "breachGraceDays", target = "breach.breachGraceDays") + @Mapping(source = "breachStartDate", target = "breach.breachStartDate") + @Mapping(source = "nearBreach", target = "breach.nearBreach") + @Mapping(target = "overpaidOnDate", ignore = true) + @Mapping(target = "customData", ignore = true) + WorkingCapitalLoanAccountDataV1 map(WorkingCapitalLoanData source); + + @Mapping(source = "principal", target = "totalPrincipal") + @Mapping(source = "totalDisbursement", target = "principalDisbursed") + @Mapping(source = "fee", target = "feeChargesCharged") + @Mapping(source = "feePaid", target = "feeChargesPaid") + @Mapping(source = "feeOutstanding", target = "feeChargesOutstanding") + @Mapping(source = "penalty", target = "penaltyChargesCharged") + @Mapping(source = "penaltyPaid", target = "penaltyChargesPaid") + @Mapping(source = "penaltyOutstanding", target = "penaltyChargesOutstanding") + @Mapping(target = "principalAdjustments", ignore = true) + @Mapping(target = "principalWrittenOff", ignore = true) + @Mapping(target = "feeChargesWrittenOff", ignore = true) + @Mapping(target = "penaltyChargesWrittenOff", ignore = true) + @Mapping(target = "totalWrittenOff", ignore = true) + @Mapping(target = "totalRecovered", ignore = true) + @Mapping(target = "writeoffReasonId", ignore = true) + @Mapping(target = "writeoffReason", ignore = true) + @Mapping(target = "chargeOffReasonId", ignore = true) + @Mapping(target = "chargeOffReason", ignore = true) + @Mapping(target = "totalPayoutRefund", ignore = true) + @Mapping(target = "totalPayoutRefundReversed", ignore = true) + @Mapping(target = "totalGoodwillCredit", ignore = true) + @Mapping(target = "totalGoodwillCreditReversed", ignore = true) + @Mapping(target = "totalChargeAdjustment", ignore = true) + @Mapping(target = "totalChargeAdjustmentReversed", ignore = true) + @Mapping(target = "totalCreditBalanceRefund", ignore = true) + @Mapping(target = "totalCreditBalanceRefundReversed", ignore = true) + @Mapping(target = "totalRepaymentTransaction", ignore = true) + @Mapping(target = "totalRepaymentTransactionReversed", ignore = true) + WorkingCapitalLoanSummaryDataV1 map(WorkingCapitalLoanSummaryData source); + + @Mapping(source = "delinquentPrincipal", target = "totalDelinquentAmount") + @Mapping(target = "lastPaymentDate", ignore = true) + @Mapping(target = "lastPaymentAmount", ignore = true) + @Mapping(target = "lastRepaymentDate", ignore = true) + @Mapping(target = "lastRepaymentAmount", ignore = true) + @Mapping(target = "delinquencySchedule", ignore = true) + WorkingCapitalLoanCollectionDataV1 map(WorkingCapitalLoanCollectionData source); + + @Mapping(target = "amountAccrued", ignore = true) + @Mapping(target = "amountUnrecognized", ignore = true) + @Mapping(target = "amountWrittenOff", ignore = true) + @Mapping(target = "customData", ignore = true) + WorkingCapitalLoanChargeDataV1 map(WorkingCapitalLoanChargeData source); + + @Mapping(target = "netDisbursalAmount", ignore = true) + WorkingCapitalLoanDisbursementDetailDataV1 map(WorkingCapitalLoanDisbursementDetailData source); + + WorkingCapitalLoanDelinquencySchedulePeriodDataV1 map(WorkingCapitalLoanDelinquencyRangeScheduleData source); + + List mapDelinquencySchedule( + List source); + + @Named("toAvroDecimalScale") + default BigDecimal toAvroDecimalScale(final BigDecimal value) { + return value == null ? null : value.setScale(AVRO_DECIMAL_SCALE, RoundingMode.HALF_UP); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java new file mode 100644 index 00000000000..19cb10f1701 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java @@ -0,0 +1,192 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.serialization.serializer; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.avro.generic.GenericContainer; +import org.apache.fineract.avro.generator.ByteBufferSerializable; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBusinessEvent; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.AbstractBusinessEventWithCustomDataSerializer; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.ExternalEventCustomDataSerializer; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.data.ChargeIdAndAmountHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionTypeTotalHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.serialization.mapper.WorkingCapitalLoanAccountDataMapper; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WorkingCapitalLoanBusinessEventSerializer + extends AbstractBusinessEventWithCustomDataSerializer implements BusinessEventSerializer { + + private static final List SUMMARY_TOTAL_TRANSACTION_TYPES = List.of(LoanTransactionType.PAYOUT_REFUND, + LoanTransactionType.GOODWILL_CREDIT, LoanTransactionType.CHARGE_ADJUSTMENT, LoanTransactionType.CREDIT_BALANCE_REFUND, + LoanTransactionType.REPAYMENT); + + private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService; + private final WorkingCapitalLoanAccountDataMapper mapper; + private final WorkingCapitalLoanTransactionRelationRepository transactionRelationRepository; + private final WorkingCapitalLoanTransactionRepository transactionRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleRepository rangeScheduleRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleMapper rangeScheduleMapper; + private final AvroDateTimeMapper avroDateTimeMapper; + private final List> externalEventCustomDataSerializers; + private final List chargeExternalEventCustomDataSerializers; + + @Override + public boolean canSerialize(BusinessEvent event) { + return event instanceof WorkingCapitalLoanBusinessEvent; + } + + @Override + public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { + final WorkingCapitalLoanBusinessEvent event = (WorkingCapitalLoanBusinessEvent) rawEvent; + final WorkingCapitalLoan loan = event.get(); + final WorkingCapitalLoanData data = readPlatformService.retrieveOne(loan.getId()); + final WorkingCapitalLoanAccountDataV1 result = mapper.map(data); + if (result.getDisbursementDetails() != null) { + result.getDisbursementDetails().forEach(detail -> detail.setNetDisbursalAmount(result.getNetDisbursalAmount())); + } + populateChargeAccruals(loan, result); + populateChargeCustomData(event, result); + populateSummaryTransactionTypeTotals(loan, result); + populateLastTransactions(loan, result); + populateDelinquencySchedule(loan, result); + result.setCustomData(collectCustomData(event)); + return result; + } + + private void populateChargeAccruals(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + if (result.getCharges() == null || result.getCharges().isEmpty() || loan.getLoanProduct() == null + || !loan.getLoanProduct().getAccountingRule().isAccrualWithDeferredRevenueAmortization()) { + return; + } + final Map accruedAmountsByChargeId = transactionRelationRepository + .fetchTransactionAmountPerCharge(loan.getId(), LoanTransactionType.ACCRUAL).stream() + .collect(Collectors.toMap(ChargeIdAndAmountHolder::chargeId, ChargeIdAndAmountHolder::amount)); + result.getCharges().forEach(charge -> { + final BigDecimal amountAccrued = accruedAmountsByChargeId.getOrDefault(charge.getId(), BigDecimal.ZERO); + charge.setAmountAccrued(amountAccrued); + charge.setAmountUnrecognized(MathUtil.subtractToZero(charge.getAmount(), amountAccrued)); + }); + } + + private void populateChargeCustomData(final WorkingCapitalLoanBusinessEvent event, final WorkingCapitalLoanAccountDataV1 result) { + if (result.getCharges() == null) { + return; + } + result.getCharges().forEach(charge -> charge.setCustomData(collectChargeCustomData(event, charge.getId()))); + } + + private Map collectChargeCustomData(final WorkingCapitalLoanBusinessEvent event, final Long chargeId) { + return chargeExternalEventCustomDataSerializers.stream().collect(HashMap::new, (map, serializer) -> { + final ByteBuffer buffer = serializer.serialize(event, chargeId); + if (buffer != null) { + map.put(serializer.key(), buffer); + } + }, HashMap::putAll); + } + + private void populateSummaryTransactionTypeTotals(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanSummaryDataV1 summary = result.getSummary(); + if (summary == null) { + return; + } + final Map> totals = transactionRepository + .fetchTotalsPerTypeAndReversed(loan.getId(), SUMMARY_TOTAL_TRANSACTION_TYPES).stream() + .collect(Collectors.groupingBy(TransactionTypeTotalHolder::transactionType, + Collectors.toMap(TransactionTypeTotalHolder::reversed, TransactionTypeTotalHolder::total))); + summary.setTotalPayoutRefund(totalOf(totals, LoanTransactionType.PAYOUT_REFUND, false)); + summary.setTotalPayoutRefundReversed(totalOf(totals, LoanTransactionType.PAYOUT_REFUND, true)); + summary.setTotalGoodwillCredit(totalOf(totals, LoanTransactionType.GOODWILL_CREDIT, false)); + summary.setTotalGoodwillCreditReversed(totalOf(totals, LoanTransactionType.GOODWILL_CREDIT, true)); + summary.setTotalChargeAdjustment(totalOf(totals, LoanTransactionType.CHARGE_ADJUSTMENT, false)); + summary.setTotalChargeAdjustmentReversed(totalOf(totals, LoanTransactionType.CHARGE_ADJUSTMENT, true)); + summary.setTotalCreditBalanceRefund(totalOf(totals, LoanTransactionType.CREDIT_BALANCE_REFUND, false)); + summary.setTotalCreditBalanceRefundReversed(totalOf(totals, LoanTransactionType.CREDIT_BALANCE_REFUND, true)); + summary.setTotalRepaymentTransaction(totalOf(totals, LoanTransactionType.REPAYMENT, false)); + summary.setTotalRepaymentTransactionReversed(totalOf(totals, LoanTransactionType.REPAYMENT, true)); + } + + private BigDecimal totalOf(final Map> totals, final LoanTransactionType transactionType, + final boolean reversed) { + return totals.getOrDefault(transactionType, Map.of()).getOrDefault(reversed, BigDecimal.ZERO); + } + + private void populateLastTransactions(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanCollectionDataV1 delinquent = result.getDelinquent(); + if (delinquent == null) { + return; + } + findLastActiveTransaction(loan, LoanTransactionType.getRepaymentLikeTransactionTypes()).ifPresent(transaction -> { + delinquent.setLastPaymentDate(avroDateTimeMapper.mapLocalDate(transaction.getTransactionDate())); + delinquent.setLastPaymentAmount(transaction.getTransactionAmount()); + }); + findLastActiveTransaction(loan, List.of(LoanTransactionType.REPAYMENT)).ifPresent(transaction -> { + delinquent.setLastRepaymentDate(avroDateTimeMapper.mapLocalDate(transaction.getTransactionDate())); + delinquent.setLastRepaymentAmount(transaction.getTransactionAmount()); + }); + } + + private Optional findLastActiveTransaction(final WorkingCapitalLoan loan, + final List transactionTypes) { + return transactionRepository.findActiveByTypesOrderByDateDesc(loan.getId(), transactionTypes).stream().findFirst(); + } + + private void populateDelinquencySchedule(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanCollectionDataV1 delinquent = result.getDelinquent(); + if (delinquent == null) { + return; + } + delinquent.setDelinquencySchedule(mapper.mapDelinquencySchedule( + rangeScheduleMapper.toDataList(rangeScheduleRepository.findByLoanIdOrderByPeriodNumberAsc(loan.getId())))); + } + + @Override + protected List> getExternalEventCustomDataSerializers() { + return externalEventCustomDataSerializers; + } + + @Override + public Class getSupportedSchema() { + return WorkingCapitalLoanAccountDataV1.class; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.java new file mode 100644 index 00000000000..79251aff7de --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.serialization.serializer; + +import java.nio.ByteBuffer; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBusinessEvent; + +public interface WorkingCapitalLoanChargeExternalEventCustomDataSerializer { + + ByteBuffer serialize(WorkingCapitalLoanBusinessEvent event, Long chargeId); + + String key(); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java index 191d025b6b2..5e567229def 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java @@ -28,6 +28,9 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanApplicationModifiedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.service.LoanOriginatorLinkingService; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; @@ -54,17 +57,20 @@ public class WorkingCapitalLoanApplicationWritePlatformServiceImpl implements Wo private final WorkingCapitalLoanNoteRepository noteRepository; private final ProjectedAmortizationLoanModelRepository projectedAmortizationLoanModelRepository; private final Optional loanOriginatorLinkingService; + private final BusinessEventNotifierService businessEventNotifierService; public WorkingCapitalLoanApplicationWritePlatformServiceImpl(WorkingCapitalLoanApplicationDataValidator validator, WorkingCapitalLoanRepository repository, WorkingCapitalLoanAssembler assembler, WorkingCapitalLoanNoteRepository noteRepository, ProjectedAmortizationLoanModelRepository projectedAmortizationLoanModelRepository, - @Qualifier("workingCapitalLoanOriginatorLinkingServiceImpl") Optional loanOriginatorLinkingService) { + @Qualifier("workingCapitalLoanOriginatorLinkingServiceImpl") Optional loanOriginatorLinkingService, + BusinessEventNotifierService businessEventNotifierService) { this.validator = validator; this.repository = repository; this.assembler = assembler; this.noteRepository = noteRepository; this.projectedAmortizationLoanModelRepository = projectedAmortizationLoanModelRepository; this.loanOriginatorLinkingService = loanOriginatorLinkingService; + this.businessEventNotifierService = businessEventNotifierService; } @Transactional @@ -80,6 +86,8 @@ public CommandProcessingResult submitApplication(final JsonCommand command) { createNote(submittedOnNote, saved); attachOriginatorsIfProvided(command, saved); + this.businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanCreatedBusinessEvent(saved)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(saved.getId()) // @@ -113,6 +121,8 @@ public CommandProcessingResult modifyApplication(final Long loanId, final JsonCo final String submittedOnNote = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.submittedOnNoteParameterName); createNote(submittedOnNote, saved); + this.businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanApplicationModifiedBusinessEvent(saved)); + return new CommandProcessingResultBuilder() // .withEntityId(loanId) // .withEntityExternalId(saved.getExternalId()) // diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java index 518294ca764..70bf17cf009 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java @@ -30,7 +30,7 @@ public interface WorkingCapitalLoanDelinquencyClassificationService { void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate); - void applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range, + boolean applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range, DelinquencyRange currentRange, LocalDate businessDate); boolean isDelinquencyDisabled(WorkingCapitalLoan loan, LocalDate date); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java index c61ec769c22..245e6b6923c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java @@ -31,6 +31,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; @@ -50,6 +52,7 @@ public class WorkingCapitalLoanDelinquencyClassificationServiceImpl implements W private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; private final WorkingCapitalLoanDelinquencyActionRepository delinquencyActionRepository; private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final BusinessEventNotifierService businessEventNotifierService; /** * If ENABLE_INSTANT_DELINQUENCY_CALCULATION is set to true in global config, classifies the delinquency of a loan @@ -90,6 +93,7 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate) List delinquencyRangeScheduleList = delinquencyRangeScheduleRepository .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + boolean delinquencyRangeChanged = false; for (WorkingCapitalLoanDelinquencyRangeSchedule range : delinquencyRangeScheduleList) { if (!Objects.equals(range.getReset(), true) && range.getToDate().isBefore(businessDate)) { long rangeDelinquentDays = range.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0 @@ -102,14 +106,19 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate) range.setDelinquentDays(rangeDelinquentDays); Optional delinquencyRangeByDays = findDelinquencyRangeByDays( loan.getLoanProductRelatedDetails().getDelinquencyBucket(), (int) rangeDelinquentDays); - applyDelinquencyTagForRange(loan, range, delinquencyRangeByDays.orElse(null), businessDate); + delinquencyRangeChanged |= applyDelinquencyTagForRange(loan, range, delinquencyRangeByDays.orElse(null), businessDate); } else { range.setDelinquentAmount(BigDecimal.ZERO); range.setDelinquentDays(0L); - applyDelinquencyTagForRange(loan, range, null, businessDate); + delinquencyRangeChanged |= applyDelinquencyTagForRange(loan, range, null, businessDate); } } } + + if (delinquencyRangeChanged) { + delinquencyRangeScheduleTagHistoryRepository.flush(); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent(loan)); + } } /** @@ -169,9 +178,10 @@ private Optional findDelinquencyRangeByDays(final DelinquencyB * the current delinquency range to be applied; can be null to lift all previous tags * @param businessDate * the date on which the tagging operation is performed + * @return true if the tag history was changed (a tag added or lifted), false if nothing changed */ @Override - public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, + public boolean applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, final DelinquencyRange currentRange, final LocalDate businessDate) { List updatedList = new ArrayList<>(); List rangeScheduleTagHistoryList = delinquencyRangeScheduleTagHistoryRepository @@ -183,7 +193,7 @@ public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final Wor // do nothing if currentRange is in rangeScheduleTagHistoryList or last and currentRange are null if ((last == null && currentRange == null) || (last != null && currentRange != null && rangeScheduleTagHistoryList.stream() .anyMatch(tag -> Objects.equals(tag.getDelinquencyRange().getId(), currentRange.getId())))) { - return; + return false; } if (currentRange == null) { @@ -202,6 +212,7 @@ public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final Wor } delinquencyRangeScheduleTagHistoryRepository.saveAll(updatedList); + return !updatedList.isEmpty(); } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java index b54f4e66280..d646604fab8 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java @@ -19,6 +19,9 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; +import static org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyPauseUtils.isPauseActiveOnDate; +import static org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyPauseUtils.resolveEffectivePauseEnd; + import java.math.BigDecimal; import java.time.LocalDate; import java.util.Comparator; @@ -27,12 +30,16 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanRangeScheduleDelinquencyData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyActionRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository; import org.springframework.stereotype.Service; @@ -47,6 +54,7 @@ public class WorkingCapitalLoanDelinquencyReadPlatformServiceImpl implements Wor private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper delinquencyRangeScheduleTagHistoryMapper; private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleRepository delinquencyRangeScheduleRepository; + private final WorkingCapitalLoanDelinquencyActionRepository delinquencyActionRepository; @Override public WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate businessDate) { @@ -73,10 +81,26 @@ public WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate .map(WorkingCapitalLoanDelinquencyRangeSchedule::getDelinquentDays).ifPresent(template::setPastDueDays); template.setInstallmentLevelDelinquency(list); + template.setDelinquencyPausePeriods(retrieveDelinquencyPausePeriods(loanId, businessDate)); return template; } + private List retrieveDelinquencyPausePeriods(final Long loanId, final LocalDate businessDate) { + final List pauses = delinquencyActionRepository + .findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(loanId, DelinquencyAction.PAUSE); + final List resumes = delinquencyActionRepository + .findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(loanId, DelinquencyAction.RESUME); + return pauses.stream().map(pause -> toPausePeriod(pause, resumes, businessDate)).toList(); + } + + private DelinquencyPausePeriod toPausePeriod(final WorkingCapitalLoanDelinquencyAction pause, + final List resumes, final LocalDate businessDate) { + final LocalDate effectiveEnd = resolveEffectivePauseEnd(pause, resumes); + return new DelinquencyPausePeriod(isPauseActiveOnDate(pause.getStartDate(), effectiveEnd, businessDate), pause.getStartDate(), + effectiveEnd); + } + @Override public List retrieveDelinquencyRangeScheduleTagHistory(Long loanId) { List byLoanIdOrderByAddedOnDateDesc = delinquencyRangeScheduleTagHistoryRepository diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 28068405a3b..4a8d37be061 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -44,6 +44,13 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanApprovedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBalanceChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanDisbursalBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanRejectedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanStatusChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanUndoApprovalBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanUndoDisbursalBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent; @@ -124,6 +131,7 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC final AppUser currentUser = this.context.authenticatedUser(); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVED, loan); // Approved date @@ -170,6 +178,9 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanApprovedBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put(WorkingCapitalLoanConstants.approvedOnDateParamName, approvedOnDate); changes.put("status", loan.getLoanStatus()); @@ -194,6 +205,7 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final this.validator.validateUndoApproval(command.json()); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVAL_UNDO, loan); loan.setApprovedOnDate(null); @@ -210,6 +222,9 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanUndoApprovalBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put("status", loan.getLoanStatus()); @@ -235,6 +250,7 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo final AppUser currentUser = this.context.authenticatedUser(); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REJECTED, loan); final LocalDate rejectedOnDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.rejectedOnDateParamName); @@ -245,6 +261,9 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanRejectedBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put(WorkingCapitalLoanConstants.rejectedOnDateParamName, rejectedOnDate); changes.put("status", loan.getLoanStatus()); @@ -294,6 +313,7 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand changes.put(WorkingCapitalLoanConstants.classificationIdParamName, classificationId); final PaymentDetail paymentDetail = createAndPersistPaymentDetailFromCommand(command, changes); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_DISBURSED, loan); if (!loan.getDisbursementDetails().isEmpty()) { @@ -348,6 +368,10 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand changes.put("status", loan.getLoanStatus()); handleNote(loan, command, changes); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanDisbursalBusinessEvent(loan)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + log.debug("Working capital loan {} disbursed by user {}", loanId, currentUser != null ? currentUser.getId() : "system"); return new CommandProcessingResultBuilder() // @@ -376,6 +400,7 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman ensureUndoDisbursalAllowed(loan); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_DISBURSAL_UNDO, loan); final WorkingCapitalLoanTransaction reversedTransaction = reverseDisbursementTransactionAndResetBalance(loan); @@ -402,6 +427,10 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman changes.put("actualAmount", null); handleNote(loan, command, changes); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanUndoDisbursalBusinessEvent(loan)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + log.debug("Working capital loan {} disbursal undone", loanId); return new CommandProcessingResultBuilder() // @@ -516,6 +545,8 @@ public CommandProcessingResult makeDiscountFee(Long loanId, JsonCommand command) delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(discountTransaction.getId()) .withEntityExternalId(discountTransaction.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) .withLoanId(loanId).with(changes).build(); @@ -587,6 +618,7 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina // The principal change moves the remaining-balance cap, so the delinquency schedule must be re-derived. delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); + final LoanStatus oldStatus = loan.getLoanStatus(); final List activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId()); stateMachine.determineAndTransition(loan, transactionDate, activeCharges); transactionProcessor.triggerInlineAmortizationIfLoanClosed(loan, transactionDate); @@ -594,7 +626,7 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina chargeAccrualService.accrueOnClosure(loan, transactionDate); changes.put("status", loan.getLoanStatus()); - loanRepository.save(loan); + loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); createNote(noteText, loan); @@ -608,6 +640,8 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina } businessEventNotifierService .notifyPostBusinessEvent(new WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent(adjustmentTransaction)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(adjustmentTransaction.getId()) .withEntityExternalId(adjustmentTransaction.getExternalId()).withSubEntityId(relatedDiscountTransaction.getId()) .withSubEntityExternalId(relatedDiscountTransaction.getExternalId()).withOfficeId(loan.getOfficeId()) @@ -649,6 +683,8 @@ private CommandProcessingResult undoDiscountFeeAdjustment(final WorkingCapitalLo final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); createNote(noteText, loan); + notifyBalanceChanged(loan); + final Map changes = new LinkedHashMap<>(); if (StringUtils.isNotBlank(noteText)) { changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); @@ -669,6 +705,7 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); this.validator.validateRepayment(command.json(), loan, transactionType); + final LoanStatus oldStatus = loan.getLoanStatus(); final LocalDate transactionDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.transactionDateParamName); final BigDecimal transactionAmount = this.fromApiJsonHelper .extractBigDecimalNamed(WorkingCapitalLoanConstants.transactionAmountParamName, command.parsedJson(), new HashSet<>()); @@ -702,6 +739,10 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, notifyPostBusinessEvent(transactionType, transaction, loan); + this.loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(transaction.getId()) // @@ -724,6 +765,16 @@ private void notifyPostBusinessEvent(LoanTransactionType transactionType, Workin } } + private void notifyStatusChanged(final WorkingCapitalLoan loan, final LoanStatus oldStatus) { + if (oldStatus != loan.getLoanStatus()) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanStatusChangedBusinessEvent(loan)); + } + } + + private void notifyBalanceChanged(final WorkingCapitalLoan loan) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanBalanceChangedBusinessEvent(loan)); + } + private WorkingCapitalLoanTransaction resolveNewTransaction(final LoanTransactionType transactionType, WorkingCapitalLoan loan, BigDecimal transactionAmount, PaymentDetail paymentDetail, LocalDate transactionDate, CodeValue classification, ExternalId txnExternalId) { @@ -752,6 +803,7 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); this.validator.validateCreditBalanceRefund(command.json(), loan); + final LoanStatus oldStatus = loan.getLoanStatus(); if (loan.getLoanStatus() != LoanStatus.OVERPAID) { throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed", "Credit balance refund is allowed only for overpaid loans", "loanStatus"); @@ -823,6 +875,9 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json businessEventNotifierService.notifyPostBusinessEvent( new WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent(creditBalanceRefundTransaction, loan.getId())); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(creditBalanceRefundTransaction.getId()) // @@ -886,6 +941,7 @@ public CommandProcessingResult undoTransaction(final WorkingCapitalLoan loan, fi validator.validateUndoTransaction(command, loan, transaction); + final LoanStatus oldStatus = loan.getLoanStatus(); Map changes = new HashMap<>(); changes.put("reversed", true); transaction.setReversed(true); @@ -929,6 +985,10 @@ public CommandProcessingResult undoTransaction(final WorkingCapitalLoan loan, fi handleNote(loan, command, changes); + this.loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder().withLoanId(loan.getId()).withLoanExternalId(loan.getExternalId()) .withEntityId(transaction.getId()).withEntityExternalId(transaction.getExternalId()).with(changes).build(); } diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index eef83d8525e..e1f7e6eb68a 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -79,4 +79,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0058_wc_loan_details_external_events.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0058_wc_loan_details_external_events.xml new file mode 100644 index 00000000000..0c9cfe07c2e --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0058_wc_loan_details_external_events.xml @@ -0,0 +1,126 @@ + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanCreatedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanApplicationModifiedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanApprovedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanUndoApprovalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanRejectedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanDisbursalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanUndoDisbursalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanStatusChangedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanBalanceChangedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent' + + + + + + + + diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java new file mode 100644 index 00000000000..332a7ea799c --- /dev/null +++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java @@ -0,0 +1,573 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.serialization.mapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.avro.generic.v1.CurrencyDataV1; +import org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1; +import org.apache.fineract.avro.loan.v1.LoanApplicationTimelineDataV1; +import org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalNearBreachDataV1; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.ExternalIdMapper; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; +import org.apache.fineract.portfolio.loanaccount.data.LoanApplicationTimelineData; +import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; +import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanChargeData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDisbursementDetailData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanSummaryData; +import org.apache.fineract.portfolio.workingcapitalloanbreach.data.WorkingCapitalBreachData; +import org.apache.fineract.portfolio.workingcapitalloannearbreach.data.WorkingCapitalNearBreachData; +import org.junit.jupiter.api.Test; + +class WorkingCapitalLoanAccountDataMapperTest { + + private final WorkingCapitalLoanAccountDataMapper mapper = new WorkingCapitalLoanAccountDataMapperImpl(new AvroDateTimeMapper(), + new ExternalIdMapper()); + + @Test + void map_nullSource_returnsNull() { + assertNull(mapper.map((WorkingCapitalLoanData) null)); + } + + @Test + void map_loanData_coversAllTopLevelFields() { + final WorkingCapitalLoanData source = fullLoanData(); + + final WorkingCapitalLoanAccountDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(101L, result.getId()); + assertEquals("WC-000101", result.getAccountNo()); + assertEquals("ext-101", result.getExternalId()); + assertEquals(11L, result.getClientId()); + assertEquals("CL-11", result.getClientAccountNo()); + assertEquals("John Doe", result.getClientName()); + assertEquals(22L, result.getClientOfficeId()); + assertEquals("client-ext", result.getClientExternalId()); + assertEquals(33L, result.getLoanProductId()); + assertEquals("WC Product", result.getLoanProductName()); + assertEquals("WC product description", result.getLoanProductDescription()); + assertEquals(44L, result.getFundId()); + assertEquals("Main Fund", result.getFundName()); + assertEquals(new BigDecimal("1000.00"), result.getPrincipal()); + assertEquals(new BigDecimal("1100.00"), result.getApprovedPrincipal()); + assertEquals(new BigDecimal("1200.00"), result.getProposedPrincipal()); + assertEquals(new BigDecimal("980.00"), result.getNetDisbursalAmount()); + assertEquals(90, result.getNumberOfRepayments()); + assertEquals(30, result.getRepaymentEvery()); + assertEquals(7, result.getLoanProductCounter()); + assertEquals(360, result.getNpvDayCount()); + assertEquals(new BigDecimal("2.50"), result.getPaymentRate()); + assertEquals(new BigDecimal("12.00"), result.getDiscountFee()); + assertEquals(new BigDecimal("13.00"), result.getProposedDiscountFee()); + assertEquals(new BigDecimal("14.00"), result.getApprovedDiscountFee()); + assertEquals(new BigDecimal("120.00"), result.getPeriodPaymentAmount()); + assertEquals(new BigDecimal("5000.00"), result.getTotalPaymentVolume()); + assertEquals(Boolean.TRUE, result.getChargedOff()); + assertEquals(Boolean.TRUE, result.getEnableInstallmentLevelDelinquency()); + + // actualNoTerm duplicates numberOfRepayments + assertEquals(90, result.getActualNoTerm()); + + // delinquencyGraceDays is exposed under the legacy loan field name + assertEquals(7, result.getGraceOnArrearsAgeing()); + + // totalOverpaid is pulled from the nested summary.overpayment + assertEquals(new BigDecimal("9.99"), result.getTotalOverpaid()); + + // EIR values are re-scaled to the fixed avro decimal scale (8, HALF_UP) + assertEquals(new BigDecimal("0.05000000"), result.getDailyEir()); + assertEquals(8, result.getDailyEir().scale()); + assertEquals(new BigDecimal("18.25000000"), result.getCalculatedAnnualEir()); + assertEquals(8, result.getCalculatedAnnualEir().scale()); + assertEquals("2024-02-01", result.getLastClosedBusinessDate()); + assertStringEnum(source.getAmortizationType(), result.getAmortizationType()); + assertStringEnum(source.getRepaymentFrequencyType(), result.getRepaymentFrequencyType()); + assertNotNull(result.getStatus()); + assertNotNull(result.getTimeline()); + assertNotNull(result.getSummary()); + assertNotNull(result.getDelinquent()); + assertNotNull(result.getDelinquency()); + assertNotNull(result.getBreach()); + assertEquals(1, result.getCharges().size()); + assertEquals(1, result.getDisbursementDetails().size()); + assertEquals(1, result.getOriginators().size()); + + // serializer-only fields stay unmapped + assertNull(result.getOverpaidOnDate()); + assertNull(result.getCustomData()); + } + + @Test + void map_loanData_mapsStatusFully() { + final LoanStatusEnumDataV1 status = mapper.map(fullLoanData()).getStatus(); + + assertNotNull(status); + assertEquals(300, status.getId()); + assertEquals("loanStatusType.active", status.getCode()); + assertEquals("Active", status.getValue()); + assertEquals(Boolean.FALSE, status.getPendingApproval()); + assertEquals(Boolean.FALSE, status.getWaitingForDisbursal()); + assertEquals(Boolean.TRUE, status.getActive()); + assertEquals(Boolean.FALSE, status.getClosedObligationsMet()); + assertEquals(Boolean.FALSE, status.getClosedWrittenOff()); + assertEquals(Boolean.FALSE, status.getClosedRescheduled()); + assertEquals(Boolean.FALSE, status.getClosed()); + assertEquals(Boolean.FALSE, status.getOverpaid()); + } + + @Test + void map_loanData_mapsTimelineFully() { + final LoanApplicationTimelineDataV1 timeline = mapper.map(fullLoanData()).getTimeline(); + + assertNotNull(timeline); + assertEquals("2024-01-01", timeline.getSubmittedOnDate()); + assertEquals("sub-user", timeline.getSubmittedByUsername()); + assertEquals("sub-first", timeline.getSubmittedByFirstname()); + assertEquals("sub-last", timeline.getSubmittedByLastname()); + assertEquals("2024-01-02", timeline.getRejectedOnDate()); + assertEquals("rej-user", timeline.getRejectedByUsername()); + assertEquals("rej-first", timeline.getRejectedByFirstname()); + assertEquals("rej-last", timeline.getRejectedByLastname()); + assertEquals("2024-01-03", timeline.getWithdrawnOnDate()); + assertEquals("wd-user", timeline.getWithdrawnByUsername()); + assertEquals("wd-first", timeline.getWithdrawnByFirstname()); + assertEquals("wd-last", timeline.getWithdrawnByLastname()); + assertEquals("2024-01-04", timeline.getApprovedOnDate()); + assertEquals("app-user", timeline.getApprovedByUsername()); + assertEquals("app-first", timeline.getApprovedByFirstname()); + assertEquals("app-last", timeline.getApprovedByLastname()); + assertEquals("2024-01-05", timeline.getExpectedDisbursementDate()); + assertEquals("2024-01-06", timeline.getActualDisbursementDate()); + assertEquals("dis-user", timeline.getDisbursedByUsername()); + assertEquals("dis-first", timeline.getDisbursedByFirstname()); + assertEquals("dis-last", timeline.getDisbursedByLastname()); + assertEquals("2024-01-07", timeline.getClosedOnDate()); + assertEquals("cl-user", timeline.getClosedByUsername()); + assertEquals("cl-first", timeline.getClosedByFirstname()); + assertEquals("cl-last", timeline.getClosedByLastname()); + assertEquals("2024-01-08", timeline.getExpectedMaturityDate()); + assertEquals("2024-01-09", timeline.getWriteOffOnDate()); + assertEquals("wo-user", timeline.getWriteOffByUsername()); + assertEquals("wo-first", timeline.getWriteOffByFirstname()); + assertEquals("wo-last", timeline.getWriteOffByLastname()); + assertEquals("2024-01-10", timeline.getChargedOffOnDate()); + assertEquals("co-user", timeline.getChargedOffByUsername()); + assertEquals("co-first", timeline.getChargedOffByFirstname()); + assertEquals("co-last", timeline.getChargedOffByLastname()); + } + + @Test + void map_loanData_mapsOriginatorsFully() { + final var originator = mapper.map(fullLoanData()).getOriginators().getFirst(); + + assertNotNull(originator); + assertEquals(70L, originator.getId()); + assertEquals("orig-ext", originator.getExternalId()); + assertEquals("Origin Bank", originator.getName()); + assertEquals("ACTIVE", originator.getStatus()); + + assertNotNull(originator.getOriginatorType()); + assertEquals(71L, originator.getOriginatorType().getId()); + assertEquals("Broker", originator.getOriginatorType().getName()); + assertEquals(1, originator.getOriginatorType().getPosition()); + assertEquals("originator type", originator.getOriginatorType().getDescription()); + assertEquals(Boolean.TRUE, originator.getOriginatorType().getActive()); + assertEquals(Boolean.FALSE, originator.getOriginatorType().getMandatory()); + + assertNotNull(originator.getChannelType()); + assertEquals(72L, originator.getChannelType().getId()); + assertEquals("Online", originator.getChannelType().getName()); + } + + @Test + void map_loanData_mapsDelinquencyFully() { + final WorkingCapitalLoanData source = fullLoanData(); + + final WorkingCapitalLoanDelinquencyDataV1 delinquency = mapper.map(source).getDelinquency(); + + assertNotNull(delinquency); + assertStringEnum(source.getDelinquencyStartType(), delinquency.getDelinquencyStartType()); + assertEquals("2024-01-05", delinquency.getDelinquencyStartDate()); + } + + @Test + void map_loanData_mapsBreachWithNearBreachFully() { + final WorkingCapitalBreachDataV1 breach = mapper.map(fullLoanData()).getBreach(); + + assertNotNull(breach); + assertEquals(80L, breach.getId()); + assertEquals("Breach rule", breach.getName()); + assertEquals(2, breach.getBreachFrequency()); + assertStringEnum(fullBreach().getBreachFrequencyType(), breach.getBreachFrequencyType()); + assertStringEnum(fullBreach().getBreachAmountCalculationType(), breach.getBreachAmountCalculationType()); + assertEquals(new BigDecimal("500.00"), breach.getBreachAmount()); + + // loan-level fields lifted into the breach record + assertEquals(3, breach.getBreachGraceDays()); + assertEquals("2024-01-06", breach.getBreachStartDate()); + + final WorkingCapitalNearBreachDataV1 nearBreach = breach.getNearBreach(); + assertNotNull(nearBreach); + assertEquals(81L, nearBreach.getId()); + assertEquals("Near breach rule", nearBreach.getName()); + assertEquals(1, nearBreach.getFrequency()); + assertStringEnum(fullNearBreach().getFrequencyType(), nearBreach.getFrequencyType()); + assertEquals(new BigDecimal("450.00"), nearBreach.getThreshold()); + } + + @Test + void map_loanData_withoutBreachConfig_stillCreatesBreachRecordFromLoanFields() { + final WorkingCapitalLoanData source = fullLoanData(); + source.setBreach(null); + source.setNearBreach(null); + + final WorkingCapitalBreachDataV1 breach = mapper.map(source).getBreach(); + + assertNotNull(breach); + assertNull(breach.getId()); + assertNull(breach.getName()); + assertNull(breach.getNearBreach()); + assertEquals(3, breach.getBreachGraceDays()); + assertEquals("2024-01-06", breach.getBreachStartDate()); + } + + @Test + void map_summary_coversAllFields() { + final WorkingCapitalLoanSummaryData source = fullSummary(); + + final WorkingCapitalLoanSummaryDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(new BigDecimal("1000"), result.getTotalPrincipal()); + assertEquals(new BigDecimal("950"), result.getPrincipalDisbursed()); + assertEquals(new BigDecimal("50"), result.getFeeChargesCharged()); + assertEquals(new BigDecimal("20"), result.getFeeChargesPaid()); + assertEquals(new BigDecimal("30"), result.getFeeChargesOutstanding()); + assertEquals(new BigDecimal("10"), result.getPenaltyChargesCharged()); + assertEquals(new BigDecimal("4"), result.getPenaltyChargesPaid()); + assertEquals(new BigDecimal("6"), result.getPenaltyChargesOutstanding()); + assertEquals(new BigDecimal("100"), result.getPrincipalPaid()); + assertEquals(new BigDecimal("900"), result.getPrincipalOutstanding()); + assertEquals(new BigDecimal("1500"), result.getTotalExpectedRepayment()); + assertEquals(new BigDecimal("600"), result.getTotalRepayment()); + assertEquals(new BigDecimal("900"), result.getTotalOutstanding()); + assertEquals(new BigDecimal("15"), result.getRealizedIncomeFromDiscountFee()); + assertEquals(new BigDecimal("35"), result.getUnrealizedIncomeFromDiscountFee()); + assertEquals(new BigDecimal("50"), result.getTotalDiscountFee()); + assertEquals(new BigDecimal("5"), result.getTotalDiscountFeeAdjustment()); + assertCurrency(source.getCurrency(), result.getCurrency()); + } + + @Test + void map_summary_leavesStubAndSerializerFilledFieldsNull() { + final WorkingCapitalLoanSummaryDataV1 result = mapper.map(fullSummary()); + + // not-yet-implemented functionality stubs + assertNull(result.getPrincipalAdjustments()); + assertNull(result.getPrincipalWrittenOff()); + assertNull(result.getFeeChargesWrittenOff()); + assertNull(result.getPenaltyChargesWrittenOff()); + assertNull(result.getTotalWrittenOff()); + assertNull(result.getTotalRecovered()); + assertNull(result.getWriteoffReasonId()); + assertNull(result.getWriteoffReason()); + assertNull(result.getChargeOffReasonId()); + assertNull(result.getChargeOffReason()); + + // per-transaction-type totals are filled by the serializer, not the mapper + assertNull(result.getTotalPayoutRefund()); + assertNull(result.getTotalPayoutRefundReversed()); + assertNull(result.getTotalGoodwillCredit()); + assertNull(result.getTotalGoodwillCreditReversed()); + assertNull(result.getTotalChargeAdjustment()); + assertNull(result.getTotalChargeAdjustmentReversed()); + assertNull(result.getTotalCreditBalanceRefund()); + assertNull(result.getTotalCreditBalanceRefundReversed()); + assertNull(result.getTotalRepaymentTransaction()); + assertNull(result.getTotalRepaymentTransactionReversed()); + } + + @Test + void map_collection_coversAllFields() { + final WorkingCapitalLoanCollectionData source = fullCollection(); + + final WorkingCapitalLoanCollectionDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(3, result.getDelinquentDays()); + assertEquals("2024-02-01", result.getDelinquentDate()); + assertEquals(new BigDecimal("100"), result.getDelinquentAmount()); + + // delinquentPrincipal is exposed as totalDelinquentAmount + assertEquals(new BigDecimal("80"), result.getTotalDelinquentAmount()); + + assertNotNull(result.getDelinquencyPausePeriods()); + assertEquals(1, result.getDelinquencyPausePeriods().size()); + assertEquals(Boolean.TRUE, result.getDelinquencyPausePeriods().getFirst().getActive()); + assertEquals("2024-01-10", result.getDelinquencyPausePeriods().getFirst().getPausePeriodStart()); + assertEquals("2024-01-20", result.getDelinquencyPausePeriods().getFirst().getPausePeriodEnd()); + + // serializer-only fields stay unmapped + assertNull(result.getLastPaymentDate()); + assertNull(result.getLastPaymentAmount()); + assertNull(result.getLastRepaymentDate()); + assertNull(result.getLastRepaymentAmount()); + assertNull(result.getDelinquencySchedule()); + } + + @Test + void map_delinquencySchedulePeriod_coversAllFields() { + final WorkingCapitalLoanDelinquencyRangeScheduleData source = fullSchedulePeriod(); + + final WorkingCapitalLoanDelinquencySchedulePeriodDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(2, result.getPeriodNumber()); + assertEquals("2024-01-01", result.getFromDate()); + assertEquals("2024-01-30", result.getToDate()); + assertEquals(new BigDecimal("150.00"), result.getExpectedAmount()); + assertEquals(new BigDecimal("50.00"), result.getPaidAmount()); + assertEquals(new BigDecimal("100.00"), result.getOutstandingAmount()); + assertEquals(Boolean.FALSE, result.getMinPaymentCriteriaMet()); + assertEquals(12, result.getDelinquentDays()); + assertEquals(new BigDecimal("100.00"), result.getDelinquentAmount()); + } + + @Test + void mapDelinquencySchedule_mapsEveryPeriod() { + final List result = mapper + .mapDelinquencySchedule(List.of(fullSchedulePeriod(), fullSchedulePeriod())); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(2, result.getFirst().getPeriodNumber()); + } + + @Test + void map_charge_coversAllFields() { + final WorkingCapitalLoanChargeData source = fullCharge(); + + final WorkingCapitalLoanChargeDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(7L, result.getId()); + assertEquals(11L, result.getChargeId()); + assertEquals("Processing fee", result.getName()); + assertEquals("2024-03-01", result.getSubmittedOnDate()); + assertEquals("2024-03-02", result.getDueDate()); + assertEquals(new BigDecimal("25.00"), result.getAmount()); + assertEquals(new BigDecimal("10.00"), result.getAmountPaid()); + assertEquals(new BigDecimal("15.00"), result.getAmountOutstanding()); + assertEquals(Boolean.FALSE, result.getPenalty()); + assertEquals(Boolean.FALSE, result.getPaid()); + assertEquals(99L, result.getLoanId()); + assertEquals("charge-ext", result.getExternalId()); + assertEquals("loan-ext", result.getExternalLoanId()); + assertEquals(1, result.getChargeTimeType().getId()); + assertEquals("chargeTimeType.disbursement", result.getChargeTimeType().getCode()); + assertEquals("Disbursement", result.getChargeTimeType().getValue()); + assertEquals(2, result.getChargeCalculationType().getId()); + assertEquals("chargeCalculationType.flat", result.getChargeCalculationType().getCode()); + assertEquals("Flat", result.getChargeCalculationType().getValue()); + assertEquals(3, result.getChargePaymentMode().getId()); + assertEquals("chargePaymentMode.regular", result.getChargePaymentMode().getCode()); + assertEquals("Regular", result.getChargePaymentMode().getValue()); + + assertCurrency(source.getCurrency(), result.getCurrency()); + + // accrual fields are intentionally not mapped for working capital charges + assertNull(result.getAmountAccrued()); + assertNull(result.getAmountUnrecognized()); + + // write-off is not implemented and custom data is never filled for the loan-details event + assertNull(result.getAmountWrittenOff()); + assertNull(result.getCustomData()); + } + + @Test + void map_disbursementDetail_coversAllFields() { + final WorkingCapitalLoanDisbursementDetailData source = fullDisbursement(); + + final WorkingCapitalLoanDisbursementDetailDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(42, result.getId()); + assertEquals(new BigDecimal("500.00"), result.getPrincipal()); + assertEquals("2024-01-01", result.getExpectedDisbursementDate()); + assertEquals("2024-01-02", result.getActualDisbursementDate()); + + // netDisbursalAmount is intentionally not mapped + assertNull(result.getNetDisbursalAmount()); + } + + @Test + void toAvroDecimalScale_nullReturnsNull_valueRescaledToEightHalfUp() { + assertNull(mapper.toAvroDecimalScale(null)); + + final BigDecimal scaled = mapper.toAvroDecimalScale(new BigDecimal("0.123456785")); + assertEquals(8, scaled.scale()); + assertEquals(new BigDecimal("0.12345679"), scaled); + } + + private static void assertStringEnum(final StringEnumOptionData source, final StringEnumOptionDataV1 result) { + assertNotNull(result); + assertEquals(source.getId(), result.getId()); + assertEquals(source.getCode(), result.getCode()); + assertEquals(source.getValue(), result.getValue()); + } + + private static void assertCurrency(final CurrencyData source, final CurrencyDataV1 result) { + assertNotNull(result); + assertEquals(source.getCode(), result.getCode()); + assertEquals(source.getName(), result.getName()); + assertEquals(source.getDecimalPlaces(), result.getDecimalPlaces()); + assertEquals(source.getInMultiplesOf(), result.getInMultiplesOf()); + assertEquals(source.getDisplaySymbol(), result.getDisplaySymbol()); + assertEquals(source.getNameCode(), result.getNameCode()); + assertEquals(source.getDisplayLabel(), result.getDisplayLabel()); + } + + private static CurrencyData currency() { + return new CurrencyData("USD", "US Dollar", 2, 1, "$", "currency.USD"); + } + + private static StringEnumOptionData stringEnum(final String id, final String code, final String value) { + return new StringEnumOptionData(id, code, value); + } + + private static WorkingCapitalLoanData fullLoanData() { + return WorkingCapitalLoanData.builder().id(101L).accountNo("WC-000101").externalId(new ExternalId("ext-101")).clientId(11L) + .clientAccountNo("CL-11").clientName("John Doe").clientExternalId(new ExternalId("client-ext")).clientOfficeId(22L) + .fundId(44L).fundName("Main Fund").loanProductId(33L).loanProductName("WC Product") + .loanProductDescription("WC product description").status(fullStatus()).proposedPrincipal(new BigDecimal("1200.00")) + .approvedPrincipal(new BigDecimal("1100.00")).principal(new BigDecimal("1000.00")) + .netDisbursalAmount(new BigDecimal("980.00")) + .amortizationType(stringEnum("1", "amortizationType.equal.installments", "Equal installments")).npvDayCount(360) + .loanProductCounter(7).paymentRate(new BigDecimal("2.50")).repaymentEvery(30) + .repaymentFrequencyType(stringEnum("0", "repaymentFrequency.days", "Days")).discountFee(new BigDecimal("12.00")) + .proposedDiscountFee(new BigDecimal("13.00")).approvedDiscountFee(new BigDecimal("14.00")).numberOfRepayments(90) + .periodPaymentAmount(new BigDecimal("120.00")).dailyEir(new BigDecimal("0.05")).calculatedAnnualEir(new BigDecimal("18.25")) + .totalPaymentVolume(new BigDecimal("5000.00")).breachGraceDays(3).delinquencyGraceDays(7) + .delinquencyStartType(stringEnum("1", "delinquencyStart.disbursement", "Disbursement")) + .delinquencyStartDate(LocalDate.of(2024, 1, 5)).breachStartDate(LocalDate.of(2024, 1, 6)) + .lastClosedBusinessDate(LocalDate.of(2024, 2, 1)).chargedOff(Boolean.TRUE).enableInstallmentLevelDelinquency(Boolean.TRUE) + .currency(currency()).timeline(fullTimeline()).summary(fullSummary()).delinquent(fullCollection()).breach(fullBreach()) + .nearBreach(fullNearBreach()).charges(List.of(fullCharge())).disbursementDetails(List.of(fullDisbursement())) + .originators(List.of(fullOriginator())).build(); + } + + private static LoanStatusEnumData fullStatus() { + return new LoanStatusEnumData(300L, "loanStatusType.active", "Active"); + } + + private static LoanApplicationTimelineData fullTimeline() { + return new LoanApplicationTimelineData().setSubmittedOnDate(LocalDate.of(2024, 1, 1)).setSubmittedByUsername("sub-user") + .setSubmittedByFirstname("sub-first").setSubmittedByLastname("sub-last").setRejectedOnDate(LocalDate.of(2024, 1, 2)) + .setRejectedByUsername("rej-user").setRejectedByFirstname("rej-first").setRejectedByLastname("rej-last") + .setWithdrawnOnDate(LocalDate.of(2024, 1, 3)).setWithdrawnByUsername("wd-user").setWithdrawnByFirstname("wd-first") + .setWithdrawnByLastname("wd-last").setApprovedOnDate(LocalDate.of(2024, 1, 4)).setApprovedByUsername("app-user") + .setApprovedByFirstname("app-first").setApprovedByLastname("app-last").setExpectedDisbursementDate(LocalDate.of(2024, 1, 5)) + .setActualDisbursementDate(LocalDate.of(2024, 1, 6)).setDisbursedByUsername("dis-user").setDisbursedByFirstname("dis-first") + .setDisbursedByLastname("dis-last").setClosedOnDate(LocalDate.of(2024, 1, 7)).setClosedByUsername("cl-user") + .setClosedByFirstname("cl-first").setClosedByLastname("cl-last").setExpectedMaturityDate(LocalDate.of(2024, 1, 8)) + .setWriteOffOnDate(LocalDate.of(2024, 1, 9)).setWriteOffByUsername("wo-user").setWriteOffByFirstname("wo-first") + .setWriteOffByLastname("wo-last").setChargedOffOnDate(LocalDate.of(2024, 1, 10)).setChargedOffByUsername("co-user") + .setChargedOffByFirstname("co-first").setChargedOffByLastname("co-last"); + } + + private static WorkingCapitalLoanSummaryData fullSummary() { + return WorkingCapitalLoanSummaryData.builder().currency(currency()).principal(new BigDecimal("1000")) + .principalPaid(new BigDecimal("100")).principalOutstanding(new BigDecimal("900")).fee(new BigDecimal("50")) + .feePaid(new BigDecimal("20")).feeOutstanding(new BigDecimal("30")).penalty(new BigDecimal("10")) + .penaltyPaid(new BigDecimal("4")).penaltyOutstanding(new BigDecimal("6")) + .realizedIncomeFromDiscountFee(new BigDecimal("15")).unrealizedIncomeFromDiscountFee(new BigDecimal("35")) + .overpayment(new BigDecimal("9.99")).totalDisbursement(new BigDecimal("950")).totalDiscountFee(new BigDecimal("50")) + .totalDiscountFeeAdjustment(new BigDecimal("5")).totalExpectedRepayment(new BigDecimal("1500")) + .totalRepayment(new BigDecimal("600")).totalOutstanding(new BigDecimal("900")).build(); + } + + private static WorkingCapitalLoanCollectionData fullCollection() { + final DelinquencyPausePeriod pause = new DelinquencyPausePeriod(true, LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 20)); + return new WorkingCapitalLoanCollectionData(5L, 3L, LocalDate.of(2024, 2, 1), new BigDecimal("100"), List.of(pause), List.of(), + new BigDecimal("80")); + } + + private static WorkingCapitalLoanDelinquencyRangeScheduleData fullSchedulePeriod() { + return new WorkingCapitalLoanDelinquencyRangeScheduleData(9L, 101L, 2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 30), + new BigDecimal("150.00"), new BigDecimal("50.00"), new BigDecimal("100.00"), Boolean.FALSE, 12L, new BigDecimal("100.00")); + } + + private static WorkingCapitalLoanChargeData fullCharge() { + return WorkingCapitalLoanChargeData.builder().id(7L).chargeId(11L).name("Processing fee") + .chargeTimeType(new EnumOptionData(1L, "chargeTimeType.disbursement", "Disbursement")) + .submittedOnDate(LocalDate.of(2024, 3, 1)).dueDate(LocalDate.of(2024, 3, 2)) + .chargeCalculationType(new EnumOptionData(2L, "chargeCalculationType.flat", "Flat")).currency(currency()) + .amount(new BigDecimal("25.00")).amountPaid(new BigDecimal("10.00")).amountOutstanding(new BigDecimal("15.00")) + .penalty(false).chargePaymentMode(new EnumOptionData(3L, "chargePaymentMode.regular", "Regular")).paid(false).loanId(99L) + .externalId(new ExternalId("charge-ext")).externalLoanId(new ExternalId("loan-ext")).build(); + } + + private static WorkingCapitalLoanDisbursementDetailData fullDisbursement() { + return WorkingCapitalLoanDisbursementDetailData.builder().id(42L).principal(new BigDecimal("500.00")) + .expectedDisbursementDate(LocalDate.of(2024, 1, 1)).actualDisbursementDate(LocalDate.of(2024, 1, 2)).build(); + } + + private static WorkingCapitalBreachData fullBreach() { + return WorkingCapitalBreachData.builder().id(80L).name("Breach rule").breachFrequency(2) + .breachFrequencyType(stringEnum("1", "breachFrequency.days", "Days")) + .breachAmountCalculationType(stringEnum("1", "breachAmount.fixed", "Fixed")).breachAmount(new BigDecimal("500.00")).build(); + } + + private static WorkingCapitalNearBreachData fullNearBreach() { + return WorkingCapitalNearBreachData.builder().id(81L).name("Near breach rule").frequency(1) + .frequencyType(stringEnum("1", "nearBreachFrequency.days", "Days")).threshold(new BigDecimal("450.00")).build(); + } + + private static LoanOriginatorData fullOriginator() { + final CodeValueData originatorType = CodeValueData.instance(71L, "Broker", "originator type", 1, true, false); + final CodeValueData channelType = CodeValueData.instance(72L, "Online", "channel type", 2, true, false); + return LoanOriginatorData.builder().id(70L).externalId("orig-ext").name("Origin Bank").status("ACTIVE") + .originatorType(originatorType).channelType(channelType).build(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index a97e9fc8184..303ac43ad45 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -715,6 +715,56 @@ public static ArrayList> getDefaultExternalEventConfiguratio workingCapitalLoanChargeAdjustmentPostBusinessEvent.put("enabled", false); defaults.add(workingCapitalLoanChargeAdjustmentPostBusinessEvent); + Map workingCapitalLoanCreatedBusinessEvent = new HashMap<>(); + workingCapitalLoanCreatedBusinessEvent.put("type", "WorkingCapitalLoanCreatedBusinessEvent"); + workingCapitalLoanCreatedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanCreatedBusinessEvent); + + Map workingCapitalLoanApplicationModifiedBusinessEvent = new HashMap<>(); + workingCapitalLoanApplicationModifiedBusinessEvent.put("type", "WorkingCapitalLoanApplicationModifiedBusinessEvent"); + workingCapitalLoanApplicationModifiedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanApplicationModifiedBusinessEvent); + + Map workingCapitalLoanApprovedBusinessEvent = new HashMap<>(); + workingCapitalLoanApprovedBusinessEvent.put("type", "WorkingCapitalLoanApprovedBusinessEvent"); + workingCapitalLoanApprovedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanApprovedBusinessEvent); + + Map workingCapitalLoanUndoApprovalBusinessEvent = new HashMap<>(); + workingCapitalLoanUndoApprovalBusinessEvent.put("type", "WorkingCapitalLoanUndoApprovalBusinessEvent"); + workingCapitalLoanUndoApprovalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanUndoApprovalBusinessEvent); + + Map workingCapitalLoanRejectedBusinessEvent = new HashMap<>(); + workingCapitalLoanRejectedBusinessEvent.put("type", "WorkingCapitalLoanRejectedBusinessEvent"); + workingCapitalLoanRejectedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanRejectedBusinessEvent); + + Map workingCapitalLoanDisbursalBusinessEvent = new HashMap<>(); + workingCapitalLoanDisbursalBusinessEvent.put("type", "WorkingCapitalLoanDisbursalBusinessEvent"); + workingCapitalLoanDisbursalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanDisbursalBusinessEvent); + + Map workingCapitalLoanUndoDisbursalBusinessEvent = new HashMap<>(); + workingCapitalLoanUndoDisbursalBusinessEvent.put("type", "WorkingCapitalLoanUndoDisbursalBusinessEvent"); + workingCapitalLoanUndoDisbursalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanUndoDisbursalBusinessEvent); + + Map workingCapitalLoanStatusChangedBusinessEvent = new HashMap<>(); + workingCapitalLoanStatusChangedBusinessEvent.put("type", "WorkingCapitalLoanStatusChangedBusinessEvent"); + workingCapitalLoanStatusChangedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanStatusChangedBusinessEvent); + + Map workingCapitalLoanBalanceChangedBusinessEvent = new HashMap<>(); + workingCapitalLoanBalanceChangedBusinessEvent.put("type", "WorkingCapitalLoanBalanceChangedBusinessEvent"); + workingCapitalLoanBalanceChangedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanBalanceChangedBusinessEvent); + + Map workingCapitalLoanDelinquencyRangeChangeBusinessEvent = new HashMap<>(); + workingCapitalLoanDelinquencyRangeChangeBusinessEvent.put("type", "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"); + workingCapitalLoanDelinquencyRangeChangeBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanDelinquencyRangeChangeBusinessEvent); + return defaults; }