diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index f100886dd..175a8f51b 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -11,7 +11,7 @@ var result = {}; if (window.location.search) { // split up the query string and store in an associative array - var params = window.location.search.slice(1).split("&"); x + var params = window.location.search.slice(1).split("&"); for ( var i = 0; i < params.length; i++) { var tmp = params[i].split("="); result[tmp[0]] = unescape(tmp[1]); @@ -90,7 +90,7 @@
- +

Overview

@@ -1063,11 +1063,19 @@ Data Table datatables - + Create Data Table List Data Tables + + + datatables/{datatable} + + Retrieve Data Table Details + Update Data Table + Delete Data Table + datatables/register/{datatable}/{apptable} @@ -3813,6 +3821,34 @@ GET https://DomainName/mifosng-provider/api/v1/groups Example Values: 2=Per month, 3=Per year + + graceOnPrincipalPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period. + + + + graceOnInterestPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods. + + + + graceOnInterestCharged + + + Optional: Integer - represents the number of repayment periods that should be interest-free. + + + + interestChargedFromDate + + + Optional: Date - The date from with interest is to start being charged. + + expectedDisbursementDate @@ -4841,6 +4877,15 @@ Request Body: expectedDisbursementDate, submittedOnDate, loanType +
+ + + + + + + +
Optional Fields
graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged
@@ -6959,6 +7004,28 @@ Request Body: + + graceOnPrincipalPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period. + + + + graceOnInterestPayment + + + Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods. + + + + graceOnInterestCharged + + + Optional: Integer - represents the number of repayment periods that should be interest-free. + + + accountingRule @@ -7516,13 +7583,14 @@ GET https://DomainName/api/v1/loanproducts/{productId} name, currencyCode, digitsAfterDecimal, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, - interestCalculationPeriodType, transactionProcessingStrategyId, accountingRule + interestCalculationPeriodType, transactionProcessingStrategyId, + accountingRule
Optional Fields
- inArrearsTolerance, charges,paymentChannelToFundSourceMappings + inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, paymentChannelToFundSourceMappings @@ -9426,6 +9494,30 @@ Content-Type: application/json Credit account for creating accounting rule + + debitTags + + + Debit tags for creating accounting rule, These tags fetches the all chart of accounts that are tagged with these tagIds + + + creditTags + + + Credit tags for creating accounting rule, These tags fetches the all chart of accounts that are tagged with these tagIds + + + allowMultipleDebitEntries + + + Allows to add multiple debits if the value is true, while creating journal entries with this rule + + + allowMultipleCreditEntries + + + Allows to add multiple credits if the value is true, while creating journal entries with this rule +
@@ -9537,6 +9629,64 @@ Request Body: { "officeId":1, "resourceId":1 +} + + +POST accountingrules +Content-Type: application/json +Request Body: +{ + "name": "fordocs", + "officeId": "1", + "debitTags": ["10","11"], + "allowMultipleDebitEntries": "true", + "creditTags": ["12","13"], + "allowMultipleCreditEntries": "true", + "description": "for api docs" +} + + +{ + "officeId":1, + "resourceId":2 +} + + +POST accountingrules +Content-Type: application/json +Request Body: +{ + "name": "test123", + "officeId": "1", + "debitAccountHead": "21", + "creditTags": ["12", "13"], + "allowMultipleCreditEntries": "false", + "description": "Employee salary" +} + + +{ + "officeId":1, + "resourceId":3 +} + + +POST accountingrules +Content-Type: application/json +Request Body: +{ + "name": "fordocstest", + "officeId": "1", + "debitTags": ["10", "11"], + "allowMultipleDebitEntries": "false", + "creditAccountHead": "9", + "description": "for api docs" +} + + +{ + "officeId":1, + "resourceId":4 }
@@ -9734,6 +9884,23 @@ Content-Type: application/json Optional: If provided, used along with lockinPeriodFrequency to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years e.g. 6 Months + withdrawalFeeAmount + Optional: If provided, used along with withdrawalFeeType to indicate the amount to be charged on the account for withdrawing. e.g. 5 Flat or 5 % of Amount + + withdrawalFeeType + Optional: If provided, used along with withdrawalFeeAmount to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 Flat or 5 % of Amount + + annualFeeAmount + Optional: If provided, used along with annualFeeOnMonthDay to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + + annualFeeOnMonthDay + + Optional: If provided, used along with annualFeeAmount and monthDayFormat to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + accountingRule Specifies if accounting is enabled for the particular @@ -10252,7 +10419,7 @@ Content-Type: application/json
- +
Optional Fields
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, paymentChannelToFundSourceMappings
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay, monthDayFormat, paymentChannelToFundSourceMappings
@@ -10558,6 +10725,23 @@ Content-Type: application/json lockinPeriodFrequencyType Optional: If provided, used along with lockinPeriodFrequency to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years e.g. 6 Months + + withdrawalFeeAmount + Optional: If provided, used along with withdrawalFeeType to indicate the amount to be charged on the account for withdrawing. e.g. 5 Flat or 5 % of Amount + + withdrawalFeeType + Optional: If provided, used along with withdrawalFeeAmount to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 Flat or 5 % of Amount + + annualFeeAmount + Optional: If provided, used along with annualFeeOnMonthDay to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + + + + annualFeeOnMonthDay + + Optional: If provided, used along with annualFeeAmount and monthDayFormat to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on 10 May. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May + +
@@ -10758,7 +10942,9 @@ Content-Type: application/json
- + +
Inherited from Product (if not provided)
nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType
nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, + lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay
@@ -11240,6 +11426,57 @@ No Request Body:
+   +
+
+

Create Data Table

+

Create a new data table and registers it with the Mifos X Core application + table.

+
+
+ +POST https://DomainName/api/v1/datatables + + +POST https://DomainName/api/v1/datatables + +Content-Type: application/json +Request Body: +{ + "datatableName": "extra_client_details", + "apptableName": "m_client", + "columns": [ + { + "name": "Gender", + "type": "Dropdown", + "code": "Gender" + }, + { + "name": "Some Decimal", + "type": "Decimal", + "mandatory": true + }, + { + "name": "Birth Date", + "type": "Date" + }, + { + "name": "Question 2", + "type": "String", + "length": 100, + "mandatory": false + } + ] +} + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+
  @@ -11267,25 +11504,191 @@ No Request Body: GET https://DomainName/api/v1/datatables - + [ - { - "applicationTableName": "m_client", - "registeredTableName": "Additional Client Fields Data" - }, - { - "applicationTableName": "m_client", - "registeredTableName": "Extra Family Details Data" - }, - { - "applicationTableName": "m_loan", - "registeredTableName": "Additional Loan Fields Data" - } -] + { + "applicationTableName": "m_client", + "registeredTableName": "extra_client_details", + "columnHeaderData": [ + { + "columnName": "client_id", + "columnType": "bigint", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": false, + "isColumnPrimaryKey": true, + "columnValues": [] + }, + { + "columnName": "Gender_cd_Question", + "columnType": "int", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Some Decimal", + "columnType": "decimal", + "columnLength": 0, + "columnDisplayType": "DECIMAL", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Birth Date", + "columnType": "date", + "columnLength": 0, + "columnDisplayType": "DATE", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + } + ] + } +] +   +
+
+

Retrieve Data Table Details

+

Lists a registered data table details and the Mifos X Core + application table they are registered to.

+
+
+ +GET https://DomainName/api/v1/datatables/{datatable} + + +{ + "applicationTableName": "m_client", + "registeredTableName": "extra_client_details", + "columnHeaderData": [ + { + "columnName": "client_id", + "columnType": "bigint", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": false, + "isColumnPrimaryKey": true, + "columnValues": [] + }, + { + "columnName": "Gender_cd_Question", + "columnType": "int", + "columnLength": 0, + "columnDisplayType": "INTEGER", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Some Decimal", + "columnType": "decimal", + "columnLength": 0, + "columnDisplayType": "DECIMAL", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + }, + { + "columnName": "Birth Date", + "columnType": "date", + "columnLength": 0, + "columnDisplayType": "DATE", + "isColumnNullable": true, + "isColumnPrimaryKey": false, + "columnValues": [] + } + ] +} + +
+
+ +   +
+
+

Update Data Table

+

Modifies fields of a data table. If the apptableName parameter is passed, + data table is deregistered and registered with the new application table.

+
+
+ +PUT https://DomainName/api/v1/datatables/{datatables} + + +PUT https://DomainName/api/v1/datatables/extra_client_details + +Content-Type: application/json +Request Body: +{ + "apptableName": "m_client", + "columns": [ + { + "name": "Gender", + "type": "Dropdown", + "code": "Gender" + }, + { + "name": "Some Decimal", + "type": "Decimal", + "mandatory": true + }, + { + "name": "Birth Date", + "type": "Date" + }, + { + "name": "Question 2", + "type": "String", + "length": 100, + "mandatory": false + } + ] +} + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+
+ +   +
+
+

Delete Data Table

+

Deletes a data table and unregisters it from the Mifos X Core + application table.

+
+
+ +DELETE https://DomainName/api/v1/datatables/{datatables} + + +DELETE https://DomainName/api/v1/datatables/extra_client_details + +Content-Type: application/json +No Request Body + + +{ + "resourceIdentifier": "extra_client_details" +} + +
+ +
 
@@ -12720,8 +13123,7 @@ GET https://DomainName/api/v1/runreports/{reportName}
-   +  

Verify authentication

@@ -12736,24 +13138,60 @@ POST authentication?username=mifos&password=password Content-Type: application/json No Request Body
+

Example response of autentication for user that is not linked with any staff.

{ - "username": "mifos", - "userId": 1, - "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", - "authenticated": true, - "roles": [ - { - "id": 1, - "name": "Super user", - "description": "This role provides all application permissions." - } - ], - "permissions": [ - "ALL_FUNCTIONS" - ] + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ] } +

Example response of autentication for user that is linked with a staff member and role.

+ +{ + "username": "mifos", + "userId": 1, + "base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=", + "authenticated": true, + "officeId": 1, + "officeName": "Head Office", + "staffId": 1, + "staffDisplayName": "Director, Program", + "organisationalRole": { + "id": 100, + "code": "staffOrganisationalRoleType.programDirector", + "value": "Program Director" + }, + "roles": [ + { + "id": 1, + "name": "Super user", + "description": "This role provides all application permissions." + } + ], + "permissions": [ + "ALL_FUNCTIONS" + ] +} + + + + + + POST authentication?username=mifos&password=fail Content-Type: application/json @@ -14534,6 +14972,68 @@ Request Body: { "officeId":1, "transactionId":"RS9MCISID4WK1ZM" +} + + +POST https://DomainName/api/v1/journalentries + + +POST journalentries +Content-Type: application/json +Request Body: +{ + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "officeId": "1", + "transactionDate": "23 May 2013", + "referenceNumber": "547", + "comments": "asdf", + "accountingRule": "2", + "credits":[{"glAccountId":1, + "amount":5000}, + {"glAccountId":2, + "amount":5000} + ], + "debits":[{"glAccountId":3, + "amount":5000}, + {"glAccountId":4, + "amount":5000} + ] +} + + +{ + "officeId":1, + "transactionId":"RS9MCISID4WK1ZM" +} + + +POST https://DomainName/api/v1/journalentries + + +POST journalentries +Content-Type: application/json +Request Body: +{ + "locale": "en", + "dateFormat": "dd MMMM yyyy", + "officeId": "1", + "transactionDate": "23 May 2013", + "referenceNumber": "547", + "comments": "asdf", + "accountingRule": "3", + "credits":[{"glAccountId":1, + "amount":5000}, + {"glAccountId":2, + "amount":5000} + ], + "amount": "10000" +} + + +{ + "officeId":1, + "transactionId":"RS9MCISID4WK1ZM" }
@@ -15286,11 +15786,19 @@ GET https://DomainName/api/v1/search?query=000000111 Data Table datatables - + Create Data Table List Data Tables + + + datatables/{datatable} + + Retrieve Data Table Details + Update Data Table + Delete Data Table + datatables/register/{datatable}/{apptable} diff --git a/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql b/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql new file mode 100644 index 000000000..b17bc2e01 --- /dev/null +++ b/mifosng-db/migrations/core_db/V45__create_acc_rule_tags_table.sql @@ -0,0 +1,11 @@ +create table `acc_rule_tags` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `acc_rule_id` BIGINT(20) NOT NULL, + `tag_id` INT(11) NOT NULL, + `acc_type_enum` SMALLINT(5) NOT NULL, + primary key(`id`), + INDEX `FK_acc_accounting_rule_id` (`acc_rule_id`), + INDEX `FK_m_code_value_id` (`tag_id`), + CONSTRAINT `FK_acc_accounting_rule_id` FOREIGN KEY (`acc_rule_id`) REFERENCES `acc_accounting_rule` (`id`), + CONSTRAINT `FK_m_code_value_id` FOREIGN KEY (`tag_id`) REFERENCES `m_code_value` (`id`) +); \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql new file mode 100644 index 000000000..5bd0a585e --- /dev/null +++ b/mifosng-db/migrations/core_db/V46__extend_datatables_api.sql @@ -0,0 +1,10 @@ + +INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`) + VALUES + ('configuration', 'CREATE_DATATABLE', 'DATATABLE', 'CREATE', 0), + ('configuration', 'CREATE_DATATABLE_CHECKER', 'DATATABLE', 'CREATE', 0), + ('configuration', 'UPDATE_DATATABLE', 'DATATABLE', 'UPDATE', 0), + ('configuration', 'UPDATE_DATATABLE_CHECKER', 'DATATABLE', 'UPDATE', 0), + ('configuration', 'DELETE_DATATABLE', 'DATATABLE', 'DELETE', 0), + ('configuration', 'DELETE_DATATABLE_CHECKER', 'DATATABLE', 'DELETE', 0); + diff --git a/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql b/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql new file mode 100644 index 000000000..1531b04d1 --- /dev/null +++ b/mifosng-db/migrations/core_db/V47__staff-hierarchy-link-to-users.sql @@ -0,0 +1,14 @@ +ALTER TABLE `m_appuser` +ADD COLUMN `staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `office_id` ; + +ALTER TABLE m_appuser +ADD CONSTRAINT `fk_m_appuser_002` +FOREIGN KEY (`staff_id`) +REFERENCES m_staff (`id`) +ON DELETE NO ACTION +ON UPDATE NO ACTION +,ADD INDEX `fk_m_appuser_002x` (`staff_id` ASC); + +ALTER TABLE `m_staff` +ADD COLUMN `organisational_role_enum` SMALLINT NULL DEFAULT NULL AFTER `external_id`, +ADD COLUMN `organisational_role_parent_staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `organisational_role_enum`; diff --git a/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql new file mode 100755 index 000000000..912598592 --- /dev/null +++ b/mifosng-db/migrations/core_db/V48__adding-S3-Support.sql @@ -0,0 +1,78 @@ +INSERT INTO c_configuration (name,enabled) VALUES ('amazon-S3',0); + +/*New table for storing details of external services used*/ +CREATE TABLE `c_external_service` ( + `name` VARCHAR(150) NOT NULL, + `value` VARCHAR(250) NULL DEFAULT NULL, + UNIQUE INDEX `name` (`name`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + +INSERT INTO c_external_service (name) VALUES ('s3_bucket_name'); +INSERT INTO c_external_service (name) VALUES ('s3_access_key'); +INSERT INTO c_external_service (name) VALUES ('s3_secret_key'); + + + +/*Image tables stores details of all images*/ +CREATE TABLE IF NOT EXISTS `m_image`( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `location` varchar(500), + `storage_type_enum` SMALLINT(5), + PRIMARY KEY (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + +/*Client points to image table*/ +ALTER TABLE `m_client` + ADD COLUMN `image_id` BIGINT(20) NULL DEFAULT NULL AFTER `display_name`, + ADD CONSTRAINT `FK_m_client_m_image` FOREIGN KEY (`image_id`) REFERENCES `m_image` (`id`); + +/*Move existing image locations to new table*/ +drop procedure if exists migrate_customer_image_data; + +delimiter # +create procedure migrate_customer_image_data() +begin +DECLARE v_counter INT DEFAULT 0; +DECLARE num_of_clients INT DEFAULT 0; +DECLARE curr_image INT DEFAULT 0; +DECLARE prev_image INT DEFAULT 0; +SELECT COUNT(*) FROM m_client INTO num_of_clients; +SET @curr_client := 0; +while v_counter < num_of_clients do + SET @s = CONCAT('INSERT INTO m_image(`location`,`storage_type_enum`) select image_key, 1 FROM m_client where image_key is not null LIMIT ', v_counter , ', ', 1); + PREPARE stmt1 FROM @s; + EXECUTE stmt1; + DEALLOCATE PREPARE stmt1; + + select ifnull(max(`id`),0) from m_image INTO curr_image; + SET @z = CONCAT('select id INTO @curr_client FROM m_client where image_key is not null limit ', v_counter , ', ', 1); + PREPARE stmt2 FROM @z; + EXECUTE stmt2; + + IF (prev_image != curr_image) THEN UPDATE m_client set image_id = curr_image where id=@curr_client; + END IF; + + set prev_image=curr_image; + set v_counter=v_counter+1; +end while; +delete from m_image where `location` is null; +end# + +delimiter ; +call migrate_customer_image_data(); + +/*Now drop the procedure*/ +drop procedure if exists migrate_customer_image_data; + +/*Delete image key from client*/ +ALTER TABLE `m_client` DROP COLUMN `image_key`; + + +/*Add storage type for m_document table and update existing documents to file storage*/ +ALTER TABLE m_document ADD COLUMN storage_type_enum SMALLINT(5); +UPDATE m_document set storage_type_enum=1; + diff --git a/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql new file mode 100755 index 000000000..206e0f909 --- /dev/null +++ b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql @@ -0,0 +1,14 @@ +CREATE TABLE `m_loan_charge_paid_by` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_transaction_id` BIGINT(20) NOT NULL, + `loan_charge_id` BIGINT(20) NOT NULL, + `amount` DECIMAL(19,6) NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK__m_loan_transaction` (`loan_transaction_id`), + INDEX `FK__m_loan_charge` (`loan_charge_id`), + CONSTRAINT `FK__m_loan_charge` FOREIGN KEY (`loan_charge_id`) REFERENCES `m_loan_charge` (`id`), + CONSTRAINT `FK__m_loan_transaction` FOREIGN KEY (`loan_transaction_id`) REFERENCES `m_loan_transaction` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + diff --git a/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql b/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql new file mode 100644 index 000000000..bef3665d4 --- /dev/null +++ b/mifosng-db/migrations/core_db/V50__add-grace-settings-to-loan-product.sql @@ -0,0 +1,9 @@ +ALTER TABLE `m_product_loan` +ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `max_number_of_repayments`, +ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`, +ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`; + +ALTER TABLE `m_loan` +ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `number_of_repayments`, +ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`, +ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`; \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V51__track-additional-details-related-to-installment-performance.sql b/mifosng-db/migrations/core_db/V51__track-additional-details-related-to-installment-performance.sql new file mode 100644 index 000000000..a383eec13 --- /dev/null +++ b/mifosng-db/migrations/core_db/V51__track-additional-details-related-to-installment-performance.sql @@ -0,0 +1,5 @@ +ALTER TABLE `m_loan_repayment_schedule` +ADD COLUMN `obligations_met_on_date` DATE NULL DEFAULT NULL AFTER `completed_derived`; + +ALTER TABLE `m_loan_repayment_schedule` +CHANGE COLUMN `interest_waived_derived` `interest_waived_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `interest_writtenoff_derived` ; \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V52__add_boolean_support_cols_to_acc_accounting_rule.sql b/mifosng-db/migrations/core_db/V52__add_boolean_support_cols_to_acc_accounting_rule.sql new file mode 100644 index 000000000..55041d7d2 --- /dev/null +++ b/mifosng-db/migrations/core_db/V52__add_boolean_support_cols_to_acc_accounting_rule.sql @@ -0,0 +1,6 @@ +ALTER TABLE `acc_accounting_rule` + ADD COLUMN `allow_multiple_debits` TINYINT(1) NOT NULL DEFAULT '0' AFTER `debit_account_id`, + ADD COLUMN `allow_multiple_credits` TINYINT(1) NOT NULL DEFAULT '0' AFTER `credit_account_id`; + +ALTER TABLE acc_rule_tags + ADD UNIQUE KEY `UNIQUE_ACCOUNT_RULE_TAGS` (`acc_rule_id`,`tag_id`,`acc_type_enum`); \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V53__track-advance-and-late-payments-on-installment.sql b/mifosng-db/migrations/core_db/V53__track-advance-and-late-payments-on-installment.sql new file mode 100644 index 000000000..f7858d098 --- /dev/null +++ b/mifosng-db/migrations/core_db/V53__track-advance-and-late-payments-on-installment.sql @@ -0,0 +1,3 @@ +ALTER TABLE `m_loan_repayment_schedule` +ADD COLUMN `total_paid_in_advance_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `penalty_charges_waived_derived`, +ADD COLUMN `total_paid_late_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `total_paid_in_advance_derived`; \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V54__charge-to-income-account-mappings.sql b/mifosng-db/migrations/core_db/V54__charge-to-income-account-mappings.sql new file mode 100755 index 000000000..ef63134cb --- /dev/null +++ b/mifosng-db/migrations/core_db/V54__charge-to-income-account-mappings.sql @@ -0,0 +1,3 @@ +ALTER TABLE `acc_product_mapping` + ADD COLUMN `charge_id` BIGINT(20) NULL DEFAULT NULL AFTER `payment_type`, + ADD CONSTRAINT `FK_acc_product_mapping_m_charge` FOREIGN KEY (`charge_id`) REFERENCES `m_charge` (`id`); \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V55__add-additional-transaction-processing-strategies.sql b/mifosng-db/migrations/core_db/V55__add-additional-transaction-processing-strategies.sql new file mode 100644 index 000000000..ea34e561a --- /dev/null +++ b/mifosng-db/migrations/core_db/V55__add-additional-transaction-processing-strategies.sql @@ -0,0 +1,12 @@ +ALTER TABLE `ref_loan_transaction_processing_strategy` +DROP COLUMN `lastmodified_date` , +DROP COLUMN `lastmodifiedby_id` , +DROP COLUMN `created_date` , +DROP COLUMN `createdby_id` ; + + +INSERT INTO `ref_loan_transaction_processing_strategy` (`id`, `code`, `name`) VALUES +(5,'principal-interest-penalties-fees-order-strategy', 'Principal Interest Penalties Fees Order'); + +INSERT INTO `ref_loan_transaction_processing_strategy` (`id`,`code`, `name`) +VALUES (6,'interest-principal-penalties-fees-order-strategy', 'Interest Principal Penalties Fees Order'); \ No newline at end of file diff --git a/mifosng-db/migrations/core_db/V56__track-overpaid-amount-on-loans.sql b/mifosng-db/migrations/core_db/V56__track-overpaid-amount-on-loans.sql new file mode 100644 index 000000000..ab9f5e4bc --- /dev/null +++ b/mifosng-db/migrations/core_db/V56__track-overpaid-amount-on-loans.sql @@ -0,0 +1,2 @@ +ALTER TABLE `m_loan` +ADD COLUMN `total_overpaid_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `total_outstanding_derived`; \ No newline at end of file diff --git a/mifosng-provider/build.gradle b/mifosng-provider/build.gradle index 04bb6b2ba..4f6b03845 100644 --- a/mifosng-provider/build.gradle +++ b/mifosng-provider/build.gradle @@ -160,7 +160,8 @@ dependencies { [group: 'com.lowagie', name: 'itext-rtf', version: '2.1.7'], [group: 'org.mnode.ical4j', name: 'ical4j', version: '1.0.4'], [group: 'com.googlecode.flyway', name: 'flyway-maven-plugin', version: '2.1.1'], - [group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'] + [group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'], + [group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.2.1'] ) testCompile 'junit:junit:4.11', diff --git a/mifosng-provider/gradle.properties b/mifosng-provider/gradle.properties index c7fb5c508..657b57df9 100644 --- a/mifosng-provider/gradle.properties +++ b/mifosng-provider/gradle.properties @@ -1 +1 @@ -releaseVersion=1.2.1.RELEASE \ No newline at end of file +releaseVersion=1.3.0.RELEASE \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java index cf75b5ce4..cd35a4079 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/ClientLoanIntegrationTest.java @@ -9,10 +9,10 @@ import java.util.HashMap; import org.junit.Before; import org.junit.Test; import org.mifosplatform.integrationtests.common.ClientHelper; +import org.mifosplatform.integrationtests.common.Utils; import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilder; import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder; import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper; -import org.mifosplatform.integrationtests.common.Utils; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.builder.ResponseSpecBuilder; @@ -30,6 +30,7 @@ public class ClientLoanIntegrationTest { private ResponseSpecification responseSpec; private RequestSpecification requestSpec; private LoanTransactionHelper loanTransactionHelper; + @Before public void setup() { Utils.initializeRESTAssured(); @@ -40,7 +41,7 @@ public class ClientLoanIntegrationTest { @Test public void checkClientLoanCreateAndDisburseFlow() { - loanTransactionHelper = new LoanTransactionHelper(requestSpec,responseSpec); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); ClientHelper.verifyClientCreatedOnServer(requestSpec, responseSpec, clientID); @@ -53,21 +54,35 @@ public class ClientLoanIntegrationTest { private Integer createLoanProduct() { System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); - String loanProductJSON = new LoanProductTestBuilder().withPrincipal("12,000.00").withNumberOfRepayments("4") - .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("2") - .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() - .withinterestRatePerPeriod("1").build(); + String loanProductJSON = new LoanProductTestBuilder() // + .withPrincipal("12,000.00") // + .withNumberOfRepayments("4") // + .withRepaymentAfterEvery("1") // + .withRepaymentTypeAsMonth() // + .withinterestRatePerPeriod("1") // + .withInterestRateFrequencyTypeAsMonths() // + .withAmortizationTypeAsEqualInstallments() // + .withInterestTypeAsDecliningBalance() // + .build(); return loanTransactionHelper.getLoanProductId(loanProductJSON); } private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) { System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); - String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("12,000.00").withLoanTermFrequency("4") - .withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1") - .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2") - .withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() - .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("20 September 2011") - .withSubmittedOnDate("20 September 2011").build(clientID.toString(), loanProductID.toString()); + String loanApplicationJSON = new LoanApplicationTestBuilder() // + .withPrincipal("12,000.00") // + .withLoanTermFrequency("4") // + .withLoanTermFrequencyAsMonths() // + .withNumberOfRepayments("4") // + .withRepaymentEveryAfter("1") // + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withAmortizationTypeAsEqualInstallments() // + .withInterestTypeAsDecliningBalance() // + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() // + .withExpectedDisbursementDate("20 September 2011") // + .withSubmittedOnDate("20 September 2011") // + .build(clientID.toString(), loanProductID.toString()); return loanTransactionHelper.getLoanId(loanApplicationJSON); } @@ -81,17 +96,17 @@ public class ClientLoanIntegrationTest { assertEquals("Checking for Due Date for 2nd Month", new ArrayList(Arrays.asList(2011, 11, 20)), loanSchedule.get(2).get("dueDate")); - assertEquals("Checking for Principal Due for 2nd Month", new Float("2969.72"), loanSchedule.get(2).get("principalDue")); + assertEquals("Checking for Principal Due for 2nd Month", new Float("2969.71"), loanSchedule.get(2).get("principalDue")); assertEquals("Checking for Interest Due for 2nd Month", new Float("181.77"), loanSchedule.get(2).get("interestOriginalDue")); assertEquals("Checking for Due Date for 3rd Month", new ArrayList(Arrays.asList(2011, 12, 20)), loanSchedule.get(3).get("dueDate")); - assertEquals("Checking for Principal Due for 3rd Month", new Float("3029.11"), loanSchedule.get(3).get("principalDue")); + assertEquals("Checking for Principal Due for 3rd Month", new Float("3029.1"), loanSchedule.get(3).get("principalDue")); assertEquals("Checking for Interest Due for 3rd Month", new Float("122.38"), loanSchedule.get(3).get("interestOriginalDue")); assertEquals("Checking for Due Date for 4th Month", new ArrayList(Arrays.asList(2012, 1, 20)), loanSchedule.get(4).get("dueDate")); - assertEquals("Checking for Principal Due for 4th Month", new Float("3089.68"), loanSchedule.get(4).get("principalDue")); + assertEquals("Checking for Principal Due for 4th Month", new Float("3089.7"), loanSchedule.get(4).get("principalDue")); assertEquals("Checking for Interest Due for 4th Month", new Float("61.79"), loanSchedule.get(4).get("interestOriginalDue")); } } \ No newline at end of file diff --git a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java index fbb1d8bee..b1b0de92a 100644 --- a/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/mifosng-provider/src/integrationTest/java/org/mifosplatform/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -31,7 +31,7 @@ public class LoanApplicationTestBuilder { private final String transactionProcessingID = MIFOS_STANDARD_STRATEGY; private String expectedDisbursmentDate = ""; private String submittedOnDate = ""; - private String loanType = "individual"; //default it to individual + private String loanType = "individual"; public String build(final String clientID, final String loanProductId) { @@ -42,7 +42,7 @@ public class LoanApplicationTestBuilder { map.put("productId", loanProductId); map.put("principal", principal); map.put("loanTermFrequency", loanTermFrequency); - map.put("loanTermFrequencyType", loanTermFrequencyType); // FIXME - Should this be using + map.put("loanTermFrequencyType", loanTermFrequencyType); map.put("numberOfRepayments", numberOfRepayment); map.put("repaymentEvery", repaymentPeriod); map.put("repaymentFrequencyType", repaymentFrequencyType); @@ -53,7 +53,7 @@ public class LoanApplicationTestBuilder { map.put("transactionProcessingStrategyId", transactionProcessingID); map.put("expectedDisbursementDate", expectedDisbursmentDate); map.put("submittedOnDate", submittedOnDate); - map.put("loanType", "individual"); + map.put("loanType", loanType); return new Gson().toJson(map); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java index f9b47b31f..35de2586a 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/common/AccountingConstants.java @@ -85,7 +85,8 @@ public class AccountingConstants { "incomeFromFeeAccountId"), INCOME_FROM_PENALTIES("incomeFromPenaltyAccountId"), LOSSES_WRITTEN_OFF("writeOffAccountId"), INTEREST_RECEIVABLE( "receivableInterestAccountId"), FEES_RECEIVABLE("receivableFeeAccountId"), PENALTIES_RECEIVABLE( "receivablePenaltyAccountId"), PAYMENT_CHANNEL_FUND_SOURCE_MAPPING("paymentChannelToFundSourceMappings"), PAYMENT_TYPE( - "paymentTypeId"); + "paymentTypeId"), FEE_INCOME_ACCOUNT_MAPPING("feeToIncomeAccountMappings"), PENALTY_INCOME_ACCOUNT_MAPPING( + "penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID("incomeAccountId"); private final String value; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java index 904bfbc87..02a150463 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/api/GLAccountsApiResource.java @@ -50,9 +50,10 @@ import org.springframework.stereotype.Component; public class GLAccountsApiResource { private static final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "name", "parentId", "glCode", - "disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", "liabilityHeaderAccountOptions", - "equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", "nameDecorated", "tagId", - "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions", "allowedExpensesTagOptions")); + "disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", + "liabilityHeaderAccountOptions", "equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", + "nameDecorated", "tagId", "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", + "allowedIncomeTagOptions", "allowedExpensesTagOptions", "creditAccounts", "debitAccounts")); private final String resourceNameForPermission = "GLACCOUNT"; @@ -63,12 +64,13 @@ public class GLAccountsApiResource { private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final CodeValueReadPlatformService codeValueReadPlatformService; - + @Autowired public GLAccountsApiResource(final PlatformSecurityContext context, final GLAccountReadPlatformService glAccountReadPlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, - final AccountingDropdownReadPlatformService dropdownReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) { + final AccountingDropdownReadPlatformService dropdownReadPlatformService, + final CodeValueReadPlatformService codeValueReadPlatformService) { this.context = context; this.apiRequestParameterHelper = apiRequestParameterHelper; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; @@ -82,7 +84,7 @@ public class GLAccountsApiResource { @Path("template") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) { + public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) { this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); @@ -167,22 +169,32 @@ public class GLAccountsApiResource { private GLAccountData handleTemplate(final GLAccountData glAccountData) { final List accountTypeOptions = this.dropdownReadPlatformService.retrieveGLAccountTypeOptions(); final List usageOptions = this.dropdownReadPlatformService.retrieveGLAccountUsageOptions(); - final List assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET)); - final List liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY)); - final List equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY)); - final List incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME)); - final List expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE)); - final Collection allowedAssetsTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME); - final Collection allowedLiabilitiesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME); - final Collection allowedEquityTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME); - final Collection allowedIncomeTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME); - final Collection allowedExpensesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME); - + final List assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET)); + final List liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY)); + final List equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY)); + final List incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME)); + final List expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService + .retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE)); + final Collection allowedAssetsTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME); + final Collection allowedLiabilitiesTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME); + final Collection allowedEquityTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME); + final Collection allowedIncomeTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME); + final Collection allowedExpensesTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME); + return new GLAccountData(glAccountData, accountTypeOptions, usageOptions, assetHeaderAccountOptions, liabilityHeaderAccountOptions, - equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions, + equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions, allowedLiabilitiesTagOptions, allowedEquityTagOptions, allowedIncomeTagOptions, allowedExpensesTagOptions); } - + private List defaultIfEmpty(final List list) { List returnList = null; if (list != null && !list.isEmpty()) { @@ -190,4 +202,4 @@ public class GLAccountsApiResource { } return returnList; } -} \ No newline at end of file +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java index 4d3e9d3d3..8fd608ebd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountData.java @@ -71,17 +71,17 @@ public class GLAccountData { this.expenseHeaderAccountOptions = null; this.allowedAssetsTagOptions = null; this.allowedLiabilitiesTagOptions = null; - this.allowedEquityTagOptions=null; - this.allowedIncomeTagOptions=null; - this.allowedExpensesTagOptions=null; + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; } public GLAccountData(final GLAccountData accountData, final List accountTypeOptions, final List usageOptions, final List assetHeaderAccountOptions, final List liabilityHeaderAccountOptions, final List equityHeaderAccountOptions, - final List incomeHeaderAccountOptions, final List expenseHeaderAccountOptions, - final Collection allowedAssetsTagOptions, final Collection allowedLiabilitiesTagOptions, - final Collection allowedEquityTagOptions, final Collection allowedIncomeTagOptions, + final List incomeHeaderAccountOptions, final List expenseHeaderAccountOptions, + final Collection allowedAssetsTagOptions, final Collection allowedLiabilitiesTagOptions, + final Collection allowedEquityTagOptions, final Collection allowedIncomeTagOptions, final Collection allowedExpensesTagOptions) { this.id = accountData.id; this.name = accountData.name; @@ -116,19 +116,20 @@ public class GLAccountData { final boolean disabled = false; final boolean manualEntriesAllowed = true; final EnumOptionData type; - if(glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue()) + if (glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue()) { type = AccountingEnumerations.gLAccountType(glAccType); - else + } else { type = AccountingEnumerations.gLAccountType(GLAccountType.ASSET); + } final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(GLAccountUsage.DETAIL); final String description = null; final String nameDecorated = null; final CodeValueData tagId = null; return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, type, usage, description, nameDecorated, tagId); } - + public GLAccountData(final Long id, final String name, final String glCode) { - this.id = id; + this.id = id; this.name = name; this.parentId = null; this.glCode = glCode; @@ -148,23 +149,21 @@ public class GLAccountData { this.expenseHeaderAccountOptions = null; this.allowedAssetsTagOptions = null; this.allowedLiabilitiesTagOptions = null; - this.allowedEquityTagOptions=null; - this.allowedIncomeTagOptions=null; - this.allowedExpensesTagOptions=null; - } + this.allowedEquityTagOptions = null; + this.allowedIncomeTagOptions = null; + this.allowedExpensesTagOptions = null; + } - public Long getId() { - return this.id; - } + public Long getId() { + return this.id; + } - public String getName() { - return this.name; - } + public String getName() { + return this.name; + } - public String getGlCode() { - return this.glCode; - } - - + public String getGlCode() { + return this.glCode; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java new file mode 100644 index 000000000..7416dee50 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/data/GLAccountDataForLookup.java @@ -0,0 +1,26 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.accounting.glaccount.data; + +public class GLAccountDataForLookup { + + private final Long id; + @SuppressWarnings("unused") + private final String name; + @SuppressWarnings("unused") + private final String glCode; + + public GLAccountDataForLookup(final Long id, final String name, final String glCode) { + this.id = id; + this.name = name; + this.glCode = glCode; + } + + public Long getId() { + return this.id; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java index 353bbbb42..751ae1e9f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/exception/InvalidParentGLAccountHeadException.java @@ -1,11 +1,17 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.accounting.glaccount.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; public class InvalidParentGLAccountHeadException extends AbstractPlatformDomainRuleException { - public InvalidParentGLAccountHeadException(Long glAccountId, Long parentId) { - super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:"+parentId+", id"+glAccountId+" should not be same", glAccountId, parentId); - } + public InvalidParentGLAccountHeadException(final Long glAccountId, final Long parentId) { + super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:" + parentId + ", id" + glAccountId + " should not be same", + glAccountId, parentId); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java index d982c3153..c03ba7423 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformService.java @@ -8,6 +8,7 @@ package org.mifosplatform.accounting.glaccount.service; import java.util.List; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; public interface GLAccountReadPlatformService { @@ -24,4 +25,6 @@ public interface GLAccountReadPlatformService { List retrieveAllEnabledHeaderGLAccounts(GLAccountType accountType); GLAccountData retrieveNewGLAccountDetails(final Integer type); + + List retrieveAccountsByTagId(final Long ruleId, final Integer transactionType); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java index 1380a34bc..87b9078cd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/glaccount/service/GLAccountReadPlatformServiceImpl.java @@ -13,6 +13,7 @@ import java.util.List; import org.apache.commons.lang.StringUtils; import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; import org.mifosplatform.accounting.glaccount.domain.GLAccountUsage; import org.mifosplatform.accounting.glaccount.exception.GLAccountInvalidClassificationException; @@ -65,7 +66,7 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe final String nameDecorated = rs.getString("nameDecorated"); final Long codeId = rs.wasNull() ? null : rs.getLong("codeId"); final String codeValue = rs.getString("codeValue"); - CodeValueData tagId = CodeValueData.instance(codeId, codeValue); + final CodeValueData tagId = CodeValueData.instance(codeId, codeValue); return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, accountType, usage, description, nameDecorated, tagId); } @@ -202,4 +203,27 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe public List retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) { return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false); } + + @Override + public List retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) { + final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper(); + final String sql = "Select " + mapper.schema() + " where rule.id=? and tags.acc_type_enum=?"; + return this.jdbcTemplate.query(sql, mapper, new Object[] { ruleId, transactionType }); + } + + private static final class GLAccountDataLookUpMapper implements RowMapper { + + public String schema() { + return " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id"; + } + + @Override + public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = JdbcSupport.getLong(rs, "id"); + final String name = rs.getString("name"); + final String glCode = rs.getString("glCode"); + return new GLAccountDataForLookup(id, name, glCode); + } + + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java index 22e45080b..2c7eab279 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/command/JournalEntryCommand.java @@ -30,8 +30,8 @@ public class JournalEntryCommand { private final SingleDebitOrCreditEntryCommand[] debits; public JournalEntryCommand(final Long officeId, final LocalDate transactionDate, final String comments, - final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits, - final String referenceNumber, final Long accountingRuleId, final BigDecimal amount) { + final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits, final String referenceNumber, + final Long accountingRuleId, final BigDecimal amount) { this.officeId = officeId; this.transactionDate = transactionDate; this.comments = comments; @@ -53,46 +53,42 @@ public class JournalEntryCommand { baseDataValidator.reset().parameter("officeId").value(this.officeId).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter("comments").value(this.comments).ignoreIfNull().notExceedingLengthOf(500); - + baseDataValidator.reset().parameter("referenceNumber").value(this.referenceNumber).ignoreIfNull().notExceedingLengthOf(100); - + baseDataValidator.reset().parameter("accountingRule").value(this.accountingRuleId).ignoreIfNull().longGreaterThanZero(); - - if (this.accountingRuleId == null) { - - baseDataValidator.reset().parameter("credits").value(this.credits).notNull(); - - baseDataValidator.reset().parameter("debits").value(this.debits).notNull(); - - // validation for credit array elements - if (this.credits != null) { - if (this.credits.length == 0) { - validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); - } else { - int i = 0; - for (final SingleDebitOrCreditEntryCommand credit : this.credits) { - validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit); - i++; - } + + baseDataValidator.reset().parameter("credits").value(this.credits).ignoreIfNull(); + + baseDataValidator.reset().parameter("debits").value(this.debits).ignoreIfNull(); + + // validation for credit array elements + if (this.credits != null) { + if (this.credits.length == 0) { + validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); + } else { + int i = 0; + for (final SingleDebitOrCreditEntryCommand credit : this.credits) { + validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit); + i++; } } - - // validation for debit array elements - if (this.debits != null) { - if (this.debits.length == 0) { - validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); - } else { - int i = 0; - for (final SingleDebitOrCreditEntryCommand debit : this.debits) { - validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit); - i++; - } - } - } - } else { - baseDataValidator.reset().parameter("amount").value(this.amount).notNull().zeroOrPositiveAmount(); } + // validation for debit array elements + if (this.debits != null) { + if (this.debits.length == 0) { + validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null)); + } else { + int i = 0; + for (final SingleDebitOrCreditEntryCommand debit : this.debits) { + validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit); + i++; + } + } + } + baseDataValidator.reset().parameter("amount").value(this.amount).ignoreIfNull().zeroOrPositiveAmount(); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } @@ -134,8 +130,8 @@ public class JournalEntryCommand { return this.referenceNumber; } - public Long getAccountingRuleId() { - return this.accountingRuleId; - } + public Long getAccountingRuleId() { + return this.accountingRuleId; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/ChargePaymentDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/ChargePaymentDTO.java new file mode 100755 index 000000000..7dbde9413 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/ChargePaymentDTO.java @@ -0,0 +1,29 @@ +package org.mifosplatform.accounting.journalentry.data; + +import java.math.BigDecimal; + +public class ChargePaymentDTO { + + private final Long chargeId; + private final BigDecimal amount; + private final Long loanChargeId; + + public ChargePaymentDTO(Long chargeId, Long loanChargeId, BigDecimal amount) { + this.chargeId = chargeId; + this.amount = amount; + this.loanChargeId = loanChargeId; + } + + public Long getChargeId() { + return this.chargeId; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public Long getLoanChargeId() { + return this.loanChargeId; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java index e277c3a3a..cd17861a0 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/data/LoanTransactionDTO.java @@ -7,6 +7,7 @@ package org.mifosplatform.accounting.journalentry.data; import java.math.BigDecimal; import java.util.Date; +import java.util.List; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; @@ -28,9 +29,14 @@ public class LoanTransactionDTO { /*** Boolean values determines if the transaction is reversed ***/ private final boolean reversed; + /** Breakdowns of fees and penalties this Transaction pays **/ + private final List penaltyPayments; + private final List feePayments; + public LoanTransactionDTO(final Long paymentTypeId, final String transactionId, final Date transactionDate, final LoanTransactionEnumData transactionType, final BigDecimal amount, final BigDecimal principal, final BigDecimal interest, - final BigDecimal fees, final BigDecimal penalties, final boolean reversed) { + final BigDecimal fees, final BigDecimal penalties, final boolean reversed, final List feePayments, + final List penaltyPayments) { this.paymentTypeId = paymentTypeId; this.transactionId = transactionId; this.transactionDate = transactionDate; @@ -41,6 +47,8 @@ public class LoanTransactionDTO { this.penalties = penalties; this.reversed = reversed; this.transactionType = transactionType; + this.feePayments = feePayments; + this.penaltyPayments = penaltyPayments; } public String getTransactionId() { @@ -83,4 +91,12 @@ public class LoanTransactionDTO { return this.paymentTypeId; } + public List getPenaltyPayments() { + return this.penaltyPayments; + } + + public List getFeePayments() { + return this.feePayments; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java index 32e462a78..e5298504c 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntry.java @@ -73,7 +73,7 @@ public class JournalEntry extends AbstractAuditableCustom { @SuppressWarnings("unused") @Column(name = "entity_id") private Long entityId; - + @Column(name = "ref_num") private String referenceNumber; @@ -104,7 +104,7 @@ public class JournalEntry extends AbstractAuditableCustom { this.entityType = entityType; this.entityId = entityId; this.referenceNumber = referenceNumber; - + } public boolean isDebitEntry() { @@ -139,8 +139,8 @@ public class JournalEntry extends AbstractAuditableCustom { this.reversed = reversed; } - public String getReferenceNumber() { - return this.referenceNumber; - } + public String getReferenceNumber() { + return this.referenceNumber; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryType.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryType.java index 4d6842f85..6c316f5af 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/domain/JournalEntryType.java @@ -45,4 +45,12 @@ public enum JournalEntryType { return name().toString(); } + public boolean isDebitType() { + return this.value.equals(JournalEntryType.DEBIT.getValue()); + } + + public boolean isCreditType() { + return this.value.equals(JournalEntryType.CREDIT.getValue()); + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java index 187a2b395..f3c38462b 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/exception/JournalEntryInvalidException.java @@ -16,7 +16,7 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce /*** enum of reasons for invalid Journal Entry **/ public static enum GL_JOURNAL_ENTRY_INVALID_REASON { - FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED; + FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED; public String errorMessage() { if (name().toString().equalsIgnoreCase("FUTURE_DATE")) { @@ -25,6 +25,8 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce return "Journal entry cannot be made prior to last account closing date for the branch"; } else if (name().toString().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) { return "Journal Entry must have atleast one Debit and one Credit"; + } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT")) { + return "Sum of All Debits OR Credits must equal the Amount for a Journal Entry"; } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) { return "Sum of All Debits must equal the sum of all Credits for a Journal Entry"; } else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java index dc23811a0..7245e7949 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/AccountingProcessorHelper.java @@ -8,6 +8,7 @@ package org.mifosplatform.accounting.journalentry.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -18,6 +19,7 @@ import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_ import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_SAVINGS; import org.mifosplatform.accounting.glaccount.domain.GLAccount; +import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO; import org.mifosplatform.accounting.journalentry.data.LoanDTO; import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO; import org.mifosplatform.accounting.journalentry.data.SavingsDTO; @@ -30,6 +32,7 @@ import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidEx import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType; import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.office.domain.OfficeRepository; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; @@ -77,8 +80,28 @@ public class AccountingProcessorHelper { final boolean reversed = (Boolean) map.get("reversed"); final Long paymentTypeId = (Long) map.get("paymentTypeId"); + List feePayments = new ArrayList(); + List penaltyPayments = new ArrayList(); + // extract charge payment details (if exists) + if (map.containsKey("loanChargesPaid")) { + @SuppressWarnings("unchecked") + final List> loanChargesPaidData = (List>) map.get("loanChargesPaid"); + for (final Map loanChargePaid : loanChargesPaidData) { + final Long chargeId = (Long) loanChargePaid.get("chargeId"); + final Long loanChargeId = (Long) loanChargePaid.get("chargeId"); + final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty"); + final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount"); + ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, loanChargeId, chargeAmountPaid); + if (isPenalty) { + penaltyPayments.add(chargePaymentDTO); + } else { + feePayments.add(chargePaymentDTO); + } + } + } + final LoanTransactionDTO transaction = new LoanTransactionDTO(paymentTypeId, transactionId, transactionDate, transactionType, - amount, principal, interest, fees, penalties, reversed); + amount, principal, interest, fees, penalties, reversed, feePayments, penaltyPayments); newLoanTransactions.add(transaction); @@ -304,6 +327,48 @@ public class AccountingProcessorHelper { } } + public void createCreditJournalEntryOrReversalForLoanCharges(final Office office, final int accountMappingTypeId, + final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate, + final BigDecimal totalAmount, final Boolean isReversal, final List chargePaymentDTOs) { + /*** + * Map to track each account and the net credit to be made for a + * particular account + ***/ + Map creditDetailsMap = new LinkedHashMap(); + for (ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) { + Long chargeId = chargePaymentDTO.getChargeId(); + GLAccount chargeSpecificAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId); + BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount(); + + // adjust net credit amount if the account is already present in the + // map + if (creditDetailsMap.containsKey(chargeSpecificAccount)) { + BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount); + chargeSpecificAmount = chargeSpecificAmount.add(existingAmount); + } + creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount); + } + + BigDecimal totalCreditedAmount = BigDecimal.ZERO; + for (Map.Entry entry : creditDetailsMap.entrySet()) { + GLAccount account = entry.getKey(); + BigDecimal amount = entry.getValue(); + totalCreditedAmount = totalCreditedAmount.add(amount); + if (isReversal) { + createDebitJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount); + } else { + createCreditJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount); + } + } + + // TODO: Vishwas Temporary validation to be removed before moving to + // release branch + if (totalAmount.compareTo(totalCreditedAmount) != 0) { throw new PlatformDataIntegrityException( + "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", + "Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction", + totalCreditedAmount, totalAmount); } + } + private void createCreditJournalEntryOrReversalForLoan(final Office office, final int accountMappingTypeId, final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount, final Boolean isReversal) { @@ -363,6 +428,30 @@ public class AccountingProcessorHelper { accountMapping = paymentChannelSpecificAccountMapping; } } + + return accountMapping.getGlAccount(); + } + + private GLAccount getLinkedGLAccountForLoanCharges(final Long loanProductId, final int accountMappingTypeId, final Long chargeId) { + ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId, + PortfolioProductType.LOAN.getValue(), accountMappingTypeId); + /***** + * Get more specific mappings for Charges and penalties (based on the + * actual charge /penalty coupled with the loan product). Note the + * income from fees and income from penalties placeholder ID would be + * the same for both cash and accrual based accounts + *****/ + + // Vishwas TODO: remove this condition as it should always be true + if (accountMappingTypeId == CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue() + || accountMappingTypeId == CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue()) { + ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository + .findByProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(loanProductId, PortfolioProductType.LOAN.getValue(), + accountMappingTypeId, chargeId); + if (chargeSpecificIncomeAccountMapping != null) { + accountMapping = chargeSpecificIncomeAccountMapping; + } + } return accountMapping.getGlAccount(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index e22aa54e8..ed207651b 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -134,14 +134,15 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) { totalDebitAmount = totalDebitAmount.add(feesAmount); - helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES, loanProductId, paymentTypeId, - loanId, transactionId, transactionDate, feesAmount, isReversal); + helper.createCreditJournalEntryOrReversalForLoanCharges(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(), + loanProductId, loanId, transactionId, transactionDate, feesAmount, isReversal, loanTransactionDTO.getFeePayments()); } if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) { totalDebitAmount = totalDebitAmount.add(penaltiesAmount); - helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES, loanProductId, - paymentTypeId, loanId, transactionId, transactionDate, penaltiesAmount, isReversal); + helper.createCreditJournalEntryOrReversalForLoanCharges(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(), + loanProductId, loanId, transactionId, transactionDate, penaltiesAmount, isReversal, + loanTransactionDTO.getPenaltyPayments()); } /*** create a single debit entry (or reversal) for the entire amount **/ diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 4f04299cf..e246e3471 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -6,6 +6,7 @@ package org.mifosplatform.accounting.journalentry.service; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; @@ -15,9 +16,11 @@ import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.mifosplatform.accounting.closure.domain.GLClosure; import org.mifosplatform.accounting.closure.domain.GLClosureRepository; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository; import org.mifosplatform.accounting.glaccount.exception.GLAccountNotFoundException; +import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.journalentry.api.JournalEntryJsonInputParams; import org.mifosplatform.accounting.journalentry.command.JournalEntryCommand; import org.mifosplatform.accounting.journalentry.command.SingleDebitOrCreditEntryCommand; @@ -61,6 +64,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa private final AccountingProcessorHelper helper; private final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final AccountingRuleRepository accountingRuleRepository; + private final GLAccountReadPlatformService glAccountReadPlatformService; @Autowired public JournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository, @@ -68,7 +72,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa final GLAccountRepository glAccountRepository, final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer, final AccountingProcessorHelper accountingProcessorHelper, final AccountingRuleRepository accountingRuleRepository, final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory, - final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory) { + final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory, + final GLAccountReadPlatformService glAccountReadPlatformService) { this.glClosureRepository = glClosureRepository; this.officeRepository = officeRepository; this.glJournalEntryRepository = glJournalEntryRepository; @@ -78,6 +83,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa this.accountingProcessorForSavingsFactory = accountingProcessorForSavingsFactory; this.helper = accountingProcessorHelper; this.accountingRuleRepository = accountingRuleRepository; + this.glAccountReadPlatformService = glAccountReadPlatformService; } @Transactional @@ -99,32 +105,65 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa /** Set a transaction Id and save these Journal entries **/ final Date transactionDate = command.DateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue()); final String transactionId = generateTransactionId(); - final String referenceNumber = journalEntryCommand.getReferenceNumber(); + final String referenceNumber = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.REFERENCE_NUMBER.getValue()); final boolean manualEntry = true; if (accountRuleId != null) { - - final Long accountingRuleId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.ACCOUNTING_RULE.getValue()); final BigDecimal amount = command.bigDecimalValueOfParameterNamed(JournalEntryJsonInputParams.AMOUNT.getValue()); - final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountingRuleId); - if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountingRuleId); } - final GLAccount creditAccountHead = accountingRule.getAccountToCredit(); - validateGLAccountForTransaction(creditAccountHead); - final GLAccount debitAccountHead = accountingRule.getAccountToDebit(); - validateGLAccountForTransaction(debitAccountHead); - String description = accountingRule.getDescription(); + + String description = null; if (StringUtils.isNotBlank(journalEntryCommand.getComments())) { description = journalEntryCommand.getComments(); } - JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry, transactionDate, - JournalEntryType.CREDIT, amount, description, null, null, referenceNumber); - this.glJournalEntryRepository.saveAndFlush(creditEntry); + final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountRuleId); + if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountRuleId); } - JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry, transactionDate, - JournalEntryType.DEBIT, amount, description, null, null, referenceNumber); - this.glJournalEntryRepository.saveAndFlush(debitEntry); + if (accountingRule.getAccountToCredit() == null) { + if (journalEntryCommand.getCredits() == null) { throw new RuntimeException( + "You have to pass atleast one value for credits"); } + if (journalEntryCommand.getDebits() != null) { + checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(), + journalEntryCommand.getDebits()); + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits()); + } else { + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), amount); + } + saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getCredits(), + transactionId, JournalEntryType.CREDIT, referenceNumber); + } else { + if (journalEntryCommand.getCredits() != null) { throw new RuntimeException( + "You can not pass credits for this rule please verify your rule"); } + final GLAccount creditAccountHead = accountingRule.getAccountToCredit(); + validateGLAccountForTransaction(creditAccountHead); + final JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry, + transactionDate, JournalEntryType.CREDIT, amount, description, null, null, referenceNumber); + this.glJournalEntryRepository.saveAndFlush(creditEntry); + } + + if (accountingRule.getAccountToDebit() == null) { + if (journalEntryCommand.getDebits() == null) { throw new RuntimeException( + "You have to pass atleast one parameter for debits"); } + if (journalEntryCommand.getCredits() != null) { + checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(), + journalEntryCommand.getDebits()); + checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits()); + } else { + checkDebitAndCreditAmounts(journalEntryCommand.getDebits(), amount); + } + + saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(), + transactionId, JournalEntryType.DEBIT, referenceNumber); + } else { + if (journalEntryCommand.getDebits() != null) { throw new RuntimeException( + "You can not pass debits for this rule please verify your rule"); } + final GLAccount debitAccountHead = accountingRule.getAccountToDebit(); + validateGLAccountForTransaction(debitAccountHead); + final JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry, + transactionDate, JournalEntryType.DEBIT, amount, description, null, null, referenceNumber); + this.glJournalEntryRepository.saveAndFlush(debitEntry); + } } else { saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(), transactionId, @@ -143,7 +182,76 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa } } - private void validateGLAccountForTransaction(GLAccount creditOrDebitAccountHead) { + @SuppressWarnings("null") + private void checkDebitOrCreditAccountsAreValid(final AccountingRule accountingRule, final SingleDebitOrCreditEntryCommand[] credits, + final SingleDebitOrCreditEntryCommand[] debits) { + // Validate the debit and credit arrays are appropriate accounts + List allowedCreditGLAccounts = new ArrayList(); + List allowedDebitGLAccounts = new ArrayList(); + final SingleDebitOrCreditEntryCommand[] validCredits = new SingleDebitOrCreditEntryCommand[credits.length]; + final SingleDebitOrCreditEntryCommand[] validDebits = new SingleDebitOrCreditEntryCommand[debits.length]; + + if (credits != null && credits.length > 0) { + allowedCreditGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(), + JournalEntryType.CREDIT.getValue()); + for (final GLAccountDataForLookup accountDataForLookup : allowedCreditGLAccounts) { + for (int i = 0; i < credits.length; i++) { + final SingleDebitOrCreditEntryCommand credit = credits[i]; + if (credit.getGlAccountId().equals(accountDataForLookup.getId())) { + validCredits[i] = credit; + } + } + } + if (credits.length != validCredits.length) { throw new RuntimeException("Invalid credits"); } + } + + if (debits != null && debits.length > 0) { + allowedDebitGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(), + JournalEntryType.DEBIT.getValue()); + for (final GLAccountDataForLookup accountDataForLookup : allowedDebitGLAccounts) { + for (int i = 0; i < debits.length; i++) { + final SingleDebitOrCreditEntryCommand debit = debits[i]; + if (debit.getGlAccountId().equals(accountDataForLookup.getId())) { + validDebits[i] = debit; + } + } + } + if (debits.length != validDebits.length) { throw new RuntimeException("Invalid debits"); } + } + } + + private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits) { + // sum of all debits must be = sum of all credits + BigDecimal creditsSum = BigDecimal.ZERO; + BigDecimal debitsSum = BigDecimal.ZERO; + for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) { + if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + creditsSum = creditsSum.add(creditEntryCommand.getAmount()); + } + for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) { + if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + debitsSum = debitsSum.add(debitEntryCommand.getAmount()); + } + if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + } + + private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] creditOrDebits, final BigDecimal amount) { + if (amount == null) { throw new JournalEntryInvalidException(GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, + null, null, null); } + BigDecimal creditOrDebitSum = BigDecimal.ZERO; + for (final SingleDebitOrCreditEntryCommand creditEntryCommand : creditOrDebits) { + if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } + creditOrDebitSum = creditOrDebitSum.add(creditEntryCommand.getAmount()); + } + if (creditOrDebitSum.compareTo(amount) != 0) { throw new JournalEntryInvalidException( + GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + } + + private void validateGLAccountForTransaction(final GLAccount creditOrDebitAccountHead) { /*** * validate that the account allows manual adjustments and is not * disabled @@ -199,7 +307,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { - final LoanDTO loanDTO = helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, + final LoanDTO loanDTO = this.helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory .determineProcessor(loanDTO); @@ -209,13 +317,13 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa @Transactional @Override - public void createJournalEntriesForSavings(Map accountingBridgeData) { + public void createJournalEntriesForSavings(final Map accountingBridgeData) { final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled"); final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled"); if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) { - final SavingsDTO savingsDTO = helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, + final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled, accrualBasedAccountingEnabled); final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory .determineProcessor(savingsDTO); @@ -247,21 +355,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa if ((credits == null || credits.length <= 0) || (debits == null || debits.length <= 0)) { throw new JournalEntryInvalidException( GL_JOURNAL_ENTRY_INVALID_REASON.NO_DEBITS_OR_CREDITS, null, null, null); } - // sum of all debits must be = sum of all credits - BigDecimal creditsSum = BigDecimal.ZERO; - BigDecimal debitsSum = BigDecimal.ZERO; - for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) { - if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } - creditsSum = creditsSum.add(creditEntryCommand.getAmount()); - } - for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) { - if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); } - debitsSum = debitsSum.add(debitEntryCommand.getAmount()); - } - if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException( - GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); } + checkDebitAndCreditAmounts(credits, debits); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/ChargeToGLAccountMapper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/ChargeToGLAccountMapper.java new file mode 100755 index 000000000..8558ab1de --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/ChargeToGLAccountMapper.java @@ -0,0 +1,20 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.accounting.producttoaccountmapping.data; + +public class ChargeToGLAccountMapper { + + @SuppressWarnings("unused") + private final long chargeId; + @SuppressWarnings("unused") + private final long incomeAccountId; + + public ChargeToGLAccountMapper(long chargeId, long incomeAccountId) { + this.chargeId = chargeId; + this.incomeAccountId = incomeAccountId; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java index 96e3afcd7..03ab03fe4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/data/PaymentTypeToGLAccountMapper.java @@ -1,3 +1,8 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.accounting.producttoaccountmapping.data; public class PaymentTypeToGLAccountMapper { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java index 40146f0de..1341dcc7a 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java @@ -15,6 +15,7 @@ import javax.persistence.UniqueConstraint; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.infrastructure.codes.domain.CodeValue; +import org.mifosplatform.portfolio.charge.domain.Charge; import org.springframework.data.jpa.domain.AbstractPersistable; @Entity @@ -33,6 +34,10 @@ public class ProductToGLAccountMapping extends AbstractPersistable { @JoinColumn(name = "payment_type", nullable = true) private CodeValue paymentType; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "charge_id", nullable = true) + private Charge charge; + @Column(name = "product_type", nullable = false) private int productType; @@ -49,16 +54,27 @@ public class ProductToGLAccountMapping extends AbstractPersistable { } public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType, final int financialAccountType) { - this(glAccount, productId, productType, financialAccountType, null); + this(glAccount, productId, productType, financialAccountType, null, null); + } + + public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType, + final int financialAccountType, final Charge charge) { + this(glAccount, productId, productType, financialAccountType, null, charge); } public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType, final int financialAccountType, final CodeValue paymentType) { + this(glAccount, productId, productType, financialAccountType, paymentType, null); + } + + private ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType, + final int financialAccountType, final CodeValue paymentType, final Charge charge) { this.glAccount = glAccount; this.productId = productId; this.productType = productType; this.financialAccountType = financialAccountType; this.paymentType = paymentType; + this.charge = charge; } public GLAccount getGlAccount() { @@ -101,4 +117,12 @@ public class ProductToGLAccountMapping extends AbstractPersistable { this.paymentType = paymentType; } + public Charge getCharge() { + return this.charge; + } + + public void setCharge(Charge charge) { + this.charge = charge; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java index 0012b5061..537b38c44 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java @@ -18,7 +18,10 @@ public interface ProductToGLAccountMappingRepository extends JpaRepository findAllPaymentTypeToFundSourceMappings(@Param("productId") Long productId, @Param("productType") int productType); + /*** The financial Account Type for income from interest will always be 4 ***/ + @Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=4 and mapping.charge is not NULL") + List findAllFeeToIncomeAccountMappings(@Param("productId") Long productId, + @Param("productType") int productType); + + /*** The financial Account Type for income from interest will always be 5 ***/ + @Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=5 and mapping.charge is not NULL") + List findAllPenaltyToIncomeAccountMappings(@Param("productId") Long productId, + @Param("productType") int productType); + List findByProductIdAndProductType(Long productId, int productType); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java index 95f224e3f..1dcb5ca36 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -1,12 +1,17 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.accounting.producttoaccountmapping.service; import java.util.HashMap; import java.util.Map; -import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN; import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS; +import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository; import org.mifosplatform.accounting.glaccount.domain.GLAccountType; import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType; @@ -14,6 +19,7 @@ import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAc import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -23,10 +29,10 @@ import com.google.gson.JsonElement; public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappingHelper { @Autowired - public LoanProductToGLAccountMappingHelper(GLAccountRepository glAccountRepository, - ProductToGLAccountMappingRepository glAccountMappingRepository, FromJsonHelper fromApiJsonHelper, - CodeValueRepositoryWrapper codeValueRepositoryWrapper) { - super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper); + public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, + final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, + final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final ChargeRepositoryWrapper chargeRepositoryWrapper) { + super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper, chargeRepositoryWrapper); } /*** Set of abstractions for saving Loan Products to GL Account Mappings ***/ @@ -68,15 +74,29 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi /*** Abstractions for payments channel related to loan products ***/ public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId, - Map changes) { + final Map changes) { savePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN); } public void updatePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId, - Map changes) { + final Map changes) { updatePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN); } + public void saveChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes) { + // save both fee and penalty charges + saveChargesToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, true); + saveChargesToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false); + } + + public void updateChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes) { + // update both fee and penalty charges + updateChargeToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, true); + updateChargeToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false); + } + public Map populateChangesForNewLoanProductToGLAccountMappingCreation(final JsonElement element, final AccountingRuleType accountingRuleType) { final Map changes = new HashMap(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index 021c5f136..46b266811 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -1,3 +1,8 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.accounting.producttoaccountmapping.service; import java.util.HashMap; @@ -21,6 +26,8 @@ import org.mifosplatform.infrastructure.codes.domain.CodeValue; import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.charge.domain.Charge; +import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.mifosplatform.portfolio.paymentdetail.PaymentDetailConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -36,15 +43,17 @@ public class ProductToGLAccountMappingHelper { protected final ProductToGLAccountMappingRepository accountMappingRepository; protected final FromJsonHelper fromApiJsonHelper; private final CodeValueRepositoryWrapper codeValueRepositoryWrapper; + private final ChargeRepositoryWrapper chargeRepositoryWrapper; @Autowired public ProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, - final CodeValueRepositoryWrapper codeValueRepositoryWrapper) { + final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final ChargeRepositoryWrapper chargeRepositoryWrapper) { this.accountRepository = glAccountRepository; this.accountMappingRepository = glAccountMappingRepository; this.fromApiJsonHelper = fromApiJsonHelper; this.codeValueRepositoryWrapper = codeValueRepositoryWrapper; + this.chargeRepositoryWrapper = chargeRepositoryWrapper; } @@ -105,6 +114,128 @@ public class ProductToGLAccountMappingHelper { } } + /** + * Saves the Charge to Income account mappings for a particular + * product/product type (also populates the changes array if passed in) + * + * @param command + * @param element + * @param productId + * @param changes + */ + public void saveChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + Map changes, final PortfolioProductType portfolioProductType, final boolean isPenalty) { + String arrayName; + if (isPenalty) { + arrayName = LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(); + } else { + arrayName = LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(); + } + + JsonArray chargeToIncomeAccountMappingArray = fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); + if (chargeToIncomeAccountMappingArray != null) { + if (changes != null) { + changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(), + command.jsonFragment(LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue())); + } + for (int i = 0; i < chargeToIncomeAccountMappingArray.size(); i++) { + final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); + final Long chargeId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue()).getAsLong(); + final Long incomeAccountId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue()).getAsLong(); + saveChargeToFundSourceMapping(productId, chargeId, incomeAccountId, portfolioProductType, isPenalty); + } + } + } + + /** + * @param command + * @param element + * @param productId + * @param changes + */ + public void updateChargeToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + Map changes, final PortfolioProductType portfolioProductType, boolean isPenalty) { + // find all existing payment Channel to Fund source Mappings + List existingChargeToIncomeAccountMappings; + String arrayFragmentName; + + if (isPenalty) { + existingChargeToIncomeAccountMappings = accountMappingRepository.findAllPenaltyToIncomeAccountMappings(productId, + portfolioProductType.getValue()); + arrayFragmentName = LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(); + } else { + existingChargeToIncomeAccountMappings = accountMappingRepository.findAllFeeToIncomeAccountMappings(productId, + portfolioProductType.getValue()); + arrayFragmentName = LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(); + } + + JsonArray chargeToIncomeAccountMappingArray = fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element); + /** + * Variable stores a map representation of charges (key) and their + * associated income Id's (value) extracted from the passed in + * Jsoncommand + **/ + Map inputChargeToIncomeAccountMap = new HashMap(); + /*** + * Variable stores all charges which have already been mapped to Income + * Accounts in the system + **/ + Set existingCharges = new HashSet(); + if (chargeToIncomeAccountMappingArray != null) { + if (changes != null) { + changes.put(arrayFragmentName, command.jsonFragment(arrayFragmentName)); + } + + for (int i = 0; i < chargeToIncomeAccountMappingArray.size(); i++) { + final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); + final Long chargeId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue()).getAsLong(); + final Long incomeAccountId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue()).getAsLong(); + inputChargeToIncomeAccountMap.put(chargeId, incomeAccountId); + } + + // If input map is empty, delete all existing mappings + if (inputChargeToIncomeAccountMap.size() == 0) { + accountMappingRepository.deleteInBatch(existingChargeToIncomeAccountMappings); + }/** + * Else,
+ * update existing mappings OR
+ * delete old mappings (which are already present, but not passed in + * as a part of Jsoncommand)
+ * Create new mappings for charges that are passed in as a part of + * the Jsoncommand but not already present + * + **/ + else { + for (ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) { + Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId(); + existingCharges.add(currentCharge); + // update existing mappings (if required) + if (inputChargeToIncomeAccountMap.containsKey(currentCharge)) { + Long newGLAccountId = inputChargeToIncomeAccountMap.get(currentCharge); + if (newGLAccountId != chargeToIncomeAccountMapping.getGlAccount().getId()) { + final GLAccount glAccount = getAccountByIdAndType(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue(), + GLAccountType.INCOME, newGLAccountId); + chargeToIncomeAccountMapping.setGlAccount(glAccount); + accountMappingRepository.save(chargeToIncomeAccountMapping); + } + }// deleted payment type + else { + accountMappingRepository.delete(chargeToIncomeAccountMapping); + } + } + // create new mappings + Set incomingCharges = inputChargeToIncomeAccountMap.keySet(); + incomingCharges.removeAll(existingCharges); + // incomingPaymentTypes now only contains the newly added + // payment Type mappings + for (Long newCharge : incomingCharges) { + Long newGLAccountId = inputChargeToIncomeAccountMap.get(newCharge); + saveChargeToFundSourceMapping(productId, newCharge, newGLAccountId, portfolioProductType, isPenalty); + } + } + } + } + /** * @param command * @param element @@ -199,6 +330,34 @@ public class ProductToGLAccountMappingHelper { this.accountMappingRepository.save(accountMapping); } + /** + * @param productId + * @param jsonObject + */ + private void saveChargeToFundSourceMapping(final Long productId, final Long chargeId, final Long incomeAccountId, + final PortfolioProductType portfolioProductType, final boolean isPenalty) { + Charge charge = chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId); + + // TODO Vishwas: Need to validate if given charge is fee or Penalty + // based on input condition + + final GLAccount glAccount = getAccountByIdAndType(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue(), + GLAccountType.INCOME, incomeAccountId); + /** + * Both CASH and Accrual placeholders have the same value for income + * from Interest and penalties + **/ + CASH_ACCOUNTS_FOR_LOAN placeHolderAccountType; + if (isPenalty) { + placeHolderAccountType = CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES; + } else { + placeHolderAccountType = CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES; + } + final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping(glAccount, productId, + portfolioProductType.getValue(), placeHolderAccountType.getValue(), charge); + this.accountMappingRepository.save(accountMapping); + } + /** * Fetches account with a particular Id and throws and Exception it is not * of the expected Account Category ('ASSET','liability' etc) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java index 389bf6904..0cfa464b1 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java @@ -8,6 +8,7 @@ package org.mifosplatform.accounting.producttoaccountmapping.service; import java.util.List; import java.util.Map; +import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; public interface ProductToGLAccountMappingReadPlatformService { @@ -16,7 +17,12 @@ public interface ProductToGLAccountMappingReadPlatformService { public List fetchPaymentTypeToFundSourceMappingsForLoanProduct(final Long loanProductId); + public List fetchFeeToIncomeAccountMappingsForLoanProduct(final Long loanProductId); + + public List fetchPenaltyToIncomeAccountMappingsForLoanProduct(final Long loanProductId); + public Map fetchAccountMappingDetailsForSavingsProduct(final Long savingsProductId, final Integer accountingType); public List fetchPaymentTypeToFundSourceMappingsForSavingsProduct(final Long savingsProductId); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 6a069ebc2..5d6561a63 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -18,8 +18,10 @@ import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS; import org.mifosplatform.accounting.common.AccountingConstants.SAVINGS_PRODUCT_ACCOUNTING_PARAMS; import org.mifosplatform.accounting.common.AccountingRuleType; +import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType; +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; @@ -39,8 +41,11 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private static final class ProductToGLAccountMappingMapper implements RowMapper> { public String schema() { - return " id as id, gl_account_id as glAccountId,product_id as productId,product_type as productType,financial_account_type as financialAccountType," - + " payment_type as paymentTypeId from acc_product_mapping " + " where product_type= ? "; + return " mapping.id as id, mapping.gl_account_id as glAccountId, mapping.product_id as productId, " + + " mapping.product_type as productType,mapping.financial_account_type as financialAccountType, " + + " mapping.payment_type as paymentTypeId, mapping.charge_id as chargeId, charge.is_penalty as penalty " + + " from acc_product_mapping mapping left join m_charge charge on mapping.charge_id=charge.id " + + " where mapping.product_type= ? "; } @Override @@ -49,7 +54,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product final Long id = rs.getLong("id"); final Long glAccountId = rs.getLong("glAccountId"); final Long productId = rs.getLong("productId"); - final Long paymentTypeId = rs.getLong("paymentTypeId"); + final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentTypeId"); + final Long chargeId = rs.getLong("chargeId"); final Integer productType = rs.getInt("productType"); final Integer financialAccountType = rs.getInt("financialAccountType"); @@ -60,6 +66,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product loanProductToGLAccountMap.put("productType", productType); loanProductToGLAccountMap.put("financialAccountType", financialAccountType); loanProductToGLAccountMap.put("paymentTypeId", paymentTypeId); + loanProductToGLAccountMap.put("chargeId", chargeId); return loanProductToGLAccountMap; } @@ -71,7 +78,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product final Map accountMappingDetails = new LinkedHashMap(8); final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null "; + final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null"; final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, new Object[] { PortfolioProductType.LOAN.getValue(), loanProductId }); @@ -138,7 +145,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product final Map accountMappingDetails = new LinkedHashMap(8); final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null "; + final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null "; final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, new Object[] { PortfolioProductType.SAVING.getValue(), savingsProductId }); @@ -168,14 +175,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product @Override public List fetchPaymentTypeToFundSourceMappingsForLoanProduct(Long loanProductId) { - List paymentTypeToGLAccountMappers = null; - return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId, paymentTypeToGLAccountMappers); + return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId); } @Override public List fetchPaymentTypeToFundSourceMappingsForSavingsProduct(Long savingsProductId) { - List paymentTypeToGLAccountMappers = null; - return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.SAVING, savingsProductId, paymentTypeToGLAccountMappers); + return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.SAVING, savingsProductId); } /** @@ -184,13 +189,14 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product * @return */ private List fetchPaymentTypeToFundSourceMappings(PortfolioProductType portfolioProductType, - Long loanProductId, List paymentTypeToGLAccountMappers) { + Long loanProductId) { final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is not null"; final List> paymentTypeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, new Object[] { portfolioProductType.getValue(), loanProductId }); + List paymentTypeToGLAccountMappers = null; for (final Map productToGLAccountMap : paymentTypeToFundSourceMappingsList) { if (paymentTypeToGLAccountMappers == null) { paymentTypeToGLAccountMappers = new ArrayList(); @@ -203,4 +209,38 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product return paymentTypeToGLAccountMappers; } + @Override + public List fetchFeeToIncomeAccountMappingsForLoanProduct(Long loanProductId) { + return fetchChargeToIncomeAccountMappings(PortfolioProductType.LOAN, loanProductId, false); + } + + @Override + public List fetchPenaltyToIncomeAccountMappingsForLoanProduct(Long loanProductId) { + return fetchChargeToIncomeAccountMappings(PortfolioProductType.LOAN, loanProductId, true); + } + + private List fetchChargeToIncomeAccountMappings(PortfolioProductType portfolioProductType, Long loanProductId, + boolean penalty) { + final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); + String sql = "select " + rm.schema() + " and product_id = ? and mapping.charge_id is not null and charge.is_penalty="; + if (penalty) { + sql = sql + " 1"; + } else { + sql = sql + " 0"; + } + + final List> chargeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, new Object[] { + portfolioProductType.getValue(), loanProductId }); + List chargeToGLAccountMappers = null; + for (final Map chargeToIncomeAccountMap : chargeToFundSourceMappingsList) { + if (chargeToGLAccountMappers == null) { + chargeToGLAccountMappers = new ArrayList(); + } + final Long glAccountId = (Long) chargeToIncomeAccountMap.get("glAccountId"); + final Long chargeId = (Long) chargeToIncomeAccountMap.get("chargeId"); + ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper(chargeId, glAccountId); + chargeToGLAccountMappers.add(chargeToGLAccountMapper); + } + return chargeToGLAccountMappers; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index 2c6743231..085c1e108 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -81,6 +81,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc // advanced accounting mappings loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); + loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); break; case ACCRUAL_BASED: @@ -119,6 +120,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc // advanced accounting mappings loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); + loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); break; } } @@ -187,12 +189,12 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc createLoanProductToGLAccountMapping(loanProductId, command); changes = loanProductToGLAccountMappingHelper.populateChangesForNewLoanProductToGLAccountMappingCreation(element, accountingRuleType); - loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, changes); }/*** else examine and update individual changes ***/ else { loanProductToGLAccountMappingHelper.handleChangesToLoanProductToGLAccountMappings(loanProductId, changes, element, accountingRuleType); loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes); + loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes); } return changes; } @@ -218,7 +220,6 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc createSavingProductToGLAccountMapping(savingsProductId, command); changes = savingsProductToGLAccountMappingHelper.populateChangesForNewSavingsProductToGLAccountMappingCreation(element, accountingRuleType); - savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingsProductId, changes); }/*** else examine and update individual changes ***/ else { savingsProductToGLAccountMappingHelper.handleChangesToSavingsProductToGLAccountMappings(savingsProductId, changes, element, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java index a5055f201..43d535d12 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java @@ -1,3 +1,8 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.accounting.producttoaccountmapping.service; import java.util.HashMap; @@ -13,6 +18,7 @@ import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAc import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; +import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -24,8 +30,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa @Autowired public SavingsProductToGLAccountMappingHelper(GLAccountRepository glAccountRepository, ProductToGLAccountMappingRepository glAccountMappingRepository, FromJsonHelper fromApiJsonHelper, - CodeValueRepositoryWrapper codeValueRepositoryWrapper) { - super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper); + CodeValueRepositoryWrapper codeValueRepositoryWrapper, ChargeRepositoryWrapper chargeRepositoryWrapper) { + super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper, chargeRepositoryWrapper); } /*** Set of abstractions for saving Saving Products to GL Account Mappings ***/ diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java index a5701ab94..8173094bf 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleApiResource.java @@ -5,9 +5,13 @@ */ package org.mifosplatform.accounting.rule.api; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.ws.rs.Consumes; @@ -18,18 +22,22 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; +import org.mifosplatform.accounting.common.AccountingConstants; import org.mifosplatform.accounting.glaccount.data.GLAccountData; import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; import org.mifosplatform.accounting.rule.data.AccountingRuleData; +import org.mifosplatform.accounting.rule.data.AccountingTagRuleData; import org.mifosplatform.accounting.rule.service.AccountingRuleReadPlatformService; import org.mifosplatform.commands.domain.CommandWrapper; import org.mifosplatform.commands.service.CommandWrapperBuilder; import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService; +import org.mifosplatform.infrastructure.codes.data.CodeValueData; +import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService; +import org.mifosplatform.infrastructure.core.api.ApiParameterHelper; import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; @@ -47,7 +55,9 @@ import org.springframework.stereotype.Component; public class AccountingRuleApiResource { private static final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "officeId", "officeName", - "accountToDebitId", "accountToCreditId", "name", "description", "systemDefined")); + "accountToDebitId", "accountToCreditId", "name", "description", "systemDefined", "allowedCreditTagOptions", + "allowedDebitTagOptions", "debitTags", "creditTags", "creditAccounts", "debitAccounts", "allowMultipleCreditEntries", + "allowMultipleDebitEntries", "tag")); private final String resourceNameForPermission = "ACCOUNTINGRULE"; @@ -58,6 +68,7 @@ public class AccountingRuleApiResource { private final ApiRequestParameterHelper apiRequestParameterHelper; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final CodeValueReadPlatformService codeValueReadPlatformService; @Autowired public AccountingRuleApiResource(final PlatformSecurityContext context, @@ -65,7 +76,7 @@ public class AccountingRuleApiResource { final DefaultToApiJsonSerializer toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper, final GLAccountReadPlatformService accountReadPlatformService, final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService, - final OfficeReadPlatformService officeReadPlatformService) { + final OfficeReadPlatformService officeReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) { this.context = context; this.apiRequestParameterHelper = apiRequestParameterHelper; this.commandsSourceWritePlatformService = commandsSourceWritePlatformService; @@ -73,30 +84,40 @@ public class AccountingRuleApiResource { this.accountingRuleReadPlatformService = accountingRuleReadPlatformService; this.officeReadPlatformService = officeReadPlatformService; this.accountReadPlatformService = accountReadPlatformService; + this.codeValueReadPlatformService = codeValueReadPlatformService; } - + @GET - @Path("template") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveTemplate(@Context final UriInfo uriInfo) { - - context.authenticatedUser().validateHasReadPermission(resourceNameForPermission); - - AccountingRuleData accountingRuleData = null; - accountingRuleData = handleTemplate(accountingRuleData); - - final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); - } + @Path("template") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveTemplate(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); + + AccountingRuleData accountingRuleData = null; + accountingRuleData = handleTemplate(accountingRuleData); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); + } @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveAllAccountingRules(@Context final UriInfo uriInfo, @QueryParam("officeId") final Long officeId) { + public String retrieveAllAccountingRules(@Context final UriInfo uriInfo) { this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); - final List accountingRuleDatas = this.accountingRuleReadPlatformService.retrieveAllAccountingRules(officeId); + final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); + final Long officeId = this.context.authenticatedUser().getOffice().getId(); + boolean isAssociationParametersExists = false; + if (!associationParameters.isEmpty()) { + if (associationParameters.contains("all")) { + isAssociationParametersExists = true; // If true, retrieve additional fields for journal entry form. + } + } + final List accountingRuleDatas = this.accountingRuleReadPlatformService.retrieveAllAccountingRules(officeId, + isAssociationParametersExists); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.apiJsonSerializerService.serialize(settings, accountingRuleDatas, RESPONSE_DATA_PARAMETERS); @@ -109,12 +130,11 @@ public class AccountingRuleApiResource { public String retreiveAccountingRule(@PathParam("accountingRuleId") final Long accountingRuleId, @Context final UriInfo uriInfo) { this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); AccountingRuleData accountingRuleData = this.accountingRuleReadPlatformService.retrieveAccountingRuleById(accountingRuleId); if (settings.isTemplate()) { - accountingRuleData = handleTemplate(accountingRuleData); + accountingRuleData = handleTemplate(accountingRuleData); } return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS); @@ -157,13 +177,60 @@ public class AccountingRuleApiResource { return this.apiJsonSerializerService.serialize(result); } - - private AccountingRuleData handleTemplate(AccountingRuleData accountingRuleData) { - List allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); - List allowedOffices = (List) this.officeReadPlatformService.retrieveAllOfficesForDropdown(); - if (accountingRuleData == null) { - return new AccountingRuleData(allowedAccounts, allowedOffices); - } - return new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices); - } + + private AccountingRuleData handleTemplate(AccountingRuleData accountingRuleData) { + final List allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts(); + final List allowedOffices = (List) this.officeReadPlatformService.retrieveAllOfficesForDropdown(); + final Collection allowedTagOptions = this.codeValueReadPlatformService + .retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME); + + allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME)); + allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME)); + allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME)); + allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME)); + + if (accountingRuleData == null) { + + final Collection allowedCreditTagOptions = allowedTagOptions; + final Collection allowedDebitTagOptions = allowedTagOptions; + + accountingRuleData = new AccountingRuleData(allowedAccounts, allowedOffices, allowedCreditTagOptions, allowedDebitTagOptions); + + } else { + + final Collection allowedCreditTagOptions; + final Collection allowedDebitTagOptions; + + if (accountingRuleData.getCreditTags() != null) { + allowedCreditTagOptions = retrieveSelectedTags(allowedTagOptions, accountingRuleData.getCreditTags()); + } else { + allowedCreditTagOptions = allowedTagOptions; + } + + if (accountingRuleData.getDebitTags() != null) { + allowedDebitTagOptions = retrieveSelectedTags(allowedTagOptions, accountingRuleData.getDebitTags()); + } else { + allowedDebitTagOptions = allowedTagOptions; + } + + accountingRuleData = new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices, allowedCreditTagOptions, + allowedDebitTagOptions); + } + return accountingRuleData; + } + + private Collection retrieveSelectedTags(final Collection allowedTagOptions, + final List existedTags) { + final Collection tempOptions = new ArrayList(allowedTagOptions); + final Map selectedTags = new HashMap(); + for (final AccountingTagRuleData accountingTagRuleData : existedTags) { + for (final CodeValueData codeValueData : tempOptions) { + if (codeValueData.getId().equals(accountingTagRuleData.getTag().getId())) { + selectedTags.put(codeValueData.getId(), codeValueData); + } + } + } + tempOptions.removeAll(selectedTags.values()); + return tempOptions; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java index e50fcde0b..795b6e2d4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/api/AccountingRuleJsonInputParams.java @@ -13,7 +13,8 @@ import java.util.Set; ***/ public enum AccountingRuleJsonInputParams { ID("id"), OFFICE_ID("officeId"), ACCOUNT_TO_DEBIT("debitAccountHead"), ACCOUNT_TO_CREDIT("creditAccountHead"), NAME("name"), DESCRIPTION( - "description"), SYSTEM_DEFINED("systemDefined"); + "description"), SYSTEM_DEFINED("systemDefined"), DEBIT_ACCOUNT_TAGS("debitTags"), CREDIT_ACCOUNT_TAGS("creditTags"), ALLOW_MULTIPLE_CREDIT_ENTRIES( + "allowMultipleCreditEntries"), ALLOW_MULTIPLE_DEBIT_ENTRIES("allowMultipleDebitEntries"); private final String value; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/command/AccountingRuleCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/command/AccountingRuleCommand.java deleted file mode 100755 index b3f3f45af..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/command/AccountingRuleCommand.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.accounting.rule.command; - -import java.util.ArrayList; -import java.util.List; - -import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; -import org.mifosplatform.infrastructure.core.data.ApiParameterError; -import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; -import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; - -/** - * Immutable command for adding an accounting closure - */ -public class AccountingRuleCommand { - - @SuppressWarnings("unused") - private final Long id; - private final Long officeId; - private final Long accountToDebitId; - private final Long accountToCreditId; - private final String name; - private final String description; - - public AccountingRuleCommand(Long id, Long officeId, Long accountToDebitId, Long accountToCreditId, String name, String description) { - this.id = id; - this.officeId = officeId; - this.accountToDebitId = accountToDebitId; - this.accountToCreditId = accountToCreditId; - this.name = name; - this.description = description; - } - - public void validateForCreate() { - - final List dataValidationErrors = new ArrayList(); - - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); - - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(this.accountToDebitId) - .notNull().integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(this.accountToCreditId) - .notNull().integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(this.officeId).ignoreIfNull() - .integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(this.name).notBlank() - .notExceedingLengthOf(100); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull() - .notExceedingLengthOf(500); - - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", - "Validation errors exist.", dataValidationErrors); } - } - - public void validateForUpdate() { - final List dataValidationErrors = new ArrayList(); - - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); - - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(this.accountToDebitId) - .ignoreIfNull().notBlank().integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(this.accountToCreditId) - .ignoreIfNull().notBlank().integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(this.officeId).ignoreIfNull() - .integerGreaterThanZero(); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(this.name).ignoreIfNull().notBlank() - .notExceedingLengthOf(100); - baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull() - .notExceedingLengthOf(500); - - if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", - "Validation errors exist.", dataValidationErrors); } - } - -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java index 248c3204d..908f7fe85 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingRuleData.java @@ -6,9 +6,12 @@ package org.mifosplatform.accounting.rule.data; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; +import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.organisation.office.data.OfficeData; /** @@ -19,85 +22,139 @@ import org.mifosplatform.organisation.office.data.OfficeData; */ public class AccountingRuleData { - private final Long id; - private final Long officeId; - private final String officeName; - private final String name; - private final String description; - private final boolean systemDefined; - private final GLAccountData debitAccountHead; - private final GLAccountData creditAccountHead; + private final Long id; + private final Long officeId; + private final String officeName; + private final String name; + private final String description; + private final boolean systemDefined; + private final boolean allowMultipleDebitEntries; + private final boolean allowMultipleCreditEntries; + private final GLAccountData debitAccountHead; + private final GLAccountData creditAccountHead; + private final List creditTags; + private final List debitTags; - // template - @SuppressWarnings("unused") - private List allowedOffices = new ArrayList(); - @SuppressWarnings("unused") - private List allowedAccounts = new ArrayList(); + // template + private List allowedOffices = new ArrayList(); + private List allowedAccounts = new ArrayList(); + private final Collection allowedCreditTagOptions; + private final Collection allowedDebitTagOptions; + private final List creditAccounts; + private final List debitAccounts; - /*public AccountingRuleData(final Long id, final Long accountToDebitId, final Long accountToCreditId) { - this(id, null, null, accountToDebitId, accountToCreditId, null, null, true, null, null, null, null, null, null); - }*/ + public AccountingRuleData(final AccountingRuleData accountingRuleData, final List allowedAccounts, + final List allowedOffices, final Collection allowedCreditTagOptions, + final Collection allowedDebitTagOptions) { + this.id = accountingRuleData.id; + this.officeId = accountingRuleData.officeId; + this.officeName = accountingRuleData.officeName; + this.name = accountingRuleData.name; + this.description = accountingRuleData.description; + this.systemDefined = accountingRuleData.systemDefined; + this.allowMultipleDebitEntries = accountingRuleData.allowMultipleDebitEntries; + this.allowMultipleCreditEntries = accountingRuleData.allowMultipleCreditEntries; + this.allowedOffices = allowedOffices; + this.allowedAccounts = allowedAccounts; + this.debitAccountHead = accountingRuleData.debitAccountHead; + this.creditAccountHead = accountingRuleData.creditAccountHead; + this.allowedCreditTagOptions = allowedCreditTagOptions; + this.allowedDebitTagOptions = allowedDebitTagOptions; + this.creditTags = accountingRuleData.creditTags; + this.debitTags = accountingRuleData.debitTags; + this.creditAccounts = accountingRuleData.creditAccounts; + this.debitAccounts = accountingRuleData.debitAccounts; + } - /*public AccountingRuleData(final Long id, final Long officeId, final String officeName, final Long accountToDebitId, - final Long accountToCreditId, final String name, final String description, final boolean systemDefined, - final String debitAccountName, final String creditAccountName, final String debitAccountGLCode, - final String creditAccountGLCode) { - this(id, officeId, officeName, accountToDebitId, accountToCreditId, name, description, systemDefined, debitAccountName, - creditAccountName, debitAccountGLCode, creditAccountGLCode, null, null); - }*/ + public AccountingRuleData(final List allowedAccounts, final List allowedOffices, + final Collection allowedCreditTagOptions, final Collection allowedDebitTagOptions) { + this.id = null; + this.officeId = null; + this.officeName = null; + this.name = null; + this.description = null; + this.systemDefined = false; + this.allowMultipleDebitEntries = false; + this.allowMultipleCreditEntries = false; + this.allowedOffices = allowedOffices; + this.allowedAccounts = allowedAccounts; + this.debitAccountHead = null; + this.creditAccountHead = null; + this.allowedCreditTagOptions = allowedCreditTagOptions; + this.allowedDebitTagOptions = allowedDebitTagOptions; + this.creditTags = null; + this.debitTags = null; + this.creditAccounts = null; + this.debitAccounts = null; + } - public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description, - final boolean systemDefined, final List allowedOffices, final List allowedAccounts) { - this.id = id; - this.officeId = officeId; - this.officeName = officeName; - this.name = name; - this.description = description; - this.systemDefined = systemDefined; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = null; - this.creditAccountHead = null; - } + public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description, + final boolean systemDefined, final boolean allowMultipleDebitEntries, final boolean allowMultipleCreditEntries, + final GLAccountData debitAccountData, final GLAccountData creditAccountData) { + this.id = id; + this.officeId = officeId; + this.officeName = officeName; + this.name = name; + this.description = description; + this.systemDefined = systemDefined; + this.allowMultipleDebitEntries = allowMultipleDebitEntries; + this.allowMultipleCreditEntries = allowMultipleCreditEntries; + this.allowedOffices = null; + this.allowedAccounts = null; + this.debitAccountHead = debitAccountData; + this.creditAccountHead = creditAccountData; + this.allowedCreditTagOptions = null; + this.allowedDebitTagOptions = null; + this.creditTags = null; + this.debitTags = null; + this.creditAccounts = new ArrayList(); + this.debitAccounts = new ArrayList(); + } - public AccountingRuleData(final AccountingRuleData accountingRuleData, - final List allowedAccounts, final List allowedOffices) { - this.id = accountingRuleData.id; - this.officeId = accountingRuleData.officeId; - this.officeName = accountingRuleData.officeName; - this.name = accountingRuleData.name; - this.description = accountingRuleData.description; - this.systemDefined = accountingRuleData.systemDefined; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = accountingRuleData.debitAccountHead; - this.creditAccountHead = accountingRuleData.creditAccountHead; - } + public AccountingRuleData(final AccountingRuleData accountingRuleData, final List creditTags, + final List creditAccounts, final List debitTags, + final List debitAccounts) { + this.id = accountingRuleData.id; + this.officeId = accountingRuleData.officeId; + this.officeName = accountingRuleData.officeName; + this.name = accountingRuleData.name; + this.description = accountingRuleData.description; + this.systemDefined = accountingRuleData.systemDefined; + this.allowMultipleDebitEntries = accountingRuleData.allowMultipleDebitEntries; + this.allowMultipleCreditEntries = accountingRuleData.allowMultipleCreditEntries; + this.allowedOffices = accountingRuleData.allowedOffices; + this.allowedAccounts = accountingRuleData.allowedAccounts; + this.debitAccountHead = accountingRuleData.debitAccountHead; + this.creditAccountHead = accountingRuleData.creditAccountHead; + this.allowedCreditTagOptions = accountingRuleData.allowedCreditTagOptions; + this.allowedDebitTagOptions = accountingRuleData.allowedDebitTagOptions; + this.creditTags = creditTags; + this.creditAccounts = creditAccounts; + this.debitTags = debitTags; + this.debitAccounts = debitAccounts; + } - public AccountingRuleData(final List allowedAccounts, final List allowedOffices) { - this.id = null; - this.officeId = null; - this.officeName = null; - this.name = null; - this.description = null; - this.systemDefined = false; - this.allowedOffices = allowedOffices; - this.allowedAccounts = allowedAccounts; - this.debitAccountHead = null; - this.creditAccountHead = null; - } + public GLAccountData getDebitAccountHead() { + return this.debitAccountHead; + } - public AccountingRuleData(Long id, Long officeId, String officeName, String name, String description, - boolean systemDefined, GLAccountData debitAccountData, GLAccountData creditAccountData) { - this.id = id; - this.officeId = officeId; - this.officeName = officeName; - this.name = name; - this.description = description; - this.systemDefined = systemDefined; - this.allowedOffices = null; - this.allowedAccounts = null; - this.debitAccountHead = debitAccountData; - this.creditAccountHead = creditAccountData; - } + public GLAccountData getCreditAccountHead() { + return this.creditAccountHead; + } + + public List getCreditTags() { + return this.creditTags; + } + + public List getDebitTags() { + return this.debitTags; + } + + public List getCreditAccounts() { + return this.creditAccounts; + } + + public List getDebitAccounts() { + return this.debitAccounts; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java new file mode 100644 index 000000000..6185f8970 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/data/AccountingTagRuleData.java @@ -0,0 +1,29 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.accounting.rule.data; + +import org.mifosplatform.infrastructure.codes.data.CodeValueData; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; + +public class AccountingTagRuleData { + + @SuppressWarnings("unused") + private final Long id; + private final CodeValueData tag; + @SuppressWarnings("unused") + private final EnumOptionData transactionType; + + public AccountingTagRuleData(final Long id, final CodeValueData tag, final EnumOptionData transactionType) { + this.id = id; + this.tag = tag; + this.transactionType = transactionType; + } + + public CodeValueData getTag() { + return tag; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java index f934efe68..0f2f6e67e 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingRule.java @@ -5,18 +5,27 @@ */ package org.mifosplatform.accounting.rule.domain; +import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.mifosplatform.accounting.glaccount.domain.GLAccount; +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.organisation.office.domain.Office; @@ -34,11 +43,11 @@ public class AccountingRule extends AbstractPersistable { private Office office; @ManyToOne - @JoinColumn(name = "debit_account_id", nullable = false) + @JoinColumn(name = "debit_account_id", nullable = true) private GLAccount accountToDebit; @ManyToOne - @JoinColumn(name = "credit_account_id", nullable = false) + @JoinColumn(name = "credit_account_id", nullable = true) private GLAccount accountToCredit; @Column(name = "description", nullable = true, length = 500) @@ -47,10 +56,21 @@ public class AccountingRule extends AbstractPersistable { @Column(name = "system_defined", nullable = false) private Boolean systemDefined; + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "accountingRule", orphanRemoval = true) + private final List accountingTagRules = new ArrayList(); + + @Column(name = "allow_multiple_credits", nullable = false) + private boolean allowMultipleCreditEntries; + + @Column(name = "allow_multiple_debits", nullable = false) + private boolean allowMultipleDebitEntries; + protected AccountingRule() {} - private AccountingRule(Office office, GLAccount accountToDebit, GLAccount accountToCredit, String name, String description, - boolean systemDefined) { + private AccountingRule(final Office office, final GLAccount accountToDebit, final GLAccount accountToCredit, final String name, + final String description, final boolean systemDefined, final boolean allowMultipleCreditEntries, + final boolean allowMultipleDebitEntries) { this.accountToDebit = accountToDebit; this.accountToCredit = accountToCredit; this.name = name; @@ -60,24 +80,34 @@ public class AccountingRule extends AbstractPersistable { this.description = this.description.trim(); } this.systemDefined = systemDefined; + this.allowMultipleCreditEntries = allowMultipleCreditEntries; + this.allowMultipleDebitEntries = allowMultipleDebitEntries; } public static AccountingRule fromJson(final Office office, final GLAccount accountToDebit, final GLAccount accountToCredit, - JsonCommand command) { + final JsonCommand command, final boolean allowMultipleCreditEntries, final boolean allowMultipleDebitEntries) { final String name = command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue()); final String description = command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue()); final boolean systemDefined = false; - return new AccountingRule(office, accountToDebit, accountToCredit, name, description, systemDefined); + return new AccountingRule(office, accountToDebit, accountToCredit, name, description, systemDefined, allowMultipleCreditEntries, + allowMultipleDebitEntries); } public Map update(final JsonCommand command) { - final Map actualChanges = new LinkedHashMap(5); - handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.OFFICE_ID.getValue(), 0L); - handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), 0L); - handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), 0L); + final Map actualChanges = new LinkedHashMap(10); + handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.OFFICE_ID.getValue(), this.office == null ? 0L + : this.office.getId()); + handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), + this.accountToDebit == null ? 0L : this.accountToDebit.getId()); + handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + this.accountToCredit == null ? 0L : this.accountToCredit.getId()); handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.NAME.getValue(), this.name); handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.DESCRIPTION.getValue(), this.description); handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.SYSTEM_DEFINED.getValue(), this.systemDefined); + handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), + this.allowMultipleCreditEntries); + handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), + this.allowMultipleDebitEntries); return actualChanges; } @@ -103,6 +133,14 @@ public class AccountingRule extends AbstractPersistable { // now update actual property if (paramName.equals(AccountingRuleJsonInputParams.SYSTEM_DEFINED.getValue())) { this.systemDefined = newValue; + } else if (paramName.equals(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())) { + if (this.accountToCredit == null) { + this.allowMultipleCreditEntries = newValue; + } + } else if (paramName.equals(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())) { + if (this.accountToDebit == null) { + this.allowMultipleDebitEntries = newValue; + } } } } @@ -123,7 +161,7 @@ public class AccountingRule extends AbstractPersistable { } } - public void setOffice(Office office) { + public void setOffice(final Office office) { this.office = office; } @@ -139,11 +177,11 @@ public class AccountingRule extends AbstractPersistable { return this.accountToCredit; } - public void setAccountToDebit(GLAccount accountToDebit) { + public void setAccountToDebit(final GLAccount accountToDebit) { this.accountToDebit = accountToDebit; } - public void setAccountToCredit(GLAccount accountToCredit) { + public void setAccountToCredit(final GLAccount accountToCredit) { this.accountToCredit = accountToCredit; } @@ -151,4 +189,66 @@ public class AccountingRule extends AbstractPersistable { return this.description; } + public List getAccountingTagRules() { + return this.accountingTagRules; + } + + public void updateAccountingRuleForTags(final List debitAccountingTagRules) { + for (final AccountingTagRule accountingTagRule : debitAccountingTagRules) { + accountingTagRule.updateAccountingTagRule(this); + this.accountingTagRules.add(accountingTagRule); + } + } + + public void updateDebitAccount(final GLAccount accountToDebit) { + this.accountToDebit = accountToDebit; + this.allowMultipleDebitEntries = false; + } + + public void updateCreditAccount(final GLAccount accountToCredit) { + this.accountToCredit = accountToCredit; + this.allowMultipleCreditEntries = false; + } + + public void updateAllowMultipleCreditEntries(final boolean allowMultipleCreditEntries) { + this.allowMultipleCreditEntries = allowMultipleCreditEntries; + } + + public void updateAllowMultipleDebitEntries(final boolean allowMultipleDebitEntries) { + this.allowMultipleDebitEntries = allowMultipleDebitEntries; + } + + public void updateTags(final JournalEntryType type) { + final Set existedCreditTags = new HashSet(); + final Set existedDebitTags = new HashSet(); + for (final AccountingTagRule accountingTagRule : this.accountingTagRules) { + if (accountingTagRule.isCreditAccount()) { + existedCreditTags.add(accountingTagRule); + } else if (accountingTagRule.isDebitAccount()) { + existedDebitTags.add(accountingTagRule); + } + } + + if (type.isCreditType()) { + this.accountingTagRules.retainAll(existedCreditTags); + } else if (type.isDebitType()) { + this.accountingTagRules.retainAll(existedDebitTags); + } + + } + + public Set getAccountingTagRulesByType(final JournalEntryType type) { + final Set existedTags = new HashSet(); + for (final AccountingTagRule accountingTagRule : this.accountingTagRules) { + if (accountingTagRule.getAccountType().equals(type.getValue())) { + existedTags.add(accountingTagRule); + } + } + return existedTags; + } + + public void removeOldTags(final List accountsToRemove) { + this.accountingTagRules.removeAll(accountsToRemove); + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java new file mode 100644 index 000000000..16b03e806 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/domain/AccountingTagRule.java @@ -0,0 +1,68 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.accounting.rule.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; +import org.mifosplatform.infrastructure.codes.domain.CodeValue; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "acc_rule_tags", uniqueConstraints = { @UniqueConstraint(columnNames = { "acc_rule_id", "tag_id", "acc_type_enum" }, name = "UNIQUE_ACCOUNT_RULE_TAGS") }) +public class AccountingTagRule extends AbstractPersistable { + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "acc_rule_id", nullable = false) + private AccountingRule accountingRule; + + @ManyToOne + @JoinColumn(name = "tag_id", nullable = false) + private CodeValue tagId; + + @Column(name = "acc_type_enum", nullable = false) + private Integer accountType; + + public static AccountingTagRule create(final CodeValue tagId, final Integer accountType) { + return new AccountingTagRule(tagId, accountType); + } + + public AccountingTagRule(final CodeValue tagId, final Integer accountType) { + this.tagId = tagId; + this.accountType = accountType; + } + + public void updateAccountingTagRule(final AccountingRule accountingRule) { + this.accountingRule = accountingRule; + } + + public AccountingTagRule() { + // TODO Auto-generated constructor stub + } + + public Integer getAccountType() { + return this.accountType; + } + + public boolean isDebitAccount() { + return JournalEntryType.fromInt(this.accountType).isDebitType(); + } + + public boolean isCreditAccount() { + return JournalEntryType.fromInt(this.accountType).isCreditType(); + } + + public Long getTagId() { + return this.tagId.getId(); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDataException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDataException.java new file mode 100644 index 000000000..f94df017b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDataException.java @@ -0,0 +1,12 @@ +package org.mifosplatform.accounting.rule.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class AccountingRuleDataException extends AbstractPlatformDomainRuleException { + + public AccountingRuleDataException(final String debitOrCreditAccount, final String debitOrCreditTags) { + super("error.msg.accounting.rule." + debitOrCreditAccount + ".or." + debitOrCreditTags + ".required", debitOrCreditAccount + " or " + + debitOrCreditTags + " required", debitOrCreditAccount, debitOrCreditTags); + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDuplicateException.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDuplicateException.java index a5023731f..90b443820 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDuplicateException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/exception/AccountingRuleDuplicateException.java @@ -16,5 +16,9 @@ public class AccountingRuleDuplicateException extends AbstractPlatformDomainRule public AccountingRuleDuplicateException(String name) { super("error.msg.accounting.rule.duplicate", "An accounting rule with the name " + name + " already exists" + name); } + + public AccountingRuleDuplicateException() { + super("error.msg.accounting.rule.tag.duplicate", "The accounting rule already have the tags which you defined"); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java index 07c2a0be1..fa32cb386 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/serialization/AccountingRuleCommandFromApiJsonDeserializer.java @@ -6,56 +6,206 @@ package org.mifosplatform.accounting.rule.serialization; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; -import org.mifosplatform.accounting.rule.command.AccountingRuleCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; -import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer; -import org.mifosplatform.infrastructure.core.serialization.FromApiJsonDeserializer; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; -import org.mifosplatform.portfolio.loanaccount.guarantor.command.GuarantorCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; -/** - * Implementation of {@link FromApiJsonDeserializer} for - * {@link GuarantorCommand}'s. - */ @Component -public final class AccountingRuleCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer { +public class AccountingRuleCommandFromApiJsonDeserializer { + + /** + * The parameters supported for this command. + */ + private final Set supportedParameters = AccountingRuleJsonInputParams.getAllValues(); private final FromJsonHelper fromApiJsonHelper; @Autowired - public AccountingRuleCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonfromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonfromApiJsonHelper; + public AccountingRuleCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; } - @Override - public AccountingRuleCommand commandFromApiJson(final String json) { + public void validateForCreate(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } final Type typeOfMap = new TypeToken>() {}.getType(); - final Set supportedParameters = AccountingRuleJsonInputParams.getAllValues(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); final JsonElement element = this.fromApiJsonHelper.parse(json); - final Long id = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ID.getValue(), element); - final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element); final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId) + .ignoreIfNull().integerGreaterThanZero(); + final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId) + .ignoreIfNull().integerGreaterThanZero(); + + final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull() + .integerGreaterThanZero(); + final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).notBlank().notExceedingLengthOf(100); + + final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull() + .notExceedingLengthOf(500); + + final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + + final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + + if (creditTags == null && accountToCreditId == null) { + final String creditTag = AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(); + final String creditAccount = AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(); + final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(creditAccount).append(".or.") + .append(creditTag).append(".required"); + final StringBuilder defaultUserMessage = new StringBuilder("The parameter ").append(creditAccount).append(" or") + .append(creditTag).append(" required"); + final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultUserMessage.toString(), + creditAccount + "," + creditTag, new Object[] { creditAccount, creditTag }); + dataValidationErrors.add(error); + } + + if (debitTags == null && accountToDebitId == null) { + final String debitTag = AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(); + final String debitAccount = AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(); + final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(debitAccount).append(".or.") + .append(debitTag).append(".required"); + final StringBuilder defaultUserMessage = new StringBuilder("The parameter ").append(debitAccount).append(" or") + .append(debitTag).append(" required"); + final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultUserMessage.toString(), + debitAccount + "," + debitTag, new Object[] { debitAccount, debitTag }); + dataValidationErrors.add(error); + } + + final String allowMultipleCredits = this.fromApiJsonHelper.extractStringNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()) + .value(allowMultipleCredits).ignoreIfNull().notBlank(); + final Boolean allowMultipleCreditEntries = this.fromApiJsonHelper.extractBooleanNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()) + .value(allowMultipleCreditEntries).ignoreIfNull(); + final String allowMultipleDebits = this.fromApiJsonHelper.extractStringNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()) + .value(allowMultipleDebits).ignoreIfNull().notBlank(); + final Boolean allowMultipleDebitEntries = this.fromApiJsonHelper.extractBooleanNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()) + .value(allowMultipleDebitEntries).ignoreIfNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + public void validateCreditOrDebitTagArray(final String[] creditOrDebitTagArray, final DataValidatorBuilder baseDataValidator, + final String parameter) { + if (creditOrDebitTagArray != null && !ObjectUtils.isEmpty(creditOrDebitTagArray)) { + for (final String tag : creditOrDebitTagArray) { + baseDataValidator.reset().parameter(parameter).value(tag).ignoreIfNull().notBlank().longGreaterThanZero(); + } + } + } + + public void validateForUpdate(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId) + .ignoreIfNull().notBlank().integerGreaterThanZero(); + + final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId) + .ignoreIfNull().notBlank().integerGreaterThanZero(); + + final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull() + .integerGreaterThanZero(); + + final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).ignoreIfNull().notBlank() + .notExceedingLengthOf(100); + final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element); - return new AccountingRuleCommand(id, officeId, accountToDebitId, accountToCreditId, name, description); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull() + .notExceedingLengthOf(500); + + final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + + final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), + element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull() + .arrayNotEmpty(); + validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + + final String allowMultipleCredits = this.fromApiJsonHelper.extractStringNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()) + .value(allowMultipleCredits).ignoreIfNull().notBlank(); + final Boolean allowMultipleCreditEntries = this.fromApiJsonHelper.extractBooleanNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()) + .value(allowMultipleCreditEntries).ignoreIfNull(); + final String allowMultipleDebits = this.fromApiJsonHelper.extractStringNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()) + .value(allowMultipleDebits).ignoreIfNull().notBlank(); + final Boolean allowMultipleDebitEntries = this.fromApiJsonHelper.extractBooleanNamed( + AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element); + baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()) + .value(allowMultipleDebitEntries).ignoreIfNull(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); } -} \ No newline at end of file +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java index d33b7fef7..d8d6ab4dd 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformService.java @@ -11,8 +11,8 @@ import org.mifosplatform.accounting.rule.data.AccountingRuleData; public interface AccountingRuleReadPlatformService { - List retrieveAllAccountingRules(Long OfficeId); + List retrieveAllAccountingRules(Long OfficeId, boolean isAssociationParametersExists); - AccountingRuleData retrieveAccountingRuleById(long accountingRuleId); + AccountingRuleData retrieveAccountingRuleById(Long accountingRuleId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java index 369d7586d..6546a4b84 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleReadPlatformServiceImpl.java @@ -7,16 +7,28 @@ package org.mifosplatform.accounting.rule.service; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup; +import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService; +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.data.AccountingRuleData; +import org.mifosplatform.accounting.rule.data.AccountingTagRuleData; import org.mifosplatform.accounting.rule.exception.AccountingRuleNotFoundException; +import org.mifosplatform.infrastructure.codes.data.CodeValueData; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @@ -24,29 +36,37 @@ import org.springframework.stereotype.Service; public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleReadPlatformService { private final JdbcTemplate jdbcTemplate; + private final GLAccountReadPlatformService glAccountReadPlatformService; @Autowired - public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { + public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource, + final GLAccountReadPlatformService glAccountReadPlatformService) { this.jdbcTemplate = new JdbcTemplate(dataSource); + this.glAccountReadPlatformService = glAccountReadPlatformService; } - private static final class AccountingRuleMapper implements RowMapper { + private static final class AccountingRuleDataExtractor implements ResultSetExtractor> { private final String schemaSql; + private final JdbcTemplate jdbcTemplate; + private final GLAccountReadPlatformService glAccountReadPlatformService; + private final boolean isAssociationParametersExists; - public AccountingRuleMapper() { + public AccountingRuleDataExtractor(final JdbcTemplate jdbcTemplate, + final GLAccountReadPlatformService glAccountReadPlatformService, final boolean isAssociationParametersExists) { + this.jdbcTemplate = jdbcTemplate; + this.glAccountReadPlatformService = glAccountReadPlatformService; + this.isAssociationParametersExists = isAssociationParametersExists; final StringBuilder sqlBuilder = new StringBuilder(400); - sqlBuilder.append("distinct rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") - .append(" rule.debit_account_id as debitAccountId,rule.credit_account_id as creditAccountId,") - .append(" rule.description as description, rule.system_defined as systemDefined, ") - .append("debitAccount.glName as debitAccountName, debitAccount.glCode as debitAccountGLCode,") - .append("creditAccount.glName as creditAccountName, creditAccount.glCode as creditAccountGLCode ") - .append("from (Select gl.id as id,gl.name as glName, gl.gl_code as glCode from acc_gl_account gl,acc_accounting_rule rule ") - .append("where gl.id=rule.debit_account_id ) as debitAccount,") - .append("(Select gl.id as id,gl.name as glName, gl.gl_code as glCode from acc_gl_account gl,acc_accounting_rule rule ") - .append("where gl.id=rule.credit_account_id ) as creditAccount,") - .append("acc_accounting_rule rule left join m_office office on rule.office_id=office.id ") - .append("where debitAccount.id=rule.debit_account_id and creditAccount.id = rule.credit_account_id"); + sqlBuilder + .append(" rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,") + .append(" rule.description as description, rule.system_defined as systemDefined, rule.allow_multiple_debits as allowMultipleDebitEntries, rule.allow_multiple_credits as allowMultipleCreditEntries, ") + .append("debitAccount.id AS debitAccountId, debitAccount.name as debitAccountName, debitAccount.gl_code as debitAccountGLCode, ") + .append("creditAccount.id AS creditAccountId, creditAccount.name as creditAccountName, creditAccount.gl_code as creditAccountGLCode") + .append(" from m_office AS office, acc_accounting_rule AS rule ") + .append(" LEFT JOIN acc_gl_account AS creditAccount ON rule.credit_account_id = creditAccount.id ") + .append(" LEFT JOIN acc_gl_account AS debitAccount ON rule.debit_account_id = debitAccount.id ") + .append("WHERE office.id=rule.office_id "); this.schemaSql = sqlBuilder.toString(); } @@ -55,52 +75,112 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead } @Override - public AccountingRuleData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + public Map extractData(final ResultSet rs) throws SQLException, DataAccessException { + final Map extractedData = new HashMap(); - final Long id = rs.getLong("id"); - final Long officeId = JdbcSupport.getLong(rs, "officeId"); - final String officeName = rs.getString("officeName"); - final String name = rs.getString("name"); - final String description = rs.getString("description"); - final Long accountToDebitId = rs.getLong("debitAccountId"); - final Long accountToCreditId = rs.getLong("creditAccountId"); - final boolean systemDefined = rs.getBoolean("systemDefined"); - final String debitAccountName = rs.getString("debitAccountName"); - final String creditAccountName = rs.getString("creditAccountName"); - final String debitAccountGLCode = rs.getString("debitAccountGLCode"); - final String creditAccountGLCode = rs.getString("creditAccountGLCode"); - - final GLAccountData debitAccountData = new GLAccountData(accountToDebitId, debitAccountName, debitAccountGLCode); - final GLAccountData creditAccountData = new GLAccountData(accountToCreditId, creditAccountName, creditAccountGLCode); + while (rs.next()) { + final Long id = rs.getLong("id"); + AccountingRuleData accountingRuleData = extractedData.get(id); + if (accountingRuleData == null) { + final Long officeId = JdbcSupport.getLong(rs, "officeId"); + final String officeName = rs.getString("officeName"); + final String name = rs.getString("name"); + final String description = rs.getString("description"); + final Long accountToDebitId = JdbcSupport.getLong(rs, "debitAccountId"); + final Long accountToCreditId = JdbcSupport.getLong(rs, "creditAccountId"); + final boolean systemDefined = rs.getBoolean("systemDefined"); + final boolean allowMultipleDebitEntries = rs.getBoolean("allowMultipleDebitEntries"); + final boolean allowMultipleCreditEntries = rs.getBoolean("allowMultipleCreditEntries"); + final String debitAccountName = rs.getString("debitAccountName"); + final String creditAccountName = rs.getString("creditAccountName"); + final String debitAccountGLCode = rs.getString("debitAccountGLCode"); + final String creditAccountGLCode = rs.getString("creditAccountGLCode"); - return new AccountingRuleData(id, officeId, officeName, name, description, systemDefined, debitAccountData, creditAccountData); + final GLAccountData debitAccountData = accountToDebitId == null ? null : new GLAccountData(accountToDebitId, + debitAccountName, debitAccountGLCode); + final GLAccountData creditAccountData = accountToCreditId == null ? null : new GLAccountData(accountToCreditId, + creditAccountName, creditAccountGLCode); + accountingRuleData = new AccountingRuleData(id, officeId, officeName, name, description, systemDefined, + allowMultipleDebitEntries, allowMultipleCreditEntries, debitAccountData, creditAccountData); + } + if (accountingRuleData.getCreditAccountHead() == null) { + final List creditTags = !this.isAssociationParametersExists ? getCreditOrDebitTags(id, + JournalEntryType.CREDIT.getValue()) : null; + final List creditAccounts = this.isAssociationParametersExists ? this.glAccountReadPlatformService + .retrieveAccountsByTagId(id, JournalEntryType.CREDIT.getValue()) : null; + accountingRuleData = new AccountingRuleData(accountingRuleData, creditTags, creditAccounts, + accountingRuleData.getDebitTags(), accountingRuleData.getDebitAccounts()); + } + if (accountingRuleData.getDebitAccountHead() == null) { + final List debitTags = !this.isAssociationParametersExists ? getCreditOrDebitTags(id, + JournalEntryType.DEBIT.getValue()) : null; + final List debitAccounts = this.isAssociationParametersExists ? this.glAccountReadPlatformService + .retrieveAccountsByTagId(id, JournalEntryType.DEBIT.getValue()) : null; + accountingRuleData = new AccountingRuleData(accountingRuleData, accountingRuleData.getCreditTags(), + accountingRuleData.getCreditAccounts(), debitTags, debitAccounts); + } + extractedData.put(id, accountingRuleData); + } + return extractedData; } + + private List getCreditOrDebitTags(final Long creditOrDebitAccount, final Integer transactionType) { + final AccountingTagRuleDataMapper mapper = new AccountingTagRuleDataMapper(); + final String taggedAccountsSchema = "Select " + mapper.taggedAccountSchema() + " where rule.id = ? and tag.acc_type_enum=?"; + return this.jdbcTemplate.query(taggedAccountsSchema, mapper, new Object[] { creditOrDebitAccount, transactionType }); + } + } @Override - public List retrieveAllAccountingRules(Long officeId) { - final AccountingRuleMapper rm = new AccountingRuleMapper(); - Object[] arguments = new Object[]{}; - String sql = "select " + rm.schema() +" and system_defined=0 "; - if(officeId != null) { - sql = sql +" and office.id = ?"; - arguments = new Object[] { officeId }; + public List retrieveAllAccountingRules(final Long officeId, final boolean isAssociationParametersExists) { + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate, + this.glAccountReadPlatformService, isAssociationParametersExists); + Object[] arguments = new Object[] {}; + String sql = "select " + resultSetExtractor.schema() + " and system_defined=0 "; + if (officeId != null) { + sql = sql + " and office.id = ?"; + arguments = new Object[] { officeId }; } - sql = sql + " order by rule.id asc"; - return this.jdbcTemplate.query(sql, rm, arguments); + sql = sql + " order by rule.id asc"; + final Map extractedData = this.jdbcTemplate.query(sql, resultSetExtractor, arguments); + return new ArrayList(extractedData.values()); } @Override - public AccountingRuleData retrieveAccountingRuleById(long accountingRuleId) { + public AccountingRuleData retrieveAccountingRuleById(final Long accountingRuleId) { try { + final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate, + this.glAccountReadPlatformService, false); + final String sql = "select " + resultSetExtractor.schema() + " and rule.id = ?"; - final AccountingRuleMapper rm = new AccountingRuleMapper(); - final String sql = "select " + rm.schema() + " and rule.id = ?"; - - final AccountingRuleData accountingRuleData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { accountingRuleId }); + final Map extractedData = this.jdbcTemplate.query(sql, resultSetExtractor, + new Object[] { accountingRuleId }); + final AccountingRuleData accountingRuleData = extractedData.get(accountingRuleId); + if (accountingRuleData == null) { throw new AccountingRuleNotFoundException(accountingRuleId); } return accountingRuleData; } catch (final EmptyResultDataAccessException e) { throw new AccountingRuleNotFoundException(accountingRuleId); } } + + private static final class AccountingTagRuleDataMapper implements RowMapper { + + public String taggedAccountSchema() { + return " tag.id as id,tag.tag_id as tagId, tag.acc_type_enum as transactionType, cv.code_value as tagName from m_code_value cv join acc_rule_tags tag on tag.tag_id=cv.id " + + "join acc_accounting_rule rule on tag.acc_rule_id=rule.id "; + } + + @Override + public AccountingTagRuleData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long tagId = rs.getLong("tagId"); + final Integer transactionType = JdbcSupport.getInteger(rs, "transactionType"); + final String tagName = rs.getString("tagName"); + final CodeValueData tag = CodeValueData.instance(tagId, tagName); + final EnumOptionData transactionTypeEnum = AccountingEnumerations.journalEntryType(transactionType); + return new AccountingTagRuleData(id, tag, transactionTypeEnum); + } + + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java index f034e06ec..c728872e4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/accounting/rule/service/AccountingRuleWritePlatformServiceJpaRepositoryImpl.java @@ -5,18 +5,30 @@ */ package org.mifosplatform.accounting.rule.service; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import org.apache.commons.lang.StringUtils; import org.mifosplatform.accounting.closure.api.GLClosureJsonInputParams; import org.mifosplatform.accounting.glaccount.domain.GLAccount; import org.mifosplatform.accounting.glaccount.domain.GLAccountRepositoryWrapper; +import org.mifosplatform.accounting.journalentry.domain.JournalEntryType; import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams; -import org.mifosplatform.accounting.rule.command.AccountingRuleCommand; import org.mifosplatform.accounting.rule.domain.AccountingRule; import org.mifosplatform.accounting.rule.domain.AccountingRuleRepository; import org.mifosplatform.accounting.rule.domain.AccountingRuleRepositoryWrapper; +import org.mifosplatform.accounting.rule.domain.AccountingTagRule; +import org.mifosplatform.accounting.rule.exception.AccountingRuleDataException; import org.mifosplatform.accounting.rule.exception.AccountingRuleDuplicateException; import org.mifosplatform.accounting.rule.serialization.AccountingRuleCommandFromApiJsonDeserializer; +import org.mifosplatform.infrastructure.codes.domain.CodeValue; +import org.mifosplatform.infrastructure.codes.domain.CodeValueRepository; +import org.mifosplatform.infrastructure.codes.exception.CodeValueNotFoundException; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; @@ -41,16 +53,19 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco private final GLAccountRepositoryWrapper accountRepositoryWrapper; private final OfficeRepository officeRepository; private final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer; + private final CodeValueRepository codeValueRepository; @Autowired public AccountingRuleWritePlatformServiceJpaRepositoryImpl(final AccountingRuleRepositoryWrapper accountingRuleRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final OfficeRepository officeRepository, - final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer, final AccountingRuleRepository ruleRepository) { + final AccountingRuleRepository ruleRepository, final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer, + final CodeValueRepository codeValueRepository) { this.accountRepositoryWrapper = accountRepositoryWrapper; this.officeRepository = officeRepository; - this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.accountingRuleRepository = ruleRepository; this.accountingRuleRepositoryWrapper = accountingRuleRepositoryWrapper; + this.fromApiJsonDeserializer = fromApiJsonDeserializer; + this.codeValueRepository = codeValueRepository; } /** @@ -59,8 +74,9 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco */ private void handleAccountingRuleIntegrityIssues(final JsonCommand command, final DataIntegrityViolationException dve) { final Throwable realCause = dve.getMostSpecificCause(); - if (realCause.getMessage().contains("accounting_rule_name_unique")) { throw new AccountingRuleDuplicateException( - command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue())); } + if (realCause.getMessage().contains("accounting_rule_name_unique")) { + throw new AccountingRuleDuplicateException(command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue())); + } else if (realCause.getMessage().contains("UNIQUE_ACCOUNT_RULE_TAGS")) { throw new AccountingRuleDuplicateException(); } logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.accounting.rule.unknown.data.integrity.issue", @@ -69,10 +85,10 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco @Transactional @Override - public CommandProcessingResult createAccountingRule(JsonCommand command) { + public CommandProcessingResult createAccountingRule(final JsonCommand command) { try { - final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json()); - accountingRuleCommand.validateForCreate(); + + this.fromApiJsonDeserializer.validateForCreate(command.json()); // check office is valid final Long officeId = command.longValueOfParameterNamed(GLClosureJsonInputParams.OFFICE_ID.getValue()); @@ -82,15 +98,8 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco if (office == null) { throw new OfficeNotFoundException(officeId); } } - // get the GL Accounts to Debit and Credit - final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); - final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); - final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); - final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); - - final AccountingRule accountingRule = AccountingRule.fromJson(office, accountToDebit, accountToCredit, command); + final AccountingRule accountingRule = assembleAccountingRuleAndTags(office, command); this.accountingRuleRepository.saveAndFlush(accountingRule); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId) .withEntityId(accountingRule.getId()).build(); } catch (final DataIntegrityViolationException dve) { @@ -99,59 +108,229 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco } } - @Transactional - @Override - public CommandProcessingResult updateAccountingRule(Long accountingRuleId, JsonCommand command) { - final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json()); - accountingRuleCommand.validateForUpdate(); + private AccountingRule assembleAccountingRuleAndTags(final Office office, final JsonCommand command) { + // get the GL Accounts or tags to Debit and Credit + final String[] debitTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + final String[] creditTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + final Set incomingDebitTags = debitTags == null ? new HashSet(): new HashSet(Arrays.asList(debitTags)); + final Set incomingCreditTags = creditTags == null ? new HashSet() : new HashSet(Arrays.asList(creditTags)); + final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); + final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); - Long officeId = null; - if (command.parameterExists(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) { - officeId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue()); + boolean allowMultipleCreditEntries = false; + boolean allowMultipleDebitEntries = false; + GLAccount debitAccount = null; + GLAccount creditAccount = null; + List accountingTagRules = new ArrayList(); + + if ((accountToDebitId != null && debitTags != null) || (accountToDebitId == null && debitTags == null)) { + throw new AccountingRuleDataException(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), + AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + } else if (accountToDebitId != null) { + debitAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); + } else if (debitTags != null) { + accountingTagRules = saveDebitOrCreditTags(incomingDebitTags, JournalEntryType.DEBIT, accountingTagRules); + allowMultipleDebitEntries = command + .booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()); } - Long accountToDebitId = null; - if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) { - accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); + if ((accountToCreditId != null && creditTags != null) || (accountToCreditId == null && creditTags == null)) { + throw new AccountingRuleDataException(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + } else if (accountToCreditId != null) { + creditAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); + } else if (creditTags != null) { + accountingTagRules = saveDebitOrCreditTags(incomingCreditTags, JournalEntryType.CREDIT, accountingTagRules); + allowMultipleCreditEntries = command + .booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()); } - Long accountToCreditId = null; - if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) { - accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); - } + final AccountingRule accountingRule = AccountingRule.fromJson(office, debitAccount, creditAccount, command, + allowMultipleCreditEntries, allowMultipleDebitEntries); + accountingRule.updateAccountingRuleForTags(accountingTagRules); - final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId); - final Map changesOnly = accountingRule.update(command); - - if (accountToDebitId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) { - final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); - accountingRule.setAccountToDebit(accountToDebit); - } - - if (accountToCreditId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) { - final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); - accountingRule.setAccountToCredit(accountToCredit); - } - - if (officeId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) { - final Office userOffice = this.officeRepository.findOne(officeId); - if (userOffice == null) { throw new OfficeNotFoundException(officeId); } - accountingRule.setOffice(userOffice); - } - - if (!changesOnly.isEmpty()) { - this.accountingRuleRepository.saveAndFlush(accountingRule); - } - - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(accountingRule.getId()) - .with(changesOnly).build(); + return accountingRule; } @Transactional @Override - public CommandProcessingResult deleteAccountingRule(Long accountingRuleId) { + public CommandProcessingResult updateAccountingRule(final Long accountingRuleId, final JsonCommand command) { + + try { + + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + Long officeId = null; + if (command.parameterExists(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) { + officeId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue()); + } + + Long accountToDebitId = null; + if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) { + accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()); + } + + Long accountToCreditId = null; + if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) { + accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()); + } + + String[] debitTags = null; + if (command.parameterExists(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue())) { + debitTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); + } + + String[] creditTags = null; + if (command.parameterExists(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue())) { + creditTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); + } + + if (accountToDebitId != null && debitTags != null) { throw new AccountingRuleDataException( + AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); } + + if (accountToCreditId != null && creditTags != null) { throw new AccountingRuleDataException( + AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), + AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); } + + boolean allowMultipleCreditEntries = false; + if (command.parameterExists(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())) { + allowMultipleCreditEntries = command + .booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue()); + } + + boolean allowMultipleDebitEntries = false; + if (command.parameterExists(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())) { + allowMultipleDebitEntries = command + .booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue()); + } + + final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId); + final Map changesOnly = accountingRule.update(command); + + if (accountToDebitId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) { + final GLAccount accountToDebit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId); + accountingRule.updateDebitAccount(accountToDebit); + accountingRule.updateTags(JournalEntryType.CREDIT); + } + + if (accountToCreditId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) { + final GLAccount accountToCredit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId); + accountingRule.updateCreditAccount(accountToCredit); + accountingRule.updateTags(JournalEntryType.DEBIT); + } + + if (creditTags != null && creditTags.length > 0 + && command.parameterExists(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue())) { + + final Set creditTagsToAdd = determineCreditTagToAddAndRemoveOldTags(creditTags, JournalEntryType.CREDIT, + accountingRule); + + if (!creditTagsToAdd.isEmpty()) { + List accountingTagRules = new ArrayList(); + accountingTagRules = saveDebitOrCreditTags(creditTagsToAdd, JournalEntryType.CREDIT, accountingTagRules); + accountingRule.updateAccountingRuleForTags(accountingTagRules); + accountingRule.updateCreditAccount(null); + if (allowMultipleCreditEntries) { + accountingRule.updateAllowMultipleCreditEntries(allowMultipleCreditEntries); + } + changesOnly.put(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), creditTagsToAdd); + } + + } + + if (debitTags != null && debitTags.length > 0 + && command.parameterExists(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue())) { + final Set debitTagsToAdd = determineCreditTagToAddAndRemoveOldTags(debitTags, JournalEntryType.DEBIT, + accountingRule); + if (!debitTagsToAdd.isEmpty()) { + List accountingTagRules = new ArrayList(); + accountingTagRules = saveDebitOrCreditTags(debitTagsToAdd, JournalEntryType.DEBIT, accountingTagRules); + accountingRule.updateAccountingRuleForTags(accountingTagRules); + accountingRule.updateDebitAccount(null); + if (allowMultipleDebitEntries) { + accountingRule.updateAllowMultipleDebitEntries(allowMultipleDebitEntries); + } + changesOnly.put(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), debitTagsToAdd); + } + } + + if (officeId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) { + final Office userOffice = this.officeRepository.findOne(officeId); + if (userOffice == null) { throw new OfficeNotFoundException(officeId); } + accountingRule.setOffice(userOffice); + } + + if (!changesOnly.isEmpty()) { + this.accountingRuleRepository.saveAndFlush(accountingRule); + } + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(accountingRule.getId()) + .with(changesOnly).build(); + } catch (final DataIntegrityViolationException dve) { + handleAccountingRuleIntegrityIssues(command, dve); + return CommandProcessingResult.empty(); + } + + } + + private Set determineCreditTagToAddAndRemoveOldTags(final String[] creditOrDebitTags, final JournalEntryType type, + final AccountingRule accountingRule) { + + final Set incomingTags = new HashSet(Arrays.asList(creditOrDebitTags)); + final Set existingTags = accountingRule.getAccountingTagRulesByType(type); + final Set existingTagIds = retrieveExistingTagIds(existingTags); + final Set tagsToAdd = new HashSet(); + final Set tagsToRemove = existingTagIds; + final Map accountsToRemove = new HashMap(); + + for (final String tagId : incomingTags) { + if (existingTagIds.contains(tagId)) { + tagsToRemove.remove(tagId); + } else { + tagsToAdd.add(tagId); + } + } + + if (!tagsToRemove.isEmpty()) { + for (final String tagId : tagsToRemove) { + for (final AccountingTagRule accountingTagRule : existingTags) { + if (tagId.equals(accountingTagRule.getTagId().toString())) { + accountsToRemove.put(accountingTagRule.getId(), accountingTagRule); + } + } + } + accountingRule.removeOldTags(new ArrayList(accountsToRemove.values())); + } + return tagsToAdd; + } + + private Set retrieveExistingTagIds(final Set existingCreditTags) { + final Set existingCreditTagIds = new HashSet(); + for (final AccountingTagRule accountingTagRule : existingCreditTags) { + existingCreditTagIds.add(accountingTagRule.getTagId().toString()); + } + return existingCreditTagIds; + } + + @Transactional + @Override + public CommandProcessingResult deleteAccountingRule(final Long accountingRuleId) { final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId); this.accountingRuleRepository.delete(accountingRule); return new CommandProcessingResultBuilder().withEntityId(accountingRule.getId()).build(); } + + private List saveDebitOrCreditTags(final Set creditOrDebitTagArray, final JournalEntryType transactionType, + final List accountingTagRules) { + for (final String creditOrDebitTag : creditOrDebitTagArray) { + if (creditOrDebitTag != null && StringUtils.isNotBlank(creditOrDebitTag)) { + final Long creditOrDebitTagIdLongValue = Long.valueOf(creditOrDebitTag); + final CodeValue creditOrDebitAccount = this.codeValueRepository.findOne(creditOrDebitTagIdLongValue); + if (creditOrDebitAccount == null) { throw new CodeValueNotFoundException(creditOrDebitTagIdLongValue); } + final AccountingTagRule accountingTagRule = AccountingTagRule.create(creditOrDebitAccount, transactionType.getValue()); + accountingTagRules.add(accountingTagRule); + } + } + return accountingTagRules; + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java index ef609f991..613188160 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/domain/CommandWrapper.java @@ -108,6 +108,21 @@ public class CommandWrapper { return this.actionName.equalsIgnoreCase("CREATE"); } + public boolean isCreateDatatable() { + return (this.actionName.equalsIgnoreCase("CREATE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + + public boolean isDeleteDatatable() { + return (this.actionName.equalsIgnoreCase("DELETE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + + public boolean isUpdateDatatable() { + return (this.actionName.equalsIgnoreCase("UPDATE") && this.href.startsWith("/datatables/") + && this.entityId == null); + } + public String getTaskPermissionName() { return this.taskPermissionName; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java index 6ff9c0957..e275acbe6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/CommandWrapperBuilder.java @@ -356,6 +356,33 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createDBDatatable(final String json) { + this.actionName = "CREATE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/"; + this.json = json; + return this; + } + + public CommandWrapperBuilder updateDBDatatable(final String datatable, final String json) { + this.actionName = "UPDATE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/" + datatable; + this.json = json; + return this; + } + + public CommandWrapperBuilder deleteDBDatatable(final String datatable, final String json) { + this.actionName = "DELETE"; + this.entityName = "DATATABLE"; + this.entityId = null; + this.href = "/datatables/" + datatable; + this.json = json; + return this; + } + public CommandWrapperBuilder createDatatable(final String datatable, final Long apptableId, final Long datatableId) { this.actionName = "CREATE"; commonDatatableSettings(datatable, apptableId, datatableId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index b792caa1f..c68ca88c8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -63,7 +63,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio final JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(), wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(), wrapper.getCodeId(), wrapper.getSupportedEntityType(), wrapper.getSupportedEntityId(), - wrapper.getTransactionId()); + wrapper.getTransactionId(), wrapper.getHref()); result = this.processAndLogCommandService.processAndLogCommand(wrapper, command, isApprovedByChecker); } catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) { @@ -86,7 +86,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json()); final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand, this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(), - commandSourceInput.subresourceId()); + commandSourceInput.subresourceId(), commandSourceInput.getResourceGetUrl()); final boolean makerCheckerApproval = true; return this.processAndLogCommandService.processAndLogCommand(wrapper, command, makerCheckerApproval); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java index a65f0c1a9..6fd1d3d66 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/commands/service/SynchronousCommandProcessingService.java @@ -78,7 +78,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer commandSourceResult.updateJsonTo(changesOnlyJson); } - if (!result.hasChanges() && wrapper.isUpdateOperation()) { + if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) { commandSourceResult.updateJsonTo(null); } @@ -108,7 +108,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer if (wrapper.isConfigurationResource()) { handler = applicationContext.getBean("updateGlobalConfigurationCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isDatatableResource()) { - if (wrapper.isCreate()) { + if (wrapper.isCreateDatatable()) { + handler = applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isDeleteDatatable()) { + handler = applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isUpdateDatatable()) { + handler = applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class); + } else if (wrapper.isCreate()) { handler = applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class); } else if (wrapper.isUpdateMultiple()) { handler = applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/codes/data/CodeValueData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/codes/data/CodeValueData.java index f9a23c779..6517087e8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/codes/data/CodeValueData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/codes/data/CodeValueData.java @@ -10,7 +10,6 @@ package org.mifosplatform.infrastructure.codes.data; */ public class CodeValueData { - @SuppressWarnings("unused") private final Long id; @SuppressWarnings("unused") @@ -32,4 +31,8 @@ public class CodeValueData { this.name = name; this.position = position; } + + public Long getId() { + return id; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java new file mode 100755 index 000000000..914fe2474 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/data/S3CredentialsData.java @@ -0,0 +1,27 @@ +package org.mifosplatform.infrastructure.configuration.data; + +public class S3CredentialsData { + + private final String bucketName; + private final String accessKey; + private final String secretKey; + + public S3CredentialsData(String bucketName, String accessKey, String secretKey) { + this.bucketName = bucketName; + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + public String getBucketName() { + return this.bucketName; + } + + public String getAccessKey() { + return this.accessKey; + } + + public String getSecretKey() { + return this.secretKey; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java index 1c63e5f35..81c4c9b17 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -8,4 +8,5 @@ package org.mifosplatform.infrastructure.configuration.domain; public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); + boolean isAmazonS3Enabled(); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 110c66b42..32107eca2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -39,4 +39,9 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return thisTask.hasMakerCheckerEnabled() && property.isEnabled(); } + + @Override + public boolean isAmazonS3Enabled(){ + return this.globalConfigurationRepository.findOneByName("amazon-S3").isEnabled(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java new file mode 100644 index 000000000..1fca5d62d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalService.java @@ -0,0 +1,37 @@ +package org.mifosplatform.infrastructure.configuration.domain; + +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "c_external_service", uniqueConstraints = { @UniqueConstraint(columnNames = ("name")) }) +public class ExternalService extends AbstractPersistable { + + @SuppressWarnings("unused") + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "value", nullable = false) + private String value; + + public ExternalService(final String name, final String value) { + this.name = name; + this.value = value; + } + + protected ExternalService() {} + + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java new file mode 100644 index 000000000..f950a9c59 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/domain/ExternalServiceRepository.java @@ -0,0 +1,9 @@ +package org.mifosplatform.infrastructure.configuration.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ExternalServiceRepository extends JpaRepository, JpaSpecificationExecutor { + + ExternalService findOneByName(String name); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java new file mode 100755 index 000000000..5d6d2127d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesConstants.java @@ -0,0 +1,9 @@ +package org.mifosplatform.infrastructure.configuration.service; + +public class ExternalServicesConstants { + + public static final String S3_BUCKET_NAME = "s3_bucket_name"; + public static final String S3_ACCESS_KEY = "s3_access_key"; + public static final String S3_SECRET_KEY = "s3_secret_key"; + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java new file mode 100755 index 000000000..91ec373d4 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformService.java @@ -0,0 +1,9 @@ +package org.mifosplatform.infrastructure.configuration.service; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; + +public interface ExternalServicesReadPlatformService { + + S3CredentialsData getS3Credentials(); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java new file mode 100644 index 000000000..83ec84d65 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java @@ -0,0 +1,51 @@ +package org.mifosplatform.infrastructure.configuration.service; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; +import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.stereotype.Service; + +@Service +public class ExternalServicesReadPlatformServiceImpl implements ExternalServicesReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public ExternalServicesReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class S3CredentialsDataExtractor implements ResultSetExtractor { + + @Override + public S3CredentialsData extractData(ResultSet rs) throws SQLException, DataAccessException { + String accessKey = null; + String bucketName = null; + String secretKey = null; + while (rs.next()) { + if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_ACCESS_KEY)) { + accessKey = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_BUCKET_NAME)) { + bucketName = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_SECRET_KEY)) { + secretKey = rs.getString("value"); + } + } + return new S3CredentialsData(bucketName, accessKey, secretKey); + } + } + + @Override + public S3CredentialsData getS3Credentials() { + final ResultSetExtractor resultSetExtractor = new S3CredentialsDataExtractor(); + final String sql = "SELECT es.name, es.value FROM c_external_service es where es.name in('s3_bucket_name','s3_access_key','s3_secret_key')"; + final S3CredentialsData s3CredentialsData = this.jdbcTemplate.query(sql, resultSetExtractor, new Object[] {}); + return s3CredentialsData; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java deleted file mode 100644 index 266e57211..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/ApiConstants.java +++ /dev/null @@ -1,15 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.infrastructure.core.api; - -public class ApiConstants { - - // TODO:Vishwas Need to move these settings to the Database - public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5; - - // TODO:Vishwas Need to move these settings to the Database - public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1; -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java index ee1248975..60e21761f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/api/JsonCommand.java @@ -45,24 +45,27 @@ public final class JsonCommand { private final String supportedEntityType; private final Long supportedEntityId; private final String transactionId; + private final String url; public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId, final String supportedEntityType, final Long supportedEntityId, - final String transactionId) { + final String transactionId, final String url) { return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId, - clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId); + clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId, url); } public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand, - final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId) { + final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, + final String url) { return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null, - null, null, null, null, null, null); + null, null, null, null, null, null, url); } public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId, - final String supportedEntityType, final Long supportedEntityId, final String transactionId) { + final String supportedEntityType, final Long supportedEntityId, final String transactionId, + final String url) { this.commandId = commandId; this.jsonCommand = jsonCommand; this.parsedCommand = parsedCommand; @@ -78,6 +81,7 @@ public final class JsonCommand { this.supportedEntityType = supportedEntityType; this.supportedEntityId = supportedEntityId; this.transactionId = transactionId; + this.url = url; } public String json() { @@ -144,6 +148,10 @@ public final class JsonCommand { public String getTransactionId() { return this.transactionId; } + + public String getUrl() { + return this.url; + } private boolean differenceExists(final LocalDate baseValue, final LocalDate workingCopyValue) { boolean differenceExists = false; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java index 0fac1f6ce..313377e10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/data/DataValidatorBuilder.java @@ -16,6 +16,8 @@ import net.fortuna.ical4j.model.property.RRule; import org.apache.commons.lang.StringUtils; import org.springframework.util.ObjectUtils; +import com.google.gson.JsonArray; + public class DataValidatorBuilder { private final List dataValidationErrors; @@ -206,12 +208,35 @@ public class DataValidatorBuilder { if (value != null) { List valuesList = Arrays.asList(values); + String valuesListStr = StringUtils.join(valuesList, ", "); if (!valuesList.contains(this.value)) { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) .append(".is.not.one.of.expected.enumerations"); StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must be one of [ ") - .append(values).append(" ] ").append("."); + .append(valuesListStr).append(" ] ").append("."); + + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), + defaultEnglishMessage.toString(), parameter, this.value, values); + + dataValidationErrors.add(error); + } + } + return this; + } + + public DataValidatorBuilder isNotOneOfTheseValues(final Object... values) { + if (value == null && ignoreNullValue) { return this; } + + if (value != null) { + List valuesList = Arrays.asList(values); + String valuesListStr = StringUtils.join(valuesList, ", "); + + if (valuesList.contains(this.value)) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".is.one.of.unwanted.enumerations"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be any of [ ") + .append(valuesListStr).append(" ] ").append("."); ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), parameter, this.value, values); @@ -309,6 +334,22 @@ public class DataValidatorBuilder { } return this; } + + public DataValidatorBuilder jsonArrayNotEmpty() { + if (value == null && ignoreNullValue) { return this; } + + JsonArray array = (JsonArray)value; + if (value != null && !array.iterator().hasNext()) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".cannot.be.empty"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append( + " cannot be empty. You must select at least one."); + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(), + parameter); + dataValidationErrors.add(error); + } + return this; + } public void expectedArrayButIsNot() { StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) @@ -385,7 +426,7 @@ public class DataValidatorBuilder { } public DataValidatorBuilder cantBeBlankWhenParameterProvidedIs(final String parameterName, final Object parameterValue) { - if (StringUtils.isNotBlank(value.toString())) { return this; } + if (value != null && StringUtils.isNotBlank(value.toString())) { return this; } StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) .append(".must.be.provided.when.").append(parameterName).append(".is.").append(parameterValue); @@ -533,4 +574,23 @@ public class DataValidatorBuilder { } return this; } + + public DataValidatorBuilder matchesRegularExpression(final String expression) { + if (value == null && ignoreNullValue) { return this; } + + if (value != null && !value.toString().matches(expression)) { + StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter) + .append(".does.not.match.regexp"); + StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter) + .append(" must match the provided regular expression [ ") + .append(expression).append(" ] ").append("."); + + ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), + defaultEnglishMessage.toString(), parameter, this.value, expression); + + dataValidationErrors.add(error); + } + + return this; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java index 846d90a82..e2ca920a0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/DefaultPlatformPasswordEncoder.java @@ -13,12 +13,13 @@ import org.springframework.security.authentication.dao.SaltSource; import org.springframework.security.authentication.encoding.PasswordEncoder; import org.springframework.stereotype.Service; +@SuppressWarnings("deprecation") @Service(value = "applicationPasswordEncoder") @Scope("singleton") public class DefaultPlatformPasswordEncoder implements PlatformPasswordEncoder { private final PasswordEncoder passwordEncoder; - private final SaltSource saltSource; + private final SaltSource saltSource; @Autowired public DefaultPlatformPasswordEncoder(final PasswordEncoder passwordEncoder, final SaltSource saltSource) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java index ca8e203a2..45d59db3e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/JdbcSupport.java @@ -47,6 +47,19 @@ public class JdbcSupport { return (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class); } + public static Integer getIntegerDefaultToNullIfZero(final ResultSet rs, final String columnName) throws SQLException { + Integer value = (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class); + return defaultToNullIfZero(value); + } + + private static Integer defaultToNullIfZero(final Integer value) { + Integer result = value; + if (result != null && Integer.valueOf(0).equals(value)) { + result = null; + } + return result; + } + public static BigDecimal getBigDecimalDefaultToZeroIfNull(final ResultSet rs, final String columnName) throws SQLException { final BigDecimal value = rs.getBigDecimal(columnName); return defaultToZeroIfNull(value); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java similarity index 96% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java index 84fef927b..80dd741ff 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/LocalDateInterval.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/domain/LocalDateInterval.java @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mifosplatform.portfolio.savings.domain; +package org.mifosplatform.infrastructure.core.domain; import org.joda.time.Days; import org.joda.time.LocalDate; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..1a214ca61 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/DatatableCommandFromApiJsonDeserializer.java @@ -0,0 +1,239 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.core.serialization; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; +import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +@Component +public class DatatableCommandFromApiJsonDeserializer { + + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static String DATATABLE_COLUMN_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,}[a-zA-Z0-9]$"; + /** + * The parameters supported for this command. + */ + private final Set supportedParametersForCreate = new HashSet(Arrays.asList( + "datatableName", "apptableName", "multiRow", "columns")); + private final Set supportedParametersForCreateColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory", "code")); + private final Set supportedParametersForUpdate = new HashSet(Arrays.asList( + "apptableName", "changeColumns", "addColumns", "dropColumns")); + private final Set supportedParametersForAddColumns = new HashSet(Arrays.asList( + "name", "type", "length", "mandatory", "after", "code")); + private final Set supportedParametersForChangeColumns = new HashSet(Arrays.asList( + "name", "newName", "mandatory", "after", "code", "newCode")); + private final Set supportedParametersForDropColumns = new HashSet(Arrays.asList( + "name")); + private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date", "Text", "Dropdown" }; + private final Object[] supportedApptableNames = { "m_loan", "m_savings_account", "m_client", "m_group", "m_office" }; + + private final FromJsonHelper fromApiJsonHelper; + + @Autowired + public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) { + this.fromApiJsonHelper = fromApiJsonHelper; + } + + private void validateType(final DataValidatorBuilder baseDataValidator, final JsonElement column) { + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notBlank() + .isOneOfTheseValues(supportedColumnTypes); + + final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column); + if (type != null && type.equals("String")) { + if (length != null) { + baseDataValidator.reset().parameter("length").value(length).notBlank() + .zeroOrPositiveAmount(); + } else { + baseDataValidator.reset().parameter("length").value(length) + .cantBeBlankWhenParameterProvidedIs("type", type); + } + } else { + baseDataValidator.reset().parameter("length").value(length) + .mustBeBlankWhenParameterProvidedIs("type", type); + } + + final String code = fromApiJsonHelper.extractStringNamed("code", column); + + if (type != null && type.equals("Dropdown")) { + baseDataValidator.reset().parameter("code").value(code).notBlank() + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + } else { + baseDataValidator.reset().parameter("code").value(code).mustBeBlankWhenParameterProvided("type", type); + } + } + + public void validateForCreate(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForCreate); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); + + final JsonElement element = fromApiJsonHelper.parse(json); + final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); + baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50) + .isOneOfTheseValues(supportedApptableNames); + final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : ""; + + final String datatableName = fromApiJsonHelper.extractStringNamed("datatableName", element); + baseDataValidator.reset().parameter("datatableName").value(datatableName).notBlank().notExceedingLengthOf(50) + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final Boolean multiRow = fromApiJsonHelper.extractBooleanNamed("multiRow", element); + baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + + final JsonArray columns = fromApiJsonHelper.extractJsonArrayNamed("columns", element); + baseDataValidator.reset().parameter("columns").value(columns).notNull().jsonArrayNotEmpty(); + + if (columns != null) { + for (JsonElement column : columns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + final String type = fromApiJsonHelper.extractStringNamed("type", column); + baseDataValidator.reset().parameter("type").value(type).notBlank().isOneOfTheseValues(supportedColumnTypes); + + validateType(baseDataValidator, column); + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateForUpdate(final String json) { + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + // Because all parameters are optional, a check to see if at least one parameter + // has been specified is necessary in order to avoid JSON requests with no parameters + if (!json.matches("(?s)\\A\\{.*?(\\\".*?\\\"\\s*?:\\s*?)+.*?\\}\\z")) { + throw new PlatformDataIntegrityException("error.msg.invalid.request.body.no.parameters", + "Provided JSON request body does not have any parameters."); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable"); + + final JsonElement element = fromApiJsonHelper.parse(json); + final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element); + baseDataValidator.reset().parameter("apptableName").value(apptableName).ignoreIfNull().notBlank() + .isOneOfTheseValues(supportedApptableNames); + final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : ""; + + final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element); + baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull().jsonArrayNotEmpty(); + + if (changeColumns != null) { + for (JsonElement column : changeColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + final String newName = fromApiJsonHelper.extractStringNamed("newName", column); + baseDataValidator.reset().parameter("newName").value(newName).ignoreIfNull().notBlank() + .notExceedingLengthOf(50).isNotOneOfTheseValues("id", fkColumnName) + .matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN); + + final String code = fromApiJsonHelper.extractStringNamed("code", column); + baseDataValidator.reset().parameter("code").value(code).ignoreIfNull().notBlank() + .notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + final String newCode = fromApiJsonHelper.extractStringNamed("newCode", column); + baseDataValidator.reset().parameter("newCode").value(newCode).ignoreIfNull().notBlank() + .notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + if (StringUtils.isBlank(code) && StringUtils.isNotBlank(newCode)) { + baseDataValidator.reset().parameter("code").value(code) + .cantBeBlankWhenParameterProvidedIs("newCode", newCode); + } + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + } + } + + final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element); + baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull().jsonArrayNotEmpty(); + + if (addColumns != null) { + for (JsonElement column : addColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + + validateType(baseDataValidator, column); + + final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column); + baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + + final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column); + baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank() + .isOneOfTheseValues(true, false); + } + } + + final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element); + baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull().jsonArrayNotEmpty(); + + if (dropColumns != null) { + for (JsonElement column : dropColumns) { + fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns); + + final String name = fromApiJsonHelper.extractStringNamed("name", column); + baseDataValidator.reset().parameter("name").value(name).notBlank() + .isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", + "Validation errors exist.", dataValidationErrors); } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java index 2e93eb625..23d2f2eba 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/FromJsonHelper.java @@ -7,11 +7,13 @@ package org.mifosplatform.infrastructure.core.serialization; import java.lang.reflect.Type; import java.math.BigDecimal; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.apache.commons.lang.StringUtils; @@ -78,6 +80,21 @@ public class FromJsonHelper { if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); } } + + public void checkForUnsupportedParameters(final JsonObject object, final Set supportedParams) { + if (object == null) { throw new InvalidParameterException(); } + + Set> entries = object.entrySet(); + List unsupportedParameterList = new ArrayList(); + + for (Entry providedParameter : entries) { + if (!supportedParams.contains(providedParameter.getKey())) { + unsupportedParameterList.add(providedParameter.getKey()); + } + } + + if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); } + } public JsonElement parse(final String json) { return parser.parse(json); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java index 39a9771d8..b1ac99140 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/api/DatatablesApiResource.java @@ -70,6 +70,46 @@ public class DatatablesApiResource { return this.toApiJsonSerializer.serializePretty(prettyPrint, result); } + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String createDatatable(final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .createDBDatatable(apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + + @PUT + @Path("{datatableName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateDatatable(@PathParam("datatableName") final String datatableName, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .updateDBDatatable(datatableName, apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + + @DELETE + @Path("{datatableName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String deleteDatatable(@PathParam("datatableName") final String datatableName, + final String apiRequestBodyAsJson) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .deleteDBDatatable(datatableName, apiRequestBodyAsJson).build(); + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); + } + @POST @Path("register/{datatable}/{apptable}") @Consumes({ MediaType.APPLICATION_JSON }) @@ -95,6 +135,18 @@ public class DatatablesApiResource { return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("{datatable}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String getDatatable(@PathParam("datatable") final String datatable, @Context final UriInfo uriInfo) { + + final DatatableData result = this.readWriteNonCoreDataService.retrieveDatatable(datatable); + + final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); + return this.toApiJsonSerializer.serializePretty(prettyPrint, result); + } + @GET @Path("{datatable}/{apptableId}") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java index cf7ca9774..99a5c59b5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/DatatableData.java @@ -5,6 +5,8 @@ */ package org.mifosplatform.infrastructure.dataqueries.data; +import java.util.List; + /** * Immutable data object representing datatable data. */ @@ -14,13 +16,18 @@ public class DatatableData { private final String applicationTableName; @SuppressWarnings("unused") private final String registeredTableName; - - public static DatatableData create(final String applicationTableName, final String registeredTableName) { - return new DatatableData(applicationTableName, registeredTableName); + @SuppressWarnings("unused") + private final List columnHeaderData; + + public static DatatableData create(final String applicationTableName, final String registeredTableName, + final List columnHeaderData) { + return new DatatableData(applicationTableName, registeredTableName, columnHeaderData); } - private DatatableData(final String applicationTableName, final String registeredTableName) { + private DatatableData(final String applicationTableName, final String registeredTableName, + final List columnHeaderData) { this.applicationTableName = applicationTableName; this.registeredTableName = registeredTableName; + this.columnHeaderData = columnHeaderData; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java index fd394c644..c13e69d3c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/data/ResultsetColumnHeaderData.java @@ -17,7 +17,6 @@ public final class ResultsetColumnHeaderData { private final String columnName; private final String columnType; - @SuppressWarnings("unused") private final Long columnLength; private final String columnDisplayType; private final boolean isColumnNullable; @@ -161,6 +160,10 @@ public final class ResultsetColumnHeaderData { public String getColumnType() { return this.columnType; } + + public Long getColumnLength() { + return this.columnLength; + } public String getColumnDisplayType() { return this.columnDisplayType; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java new file mode 100644 index 000000000..ec287d4af --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CreateDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.createDatatable(command); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java new file mode 100644 index 000000000..4b7d067fc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/DeleteDatatableCommandHandler.java @@ -0,0 +1,40 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DeleteDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public DeleteDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + + this.writePlatformService.deleteDatatable(datatableName); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java new file mode 100644 index 000000000..b96a8bfc5 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -0,0 +1,40 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.dataqueries.handler; + +import org.mifosplatform.commands.handler.NewCommandSourceHandler; +import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; +import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { + + private final ReadWriteNonCoreDataService writePlatformService; + + @Autowired + public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final String datatableName = command.getUrl().replaceAll("/datatables/", ""); + + this.writePlatformService.updateDatatable(datatableName, command); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 7d4ca2b08..cace21ef3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -29,7 +29,6 @@ import javax.ws.rs.core.StreamingOutput; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData; import org.mifosplatform.infrastructure.dataqueries.data.ReportData; @@ -38,6 +37,7 @@ import org.mifosplatform.infrastructure.dataqueries.data.ReportParameterJoinData import org.mifosplatform.infrastructure.dataqueries.data.ResultsetColumnHeaderData; import org.mifosplatform.infrastructure.dataqueries.data.ResultsetRowData; import org.mifosplatform.infrastructure.dataqueries.exception.ReportNotFoundException; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.useradministration.domain.AppUser; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; @@ -243,7 +243,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { if (noPentaho) { throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled", "Pentaho is not enabled"); } - final String reportPath = FileUtils.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; + final String reportPath = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt"; logger.info("Report path: " + reportPath); // load report definition @@ -357,7 +357,7 @@ public class ReadReportingServiceImpl implements ReadReportingService { @Override public String retrieveReportPDF(final String reportName, final String type, final Map queryParams) { - String fileLocation = FileUtils.MIFOSX_BASE_DIR + File.separator + ""; + String fileLocation = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + ""; if (!new File(fileLocation).isDirectory()) { new File(fileLocation).mkdirs(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java index b01e52f06..5f67f4768 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataService.java @@ -16,7 +16,9 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface ReadWriteNonCoreDataService { List retrieveDatatableNames(String appTable); - + + DatatableData retrieveDatatable(String datatable); + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'REGISTER_DATATABLE')") void registerDatatable(String datatable, String appTable); @@ -25,6 +27,12 @@ public interface ReadWriteNonCoreDataService { GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id); + CommandProcessingResult createDatatable(JsonCommand command); + + void updateDatatable(String datatableName, JsonCommand command); + + void deleteDatatable(String datatableName); + CommandProcessingResult createNewDatatableEntry(String datatable, Long appTableId, JsonCommand command); CommandProcessingResult updateDatatableEntryOneToOne(String datatable, Long appTableId, JsonCommand command); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 54b5b8bba..a46f1b5bf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -18,6 +18,7 @@ import javax.sql.DataSource; import org.apache.commons.lang.StringUtils; import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.SQLGrammarException; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; @@ -25,6 +26,7 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; +import org.mifosplatform.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer; import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; @@ -47,12 +49,21 @@ import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; @Service public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService { + private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$"; + private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class); + private final static HashMap apiTypeToMySQL = new HashMap() {{ + put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE"); + put("Text", "TEXT"); put ("Dropdown", "INT"); + }}; private final JdbcTemplate jdbcTemplate; private final DataSource dataSource; @@ -60,16 +71,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private final FromJsonHelper fromJsonHelper; private final JsonParserHelper helper; private final GenericDataService genericDataService; + private final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer; @Autowired public ReadWriteNonCoreDataServiceImpl(final TenantAwareRoutingDataSource dataSource, final PlatformSecurityContext context, - final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService) { + final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService, + final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer) { this.dataSource = dataSource; this.jdbcTemplate = new JdbcTemplate(this.dataSource); this.context = context; this.fromJsonHelper = fromJsonHelper; this.helper = new JsonParserHelper(); this.genericDataService = genericDataService; + this.fromApiJsonDeserializer = fromApiJsonDeserializer; } @Override @@ -96,13 +110,41 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ while (rs.next()) { final String appTableName = rs.getString("application_table_name"); final String registeredDatatableName = rs.getString("registered_table_name"); + final List columnHeaderData = this.genericDataService + .fillResultsetColumnHeaders(registeredDatatableName); - datatables.add(DatatableData.create(appTableName, registeredDatatableName)); + datatables.add(DatatableData.create(appTableName, registeredDatatableName, columnHeaderData)); } return datatables; } + @Override + public DatatableData retrieveDatatable(final String datatable) { + + // PERMITTED datatables + final String sql = "select application_table_name, registered_table_name" + " from x_registered_table " + " where exists" + + " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id" + + " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id" + + " where ur.appuser_id = " + context.authenticatedUser().getId() + " and registered_table_name='" + datatable + "'" + + " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) " + + " order by application_table_name, registered_table_name"; + + final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); + + DatatableData datatableData = null; + while (rs.next()) { + final String appTableName = rs.getString("application_table_name"); + final String registeredDatatableName = rs.getString("registered_table_name"); + final List columnHeaderData = this.genericDataService + .fillResultsetColumnHeaders(registeredDatatableName); + + datatableData = DatatableData.create(appTableName, registeredDatatableName, columnHeaderData); + } + + return datatableData; + } + private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) { logger.error(dve.getMessage(), dve); } @@ -115,7 +157,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String registerDatatableSql = "insert into x_registered_table (registered_table_name, application_table_name) values ('" + dataTableName + "', '" + applicationTableName + "')"; - + final String createPermission = "'CREATE_" + dataTableName + "'"; final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'"; final String readPermission = "'READ_" + dataTableName + "'"; @@ -160,15 +202,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ + ", 'DELETE', '" + dataTableName + "', false)"; - - try { - String[] sqlArray = {registerDatatableSql, permissionsSql}; + String[] sqlArray = { registerDatatableSql, permissionsSql }; this.jdbcTemplate.batchUpdate(sqlArray); - + } catch (DataIntegrityViolationException dve) { Throwable realCause = dve.getMostSpecificCause(); - //even if duplicate is only due to permission duplicate, okay to show duplicate datatable error msg + // even if duplicate is only due to permission duplicate, okay to + // show duplicate datatable error msg if (realCause.getMessage().contains("Duplicate entry")) { throw new PlatformDataIntegrityException( "error.msg.datatable.registered", "Datatable `" + dataTableName + "` is already registered against an application table.", "dataTableName", dataTableName); } @@ -191,9 +232,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String deletePermissionsSql = "delete from m_permission where code in " + permissionList; - final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'"; + final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'"; - String[] sqlArray = {deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql}; + String[] sqlArray = { deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql }; this.jdbcTemplate.batchUpdate(sqlArray); } @@ -213,9 +254,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams); this.jdbcTemplate.update(sql); - + return commandProcessingResult; // - + } catch (ConstraintViolationException dve) { // NOTE: jdbctemplate throws a // org.hibernate.exception.ConstraintViolationException even though @@ -240,11 +281,356 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } + private void validateDatatableName(final String name) { + + if (name == null || name.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name", + "Data table name must not be blank."); + } else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) { + throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex", + "Invalid data table name.", name); + } + } + + private String datatableColumnNameToCodeValueName(final String columnName, final String code) { + + return (code + "_cd_" + columnName); + } + + private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String code = (column.has("code")) ? column.get("code").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + } + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType); + + if (type != null) + { + if (type.equals("String")) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } else if (type.equals("Dropdown")) { + sqlBuilder = sqlBuilder.append("(11)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + + sqlBuilder = sqlBuilder.append(", "); + } + + @Transactional + @Override + public CommandProcessingResult createDatatable(final JsonCommand command) { + + String datatableName = null; + + try { + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForCreate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element); + datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element); + String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); + Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element); + + if (multiRow == null) { + multiRow = false; + } + + validateDatatableName(datatableName); + validateAppTable(apptableName); + + String fkColumnName = apptableName.substring(2) + "_id"; + String fkName = datatableName.toLowerCase().replaceAll("\\s", "_"); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` ("); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ") + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } else { + sqlBuilder = sqlBuilder + .append("`" + fkColumnName + "` BIGINT(20) NOT NULL, "); + } + + for (JsonElement column : columns) { + parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder); + } + + // Remove trailing comma and space + sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length()); + + if (multiRow) { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`id`)") + .append(", KEY `fk_" + apptableName + "` (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptableName + "` (`id`)"); + } else { + sqlBuilder = sqlBuilder + .append(", PRIMARY KEY (`" + fkColumnName + "`)") + .append(", CONSTRAINT `fk_" + fkName + "` ") + .append("FOREIGN KEY (`" + fkColumnName + "`) ") + .append("REFERENCES `" + apptableName + "` (`id`)"); + } + + sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); + this.jdbcTemplate.execute(sqlBuilder.toString()); + + registerDatatable(datatableName, apptableName); + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); + + if (realCause.getMessage().contains("Table") && realCause.getMessage().contains("already exists")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.already.exists", realCause.getMessage()); + } + } + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withResourceIdAsString(datatableName) + .build(); + } + + private void parseDatatableColumnForUpdate(final JsonObject column, + final Map mapColumnNameDefinition, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String newName = (column.has("newName")) ? column.get("newName").getAsString() : name; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + String code = (column.has("code")) ? column.get("code").getAsString() : null; + String newCode = (column.has("newCode")) ? column.get("newCode").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + + if (StringUtils.isNotBlank(newCode)) { + newName = datatableColumnNameToCodeValueName(newName, newCode); + } else { + newName = datatableColumnNameToCodeValueName(newName, code); + } + } + + if (!mapColumnNameDefinition.containsKey(name)) { + throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse", + "Column " + name + " does not exist.", name); + } + + String type = mapColumnNameDefinition.get(name).getColumnType(); + Long length = mapColumnNameDefinition.get(name).getColumnLength(); + + sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + type); + if (length != null && length > 0) { + if (type.equals("DECIMAL")) { + sqlBuilder.append("(19,6)"); + } else if (type.equals("VARCHAR")) { + sqlBuilder.append("(" + length + ")"); + } + } + + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } + } + + private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + String type = (column.has("type")) ? column.get("type").getAsString() : null; + Integer length = (column.has("length")) ? column.get("length").getAsInt() : null; + Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false; + String after = (column.has("after")) ? column.get("after").getAsString() : null; + String code = (column.has("code")) ? column.get("code").getAsString() : null; + + if (StringUtils.isNotBlank(code)) { + name = datatableColumnNameToCodeValueName(name, code); + } + + String mysqlType = apiTypeToMySQL.get(type); + sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType); + + if (type != null) { + if (type.equals("String") && length != null) { + sqlBuilder = sqlBuilder.append("(" + length + ")"); + } else if (type.equals("Decimal")) { + sqlBuilder = sqlBuilder.append("(19,6)"); + } else if (type.equals("Dropdown")) { + sqlBuilder = sqlBuilder.append("(11)"); + } + } + if (mandatory != null) { + if (mandatory) { + sqlBuilder = sqlBuilder.append(" NOT NULL"); + } else { + sqlBuilder = sqlBuilder.append(" DEFAULT NULL"); + } + } + if (after != null) { + sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`"); + } + } + + private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) { + + String name = (column.has("name")) ? column.get("name").getAsString() : null; + + sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`"); + } + + @Transactional + @Override + public void updateDatatable(final String datatableName, final JsonCommand command) { + + try { + context.authenticatedUser(); + this.fromApiJsonDeserializer.validateForUpdate(command.json()); + + JsonElement element = this.fromJsonHelper.parse(command.json()); + JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element); + JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element); + JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element); + String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element); + + validateDatatableName(datatableName); + + List columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName); + Map mapColumnNameDefinition = new HashMap(); + for (ResultsetColumnHeaderData columnHeader : columnHeaderData) { + mapColumnNameDefinition.put(columnHeader.getColumnName(), columnHeader); + } + + if (!StringUtils.isBlank(apptableName)) { + validateAppTable(apptableName); + + String oldApptableName = this.queryForApplicationTableName(datatableName); + if (!StringUtils.equals(oldApptableName, apptableName)) { + deregisterDatatable(datatableName); + registerDatatable(datatableName, apptableName); + } + } + + if (changeColumns == null && addColumns == null && dropColumns == null) { + return; + } + + if (dropColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : dropColumns) { + parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); + } + if (addColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : addColumns) { + parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); + } + if (changeColumns != null) { + + StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`"); + + for (JsonElement column : changeColumns) { + parseDatatableColumnForUpdate(column.getAsJsonObject(), mapColumnNameDefinition, sqlBuilder); + } + + // Remove the first comma, right after ALTER TABLE `datatable` + int indexOfFirstComma = sqlBuilder.indexOf(","); + if (indexOfFirstComma != -1) { + sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); + } + + this.jdbcTemplate.execute(sqlBuilder.toString()); + } + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); + + if (realCause.getMessage().toLowerCase().contains("unknown column")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.column.missing.update", realCause.getMessage()); + } else if (realCause.getMessage().toLowerCase().contains("can't drop")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.column.missing.drop", realCause.getMessage()); + } else if (realCause.getMessage().toLowerCase().contains("duplicate column")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.duplicate.column", realCause.getMessage()); + } + } + } + + @Transactional + @Override + public void deleteDatatable(final String datatableName) { + + try { + context.authenticatedUser(); + + validateDatatableName(datatableName); + deregisterDatatable(datatableName); + + String sql = "DROP TABLE `" + datatableName + "`"; + this.jdbcTemplate.execute(sql); + } catch (SQLGrammarException e) { + Throwable realCause = e.getCause(); + + if (realCause.getMessage().contains("Unknown table")) { + throw new PlatformDataIntegrityException( + "error.msg.datatable.missing", realCause.getMessage()); + } + } + } + @Transactional @Override public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, null, command); + return updateDatatableEntry(dataTableName, appTableId, null, command); } @Transactional @@ -252,34 +638,37 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - return updateDatatableEntry(dataTableName, appTableId, datatableId, command); + return updateDatatableEntry(dataTableName, appTableId, datatableId, command); } - private CommandProcessingResult updateDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId, final JsonCommand command) { - + final String appTable = queryForApplicationTableName(dataTableName); CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - + final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(appTable, dataTableName, appTableId, datatableId); - + if (grs.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); } - + if (grs.hasMoreThanOneEntry()) { throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update", "Application table: " + dataTableName + " Foreign key id: " + appTableId); } - + final Type typeOfMap = new TypeToken>() {}.getType(); Map dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json()); - String pkName = "id"; //1:M datatable - if (datatableId == null) { pkName = getFKField(appTable);} //1:1 datatable - + String pkName = "id"; // 1:M datatable + if (datatableId == null) { + pkName = getFKField(appTable); + } // 1:1 datatable + final Map changes = getAffectedAndChangedColumns(grs, dataParams, pkName); if (!changes.isEmpty()) { - Long pkValue = appTableId; - if (datatableId != null) { pkValue = datatableId;} + Long pkValue = appTableId; + if (datatableId != null) { + pkValue = datatableId; + } final String sql = getUpdateSql(dataTableName, pkName, pkValue, changes); logger.info("Update sql: " + sql); if (StringUtils.isNotBlank(sql)) { @@ -287,20 +676,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ changes.put("locale", dataParams.get("locale")); changes.put("dateFormat", "yyyy-MM-dd"); } else { - logger.info("No Changes"); - } + logger.info("No Changes"); + } } return new CommandProcessingResultBuilder() // - .withOfficeId(commandProcessingResult.getOfficeId()) // - .withGroupId(commandProcessingResult.getGroupId()) // - .withClientId(commandProcessingResult.getClientId()) // - .withSavingsId(commandProcessingResult.getSavingsId()) // - .withLoanId(commandProcessingResult.getLoanId()) // - .with(changes) // - .build(); + .withOfficeId(commandProcessingResult.getOfficeId()) // + .withGroupId(commandProcessingResult.getGroupId()) // + .withClientId(commandProcessingResult.getClientId()) // + .withSavingsId(commandProcessingResult.getSavingsId()) // + .withLoanId(commandProcessingResult.getLoanId()) // + .with(changes) // + .build(); } - @Transactional @Override @@ -308,13 +696,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final String appTable = queryForApplicationTableName(dataTableName); CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId); - final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(appTable), appTableId); int rowsDeleted = this.jdbcTemplate.update(deleteOneToOneEntrySql); if (rowsDeleted < 1) { throw new DatatableNotFoundException(dataTableName, appTableId); } - + return commandProcessingResult; } @@ -358,10 +745,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return new GenericResultsetData(columnHeaders, result); } - - private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, String dataTableName, final Long appTableId, - final Long id) { + private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, final String dataTableName, + final Long appTableId, final Long id) { final List columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName); @@ -383,9 +769,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private CommandProcessingResult checkMainResourceExistsWithinScope(final String appTable, final Long appTableId) { final String sql = dataScopedSQL(appTable, appTableId); -logger.info("data scoped sql: " + sql); + logger.info("data scoped sql: " + sql); final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql); - + if (!rs.next()) { throw new DatatableNotFoundException(appTable, appTableId); } Long officeId = getLongSqlRowSet(rs, "officeId"); @@ -393,90 +779,86 @@ logger.info("data scoped sql: " + sql); Long clientId = getLongSqlRowSet(rs, "clientId"); Long savingsId = getLongSqlRowSet(rs, "savingsId"); Long LoanId = getLongSqlRowSet(rs, "loanId"); - + if (rs.next()) { throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); } - + return new CommandProcessingResultBuilder() // - .withOfficeId(officeId) // - .withGroupId(groupId) // - .withClientId(clientId) // - .withSavingsId(savingsId) // - .withLoanId(LoanId) // - .build(); + .withOfficeId(officeId) // + .withGroupId(groupId) // + .withClientId(clientId) // + .withSavingsId(savingsId) // + .withLoanId(LoanId) // + .build(); } - - private Long getLongSqlRowSet(SqlRowSet rs, String column) { + + private Long getLongSqlRowSet(final SqlRowSet rs, final String column) { Long val = rs.getLong(column); if (val == 0) val = null; - return val; + return val; } - private String dataScopedSQL(final String appTable, final Long appTableId) { /* * unfortunately have to, one way or another, be able to restrict data - * to the users office hierarchy. Here, a few key tables are done. - * But if additional fields are needed on other tables the - * same pattern applies + * to the users office hierarchy. Here, a few key tables are done. But + * if additional fields are needed on other tables the same pattern + * applies */ AppUser currentUser = context.authenticatedUser(); String scopedSQL = null; /* - * m_loan and m_savings_account are connected to an m_office thru either an m_client or an m_group - * If both it means it relates to an m_client that is in a group (still an m_client account) + * m_loan and m_savings_account are connected to an m_office thru either + * an m_client or an m_group If both it means it relates to an m_client + * that is in a group (still an m_client account) */ if (appTable.equalsIgnoreCase("m_loan")) { - scopedSQL = "select distinctrow x.* from (" + - " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + - " join m_client c on c.id = l.client_id " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where l.id = " + appTableId + ")" + - " union all " + - " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + - " join m_group g on g.id = l.group_id " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where l.id = " + appTableId + ")" + - " ) x"; + scopedSQL = "select distinctrow x.* from (" + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + + " join m_client c on c.id = l.client_id " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + + "%'" + + " where l.id = " + + appTableId + + ")" + + " union all " + + " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " + + " join m_group g on g.id = l.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + "%'" + " where l.id = " + appTableId + ")" + " ) x"; } if (appTable.equalsIgnoreCase("m_savings_account")) { - scopedSQL = "select distinctrow x.* from (" + - " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + - " join m_client c on c.id = s.client_id " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where s.id = " + appTableId+ ")" + - " union all " + - " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + - " join m_group g on g.id = s.group_id " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where s.id = " + appTableId+ ")" + - " ) x"; + scopedSQL = "select distinctrow x.* from (" + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + + " join m_client c on c.id = s.client_id " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + + "%'" + + " where s.id = " + + appTableId + + ")" + + " union all " + + " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " + + " join m_group g on g.id = s.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '" + + currentUser.getOffice().getHierarchy() + "%'" + " where s.id = " + appTableId + ")" + " ) x"; } if (appTable.equalsIgnoreCase("m_client")) { - scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c " + - " join m_office o on o.id = c.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where c.id = " + appTableId; + scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c " + + " join m_office o on o.id = c.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + + " where c.id = " + appTableId; + } + if (appTable.equalsIgnoreCase("m_group")) { + scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g " + + " join m_office o on o.id = g.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + + " where g.id = " + appTableId; } - if (appTable.equalsIgnoreCase("m_group")) { - scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g " + - " join m_office o on o.id = g.office_id and o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " where g.id = " + appTableId; - } if (appTable.equalsIgnoreCase("m_office")) { - scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o " + - " where o.hierarchy like '" + - currentUser.getOffice().getHierarchy() + "%'" + - " and o.id = " + appTableId; - } + scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o " + + " where o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + " and o.id = " + appTableId; + } - if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", - "Application Table: " + appTable + " not catered for in data Scoping"); } + if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", "Application Table: " + + appTable + " not catered for in data Scoping"); } return scopedSQL; @@ -621,7 +1003,7 @@ logger.info("data scoped sql: " + sql); affectedAndChangedColumns.put(key, columnValue); } } - + return affectedAndChangedColumns; } @@ -635,9 +1017,7 @@ logger.info("data scoped sql: " + sql); if (key.equals(grs.getColumnHeaders().get(i).getColumnName())) { columnValue = columnValues.get(i); - if (notTheSame(columnValue, keyValue, colType)) { - return true; - } + if (notTheSame(columnValue, keyValue, colType)) { return true; } return false; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java index b12ae5054..dbf103a2b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/DocumentManagementApiResource.java @@ -5,36 +5,16 @@ */ package org.mifosplatform.infrastructure.documentmanagement.api; -import java.io.File; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import javax.ws.rs.core.UriInfo; - -import org.mifosplatform.infrastructure.core.api.ApiConstants; +import com.sun.jersey.core.header.FormDataContentDisposition; +import com.sun.jersey.multipart.FormDataBodyPart; +import com.sun.jersey.multipart.FormDataParam; import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer; -import org.mifosplatform.infrastructure.core.service.FileUtils; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mifosplatform.infrastructure.documentmanagement.service.DocumentReadPlatformService; import org.mifosplatform.infrastructure.documentmanagement.service.DocumentWritePlatformService; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; @@ -42,9 +22,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import com.sun.jersey.core.header.FormDataContentDisposition; -import com.sun.jersey.multipart.FormDataBodyPart; -import com.sun.jersey.multipart.FormDataParam; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriInfo; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; @Path("{entityType}/{entityId}/documents") @Component @@ -95,8 +83,6 @@ public class DocumentManagementApiResource { @FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name, @FormDataParam("description") final String description) { - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); - /** * TODO: also need to have a backup and stop reading from stream after * max size is reached to protect against malicious clients @@ -124,8 +110,6 @@ public class DocumentManagementApiResource { @FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name, @FormDataParam("description") final String description) { - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); - final Set modifiedParams = new HashSet(); modifiedParams.add("name"); modifiedParams.add("description"); @@ -166,9 +150,6 @@ public class DocumentManagementApiResource { final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId); - // we do not want to send document location as a part of the response - documentData.setLocation(null); - final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, documentData, this.RESPONSE_DATA_PARAMETERS); } @@ -182,11 +163,10 @@ public class DocumentManagementApiResource { this.context.authenticatedUser().validateHasReadPermission(this.SystemEntityType); - final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId); - final File file = new File(documentData.fileLocation()); - final ResponseBuilder response = Response.ok(file); - response.header("Content-Disposition", "attachment; filename=\"" + documentData.fileName() + "\""); - response.header("Content-Type", documentData.contentType()); + final FileData fileData = this.documentReadPlatformService.retrieveFileData(entityType, entityId, documentId); + final ResponseBuilder response = Response.ok(fileData.file()); + response.header("Content-Disposition", "attachment; filename=\"" + fileData.name() + "\""); + response.header("Content-Type", fileData.contentType()); return response.build(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java similarity index 60% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java index dccf49f26..dbd576106 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/api/ClientImagesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/api/ImagesApiResource.java @@ -3,9 +3,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mifosplatform.portfolio.client.api; +package org.mifosplatform.infrastructure.documentmanagement.api; -import java.io.File; import java.io.InputStream; import javax.ws.rs.Consumes; @@ -22,19 +21,16 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import org.apache.commons.lang.StringUtils; -import org.mifosplatform.infrastructure.core.api.ApiConstants; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.mifosplatform.infrastructure.core.service.FileUtils; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_DATA_URI_SUFFIX; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_FILE_EXTENSION; -import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_MIME_TYPE; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.IMAGE_FILE_EXTENSION; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.service.ImageReadPlatformService; +import org.mifosplatform.infrastructure.documentmanagement.service.ImageWritePlatformService; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.portfolio.client.data.ClientData; -import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; -import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; -import org.mifosplatform.portfolio.client.service.ClientWritePlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -44,22 +40,26 @@ import com.sun.jersey.core.header.FormDataContentDisposition; import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataParam; -@Path("/clients/{clientId}/images") +/** + * TODO Vishwas need to change the url to entity/entityId/images to make the + * image management API's generic for other entities like staff etc + **/ +@Path("{clients}/{clientId}/images") @Component @Scope("singleton") -public class ClientImagesApiResource { +public class ImagesApiResource { private final PlatformSecurityContext context; - private final ClientReadPlatformService clientReadPlatformService; - private final ClientWritePlatformService clientWritePlatformService; + private final ImageReadPlatformService imageReadPlatformService; + private final ImageWritePlatformService imageWritePlatformService; private final DefaultToApiJsonSerializer toApiJsonSerializer; @Autowired - public ClientImagesApiResource(final PlatformSecurityContext context, final ClientReadPlatformService readPlatformService, - final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { + public ImagesApiResource(final PlatformSecurityContext context, final ImageReadPlatformService readPlatformService, + final ImageWritePlatformService imageWritePlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer) { this.context = context; - this.clientReadPlatformService = readPlatformService; - this.clientWritePlatformService = clientWritePlatformService; + this.imageReadPlatformService = readPlatformService; + this.imageWritePlatformService = imageWritePlatformService; this.toApiJsonSerializer = toApiJsonSerializer; } @@ -76,12 +76,11 @@ public class ClientImagesApiResource { // TODO: vishwas might need more advances validation (like reading magic // number) for handling malicious clients // and clients not setting mime type - FileUtils.validateClientImageNotEmpty(fileDetails.getFileName()); - FileUtils.validateImageMimeType(bodyPart.getMediaType().toString()); - FileUtils.validateFileSizeWithinPermissibleRange(fileSize, fileDetails.getFileName(), ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB); + ContentRepositoryUtils.validateClientImageNotEmpty(fileDetails.getFileName()); + ContentRepositoryUtils.validateImageMimeType(bodyPart.getMediaType().toString()); - final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), - inputStream); + final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(), + inputStream, fileSize); return this.toApiJsonSerializer.serialize(result); } @@ -94,9 +93,9 @@ public class ClientImagesApiResource { @Produces({ MediaType.APPLICATION_JSON }) public String addNewClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) { - final Base64EncodedImage base64EncodedImage = FileUtils.extractImageFromDataURL(jsonRequestBody); + final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody); - final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); + final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage); return this.toApiJsonSerializer.serialize(result); } @@ -111,20 +110,17 @@ public class ClientImagesApiResource { context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); - final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId); - - if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } + final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId); // TODO: Need a better way of determining image type - String imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); - if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.GIF.getValue())) { - imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.GIF.getValue(); - } else if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.PNG.getValue())) { - imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.PNG.getValue(); + String imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.JPEG.getValue(); + if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.GIF.getValue(); + } else if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.PNG.getValue(); } - String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeFromFile(clientData.imageKey()); - + String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeBytes(imageData.getContent()); return clientImageAsBase64Text; } @@ -134,26 +130,15 @@ public class ClientImagesApiResource { public Response downloadClientImage(@PathParam("clientId") final Long clientId) { context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE"); - final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId); + final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId); - if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); } - - File image = new File(clientData.imageKey()); - String imageName = image.getName(); - ResponseBuilder response = Response.ok(image); - response.header("Content-Disposition", "attachment; filename=\"" + imageName + "\""); + ResponseBuilder response = Response.ok(imageData.getContent()); + response.header("Content-Disposition", "attachment; filename=\"" + imageData.getEntityDisplayName() + IMAGE_FILE_EXTENSION.JPEG + + "\""); // TODO: Need a better way of determining image type - // determine image type - String contentType = IMAGE_MIME_TYPE.JPEG.getValue(); - if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.GIF.getValue())) { - contentType = IMAGE_MIME_TYPE.GIF.getValue(); - } else if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.PNG.getValue())) { - contentType = IMAGE_MIME_TYPE.PNG.getValue(); - } - - response.header("Content-Type", contentType); + response.header("Content-Type", imageData.contentType()); return response.build(); } @@ -187,7 +172,7 @@ public class ClientImagesApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String deleteClientImage(@PathParam("clientId") final Long clientId) { - this.clientWritePlatformService.deleteClientImage(clientId); + this.imageWritePlatformService.deleteClientImage(clientId); return this.toApiJsonSerializer.serialize(new CommandProcessingResult(clientId)); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java index d138ad51f..1706f7f8e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/command/DocumentCommand.java @@ -22,6 +22,7 @@ public class DocumentCommand { private Long size; private String type; private String location; + private Integer storageType; private final Set modifiedParameters; @@ -95,6 +96,14 @@ public class DocumentCommand { this.location = location; } + public Integer getStorageType() { + return this.storageType; + } + + public void setStorageType(Integer storageType) { + this.storageType = storageType; + } + public boolean isNameChanged() { return this.modifiedParameters.contains("name"); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java new file mode 100644 index 000000000..96c10cc65 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepository.java @@ -0,0 +1,38 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; + +public interface ContentRepository { + + public StorageType type = null; + + // TODO:Vishwas Need to move these settings to the Database + public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5; + + // TODO:Vishwas Need to move these settings to the Database + public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1; + + public abstract String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand); + + public abstract void deleteFile(String fileName, String documentPath); + + public abstract FileData fetchFile(DocumentData documentData); + + public abstract String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize); + + public abstract String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName); + + public abstract void deleteImage(final Long resourceId, final String location); + + public abstract ImageData fetchImage(ImageData imageData); + + public abstract StorageType getStorageType(); + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java new file mode 100644 index 000000000..837ad7dfc --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java @@ -0,0 +1,41 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData; +import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService; +import org.mifosplatform.infrastructure.configuration.service.ExternalServicesReadPlatformService; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +public class ContentRepositoryFactory { + + private ApplicationContext applicationContext; + private ExternalServicesReadPlatformService externalServicesReadPlatformService; + + @Autowired + public ContentRepositoryFactory(final ApplicationContext applicationContext, + final ExternalServicesReadPlatformService externalServicesReadPlatformService) { + this.applicationContext = applicationContext; + this.externalServicesReadPlatformService = externalServicesReadPlatformService; + } + + public ContentRepository getRepository() { + ConfigurationDomainService configurationDomainServiceJpa = applicationContext.getBean("configurationDomainServiceJpa", + ConfigurationDomainService.class); + if (configurationDomainServiceJpa.isAmazonS3Enabled()) { return createS3DocumentStore(); } + return new FileSystemContentRepository(); + } + + public ContentRepository getRepository(StorageType documentStoreType) { + if (documentStoreType == StorageType.FILE_SYSTEM) { return new FileSystemContentRepository(); } + return createS3DocumentStore(); + } + + private ContentRepository createS3DocumentStore() { + S3CredentialsData s3CredentialsData = externalServicesReadPlatformService.getS3Credentials(); + return new S3ContentRepository(s3CredentialsData.getBucketName(), s3CredentialsData.getSecretKey(), + s3CredentialsData.getAccessKey()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java similarity index 58% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java index 5f464a226..730bf9986 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/service/FileUtils.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/ContentRepositoryUtils.java @@ -1,15 +1,5 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.infrastructure.core.service; +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -20,17 +10,11 @@ import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException; import org.mifosplatform.infrastructure.core.exception.ImageUploadException; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; -import com.lowagie.text.pdf.codec.Base64; +public class ContentRepositoryUtils { -public class FileUtils { - - private final static Logger logger = LoggerFactory.getLogger(FileUtils.class); - - public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; + public static Random random = new Random(); public static enum IMAGE_MIME_TYPE { GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png"); @@ -44,7 +28,7 @@ public class FileUtils { public String getValue() { return this.value; } - }; + } public static enum IMAGE_FILE_EXTENSION { GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png"); @@ -58,7 +42,7 @@ public class FileUtils { public String getValue() { return this.value; } - }; + } public static enum IMAGE_DATA_URI_SUFFIX { GIF("data:" + IMAGE_MIME_TYPE.GIF.getValue() + ";base64,"), JPEG("data:" + IMAGE_MIME_TYPE.JPEG.getValue() + ";base64,"), PNG( @@ -73,107 +57,6 @@ public class FileUtils { public String getValue() { return this.value; } - }; - - public static Random random = new Random(); - - /** - * Generate a random String - * - * @return - */ - public static String generateRandomString() { - String characters = "abcdefghijklmnopqrstuvwxyz123456789"; - int length = generateRandomNumber(); - char[] text = new char[length]; - for (int i = 0; i < length; i++) { - text[i] = characters.charAt(random.nextInt(characters.length())); - } - return new String(text); - } - - /** - * Generate a random number between 5 to 16 - * - * @return - */ - public static int generateRandomNumber() { - Random randomGenerator = new Random(); - return randomGenerator.nextInt(11) + 5; - } - - /** - * Generate the directory path for storing the new document - * - * @param entityType - * @param entityId - * @return - */ - public static String generateFileParentDirectory(String entityType, Long entityId) { - return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "documents" + File.separator + entityType + File.separator + entityId + File.separator - + FileUtils.generateRandomString(); - } - - /** - * Generate directory path for storing new Image - */ - public static String generateClientImageParentDirectory(final Long resourceId) { - return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() - + File.separator + "images" + File.separator + "clients" + File.separator + resourceId; - } - - /** - * @param uploadedInputStream - * @param uploadedFileLocation - * @return - * @throws IOException - */ - public static String saveToFileSystem(InputStream uploadedInputStream, String uploadedFileLocation, String fileName) throws IOException { - String fileLocation = uploadedFileLocation + File.separator + fileName; - OutputStream out = new FileOutputStream(new File(fileLocation)); - int read = 0; - byte[] bytes = new byte[1024]; - - while ((read = uploadedInputStream.read(bytes)) != -1) { - out.write(bytes, 0, read); - } - out.flush(); - out.close(); - return fileLocation; - } - - /** - * @param base64EncodedImage - * @param uploadedFileLocation - * @param fileName - * @return - * @throws IOException - */ - public static String saveToFileSystem(Base64EncodedImage base64EncodedImage, String uploadedFileLocation, String fileName) - throws IOException { - String fileLocation = uploadedFileLocation + File.separator + fileName + base64EncodedImage.getFileExtension(); - OutputStream out = new FileOutputStream(new File(fileLocation)); - byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString()); - out.write(imgBytes); - out.flush(); - out.close(); - return fileLocation; - } - - /** - * @param fileSize - * @param name - */ - public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name, int maxFileSize) { - /** - * Using Content-Length gives me size of the entire request, which is - * good enough for now for a fast fail as the length of the rest of the - * content i.e name and description while compared to the uploaded file - * size is negligible - **/ - if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) { throw new DocumentManagementException(name, fileSize, - maxFileSize); } } /** @@ -210,6 +93,17 @@ public class FileUtils { return new Base64EncodedImage(base64EncodedString, fileExtension); } + public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name) { + /** + * Using Content-Length gives me size of the entire request, which is + * good enough for now for a fast fail as the length of the rest of the + * content i.e name and description while compared to the uploaded file + * size is negligible + **/ + if (fileSize != null && ((fileSize / (1024 * 1024)) > ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB)) { throw new ContentManagementException( + name, fileSize, ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB); } + } + public static void validateClientImageNotEmpty(String imageFileName) { List dataValidationErrors = new ArrayList(); if (imageFileName == null) { @@ -223,12 +117,28 @@ public class FileUtils { } } - public static void deleteClientImage(final Long resourceId, final String location) { - File fileToBeDeleted = new File(location); - boolean fileDeleted = fileToBeDeleted.delete(); - if (!fileDeleted) { - // no need to throw an Error, simply log a warning - logger.warn("Unable to delete image associated with clients with Id " + resourceId); + /** + * Generate a random String + * + * @return + */ + public static String generateRandomString() { + String characters = "abcdefghijklmnopqrstuvwxyz123456789"; + int length = generateRandomNumber(); + char[] text = new char[length]; + for (int i = 0; i < length; i++) { + text[i] = characters.charAt(random.nextInt(characters.length())); } + return new String(text); + } + + /** + * Generate a random number between 5 to 16 + * + * @return + */ + public static int generateRandomNumber() { + Random randomGenerator = new Random(); + return randomGenerator.nextInt(11) + 5; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java new file mode 100644 index 000000000..0f065d925 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java @@ -0,0 +1,164 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.lowagie.text.pdf.codec.Base64; + +public class FileSystemContentRepository implements ContentRepository { + + private final static Logger logger = LoggerFactory.getLogger(FileSystemContentRepository.class); + + public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx"; + + @Override + public String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand) { + String fileName = documentCommand.getFileName(); + String uploadDocumentLocation = generateFileParentDirectory(documentCommand.getParentEntityType(), + documentCommand.getParentEntityId()); + + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName); + makeDirectories(uploadDocumentLocation); + + String fileLocation = uploadDocumentLocation + File.separator + fileName; + + writeFileToFileSystem(fileName, uploadedInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName); + makeDirectories(uploadImageLocation); + + String fileLocation = uploadImageLocation + File.separator + imageName; + + writeFileToFileSystem(imageName, uploadedInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + + makeDirectories(uploadImageLocation); + + String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); + try { + OutputStream out = new FileOutputStream(new File(fileLocation)); + byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString()); + out.write(imgBytes); + out.flush(); + out.close(); + } catch (IOException ioe) { + throw new ContentManagementException(imageName, ioe.getMessage()); + } + return fileLocation; + } + + @Override + public void deleteImage(final Long resourceId, final String location) { + final boolean fileDeleted = deleteFile(location); + if (!fileDeleted) { + // no need to throw an Error, simply log a warning + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } + } + + @Override + public void deleteFile(String fileName, String documentPath) { + final boolean fileDeleted = deleteFile(documentPath); + if (!fileDeleted) { throw new ContentManagementException(fileName, null); } + } + + private boolean deleteFile(String documentPath) { + final File fileToBeDeleted = new File(documentPath); + return fileToBeDeleted.delete(); + } + + @Override + public StorageType getStorageType() { + return StorageType.FILE_SYSTEM; + } + + @Override + public FileData fetchFile(DocumentData documentData) { + File file = new File(documentData.fileLocation()); + return new FileData(file, documentData.fileName(), documentData.contentType()); + } + + @Override + public ImageData fetchImage(ImageData imageData) { + File file = new File(imageData.location()); + imageData.updateContent(file); + return imageData; + } + + /** + * Generate the directory path for storing the new document + * + * @param entityType + * @param entityId + * @return + */ + private String generateFileParentDirectory(String entityType, Long entityId) { + return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "documents" + File.separator + + entityType + File.separator + entityId + File.separator + ContentRepositoryUtils.generateRandomString(); + } + + /** + * Generate directory path for storing new Image + */ + private String generateClientImageParentDirectory(final Long resourceId) { + return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "images" + File.separator + + "clients" + File.separator + resourceId; + } + + /** + * Recursively create the directory if it does not exist * + */ + private void makeDirectories(String uploadDocumentLocation) { + if (!new File(uploadDocumentLocation).isDirectory()) { + new File(uploadDocumentLocation).mkdirs(); + } + } + + private void writeFileToFileSystem(String fileName, InputStream uploadedInputStream, String fileLocation) { + try { + OutputStream out = new FileOutputStream(new File(fileLocation)); + int read = 0; + byte[] bytes = new byte[1024]; + + while ((read = uploadedInputStream.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + out.flush(); + out.close(); + } catch (IOException ioException) { + throw new ContentManagementException(fileName, ioException.getMessage()); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java new file mode 100644 index 000000000..fb80d9784 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/contentrepository/S3ContentRepository.java @@ -0,0 +1,157 @@ +package org.mifosplatform.infrastructure.documentmanagement.contentrepository; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; +import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.DeleteObjectRequest; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import com.lowagie.text.pdf.codec.Base64; + +public class S3ContentRepository implements ContentRepository { + + private final static Logger logger = LoggerFactory.getLogger(S3ContentRepository.class); + + private final String s3BucketName; + private AmazonS3 s3Client; + + public S3ContentRepository(String bucketName, String secretKey, String accessKey) { + this.s3BucketName = bucketName; + this.s3Client = new AmazonS3Client(new BasicAWSCredentials(accessKey, secretKey)); + } + + @Override + public String saveFile(InputStream toUpload, DocumentCommand documentCommand) { + String fileName = documentCommand.getFileName(); + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName); + + String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId()); + String uploadDocFullPath = uploadDocFolder + File.separator + fileName; + + uploadDocument(fileName, toUpload, uploadDocFullPath); + return uploadDocFullPath; + } + + @Override + public void deleteFile(String documentName, String documentPath) { + try { + deleteObjectFromS3(documentPath); + } catch (AmazonClientException ace) { + throw new ContentManagementException(documentName, ace.getMessage()); + } + } + + @Override + public String saveImage(InputStream toUploadInputStream, Long resourceId, String imageName, Long fileSize) { + ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName); + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName; + + uploadDocument(imageName, toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) { + String uploadImageLocation = generateClientImageParentDirectory(resourceId); + String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension(); + InputStream toUploadInputStream = new ByteArrayInputStream(Base64.decode(base64EncodedImage.getBase64EncodedString())); + + uploadDocument(imageName, toUploadInputStream, fileLocation); + return fileLocation; + } + + @Override + public void deleteImage(Long resourceId, String location) { + try { + deleteObjectFromS3(location); + } catch (AmazonServiceException ase) { + deleteObjectAmazonServiceExceptionMessage(ase); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } catch (AmazonClientException ace) { + deleteObjectAmazonClientExceptionMessage(ace); + logger.warn("Unable to delete image associated with clients with Id " + resourceId); + } + } + + @Override + public StorageType getStorageType() { + return StorageType.S3; + } + + @Override + public FileData fetchFile(DocumentData documentData) throws DocumentNotFoundException { + FileData fileData = null; + String fileName = documentData.fileName(); + try { + logger.info("Downloading an object"); + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, documentData.fileLocation())); + fileData = new FileData(s3object.getObjectContent(), fileName, documentData.contentType()); + } catch (AmazonClientException ace) { + logger.error(ace.getMessage()); + throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId()); + } + return fileData; + } + + @Override + public ImageData fetchImage(ImageData imageData) { + S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, imageData.location())); + imageData.updateContent(s3object.getObjectContent()); + return imageData; + } + + private void deleteObjectAmazonClientExceptionMessage(AmazonClientException ace) { + String message = "Caught an AmazonClientException." + "Error Message: " + ace.getMessage(); + logger.error(message); + } + + private void deleteObjectAmazonServiceExceptionMessage(AmazonServiceException ase) { + String message = "Caught an AmazonServiceException." + "Error Message: " + ase.getMessage() + "HTTP Status Code: " + + ase.getStatusCode() + "AWS Error Code: " + ase.getErrorCode() + "Error Type: " + ase.getErrorType() + + "Request ID: " + ase.getRequestId(); + logger.error(message); + } + + private String generateFileParentDirectory(String entityType, Long entityId) { + return "documents" + File.separator + entityType + File.separator + entityId + File.separator + + ContentRepositoryUtils.generateRandomString(); + } + + private String generateClientImageParentDirectory(Long resourceId) { + return "images" + File.separator + "clients" + File.separator + resourceId; + } + + private void deleteObjectFromS3(String location) { + s3Client.deleteObject(new DeleteObjectRequest(s3BucketName, location)); + } + + private void uploadDocument(String filename, InputStream inputStream, String s3UploadLocation) throws ContentManagementException { + try { + logger.info("Uploading a new object to S3 from a file to " + s3UploadLocation); + s3Client.putObject(new PutObjectRequest(this.s3BucketName, s3UploadLocation, inputStream, new ObjectMetadata())); + } catch (AmazonClientException ace) { + String message = ace.getMessage(); + throw new ContentManagementException(filename, message); + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java index 25cdcb931..25d155473 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/DocumentData.java @@ -5,16 +5,15 @@ */ package org.mifosplatform.infrastructure.documentmanagement.data; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; + /** * Immutable data object represent document being managed on platform. */ public class DocumentData { - @SuppressWarnings("unused") private final Long id; - @SuppressWarnings("unused") private final String parentEntityType; - @SuppressWarnings("unused") private final Long parentEntityId; @SuppressWarnings("unused") private final String name; @@ -24,10 +23,11 @@ public class DocumentData { private final String type; @SuppressWarnings("unused") private final String description; - private String location; + private final String location; + private final Integer storageType; public DocumentData(final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location) { + final Long size, final String type, final String description, final String location, Integer storageType) { this.id = id; this.parentEntityType = parentEntityType; this.parentEntityId = parentEntityId; @@ -37,6 +37,7 @@ public class DocumentData { this.type = type; this.description = description; this.location = location; + this.storageType = storageType; } public String contentType() { @@ -51,8 +52,20 @@ public class DocumentData { return this.location; } - public void setLocation(final String location) { - this.location = location; + public StorageType storageType() { + return StorageType.fromInt(storageType); + } + + public String getParentEntityType() { + return this.parentEntityType; + } + + public Long getParentEntityId() { + return this.parentEntityId; + } + + public Long getId() { + return this.id; } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java new file mode 100644 index 000000000..6c7761154 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/FileData.java @@ -0,0 +1,51 @@ +package org.mifosplatform.infrastructure.documentmanagement.data; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class FileData { + + private static final Logger logger = LoggerFactory.getLogger(FileData.class); + + private final File file; + private final String fileName; + private final String contentType; + private final InputStream inputStream; + + public FileData(File file, String fileName, String contentType) { + this.file = file; + this.fileName = fileName; + this.contentType = contentType; + this.inputStream = null; + } + + public FileData(InputStream inputStream, String fileName, String contentType) { + this.file = null; + this.inputStream = inputStream; + this.fileName = fileName; + this.contentType = contentType; + } + + public String contentType() { + return contentType; + } + + public String name() { + return fileName; + } + + public InputStream file() { + try { + if (inputStream == null) { return new FileInputStream(file); } + return inputStream; + } catch (FileNotFoundException e) { + logger.error(e.toString()); + return null; + } + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java new file mode 100644 index 000000000..7a25e895d --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/data/ImageData.java @@ -0,0 +1,90 @@ +package org.mifosplatform.infrastructure.documentmanagement.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.lang.StringUtils; +import org.apache.poi.util.IOUtils; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; + +public class ImageData { + + @SuppressWarnings("unused") + private final Long imageId; + private final String location; + private final Integer storageType; + private final String entityDisplayName; + + private File file; + private String contentType; + private InputStream inputStream; + + public ImageData(Long imageId, String location, Integer storageType, String entityDisplayName) { + this.imageId = imageId; + this.location = location; + this.storageType = storageType; + this.entityDisplayName = entityDisplayName; + } + + public byte[] getContent() { + // TODO Vishwas Fix error handling + try { + if (inputStream == null) { + FileInputStream fileInputStream = new FileInputStream(file); + return IOUtils.toByteArray(fileInputStream); + } + + return IOUtils.toByteArray(inputStream); + } catch (IOException e) { + return null; + } + } + + private String setImageContentType() { + String contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.JPEG.getValue(); + + if (file != null) { + String fileName = file.getName(); + + if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) { + contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.GIF.getValue(); + } else if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) { + contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.PNG.getValue(); + } + } + return contentType; + } + + public String contentType() { + return this.contentType; + } + + public StorageType storageType() { + return StorageType.fromInt(storageType); + } + + public String name() { + return this.file.getName(); + } + + public String location() { + return this.location; + } + + public void updateContent(InputStream objectContent) { + this.inputStream = objectContent; + } + + public void updateContent(File file) { + this.file = file; + setImageContentType(); + } + + public String getEntityDisplayName() { + return this.entityDisplayName; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java index 467503668..5ae3e0a73 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Document.java @@ -41,15 +41,18 @@ public class Document extends AbstractPersistable { @Column(name = "location", length = 500) private String location; + @Column(name = "storage_type_enum") + private Integer storageType; + public Document() {} public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, - final Long size, final String type, final String description, final String location) { - return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location); + final Long size, final String type, final String description, final String location, final StorageType storageType) { + return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType); } private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size, - final String type, final String description, final String location) { + final String type, final String description, final String location, final StorageType storageType) { this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null); this.parentEntityId = parentEntityId; this.name = StringUtils.defaultIfEmpty(name, null); @@ -58,6 +61,7 @@ public class Document extends AbstractPersistable { this.type = StringUtils.defaultIfEmpty(type, null); this.description = StringUtils.defaultIfEmpty(description, null); this.location = StringUtils.defaultIfEmpty(location, null); + this.storageType = storageType.getValue(); } public void update(final DocumentCommand command) { @@ -79,7 +83,6 @@ public class Document extends AbstractPersistable { if (command.isSizeChanged()) { this.size = command.getSize(); } - } public String getParentEntityType() { @@ -146,4 +149,7 @@ public class Document extends AbstractPersistable { this.location = location; } + public StorageType storageType() { + return StorageType.fromInt(this.storageType); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java new file mode 100644 index 000000000..c4b21c373 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/Image.java @@ -0,0 +1,49 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_image") +public final class Image extends AbstractPersistable { + + @Column(name = "location", length = 500) + private String location; + + @Column(name = "storage_type_enum") + private Integer storageType; + + public Image(String location, StorageType storageType) { + this.location = location; + this.storageType = storageType.getValue(); + } + + protected Image() { + + } + + public String getLocation() { + return this.location; + } + + public Integer getStorageType() { + return this.storageType; + } + + public void setLocation(String location) { + this.location = location; + } + + public void setStorageType(Integer storageType) { + this.storageType = storageType; + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java new file mode 100644 index 000000000..946cf2eae --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/ImageRepository.java @@ -0,0 +1,12 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.domain; + +import org.mifosplatform.portfolio.client.domain.Client; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ImageRepository extends JpaRepository, JpaSpecificationExecutor {} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java new file mode 100644 index 000000000..2cd78d59f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/domain/StorageType.java @@ -0,0 +1,30 @@ +package org.mifosplatform.infrastructure.documentmanagement.domain; + +import java.util.HashMap; +import java.util.Map; + +public enum StorageType { + FILE_SYSTEM(1), S3(2); + + private Integer value; + + StorageType(Integer value) { + this.value = value; + } + + public Integer getValue() { + return value; + } + + private static final Map intToEnumMap = new HashMap(); + static { + for (final StorageType type : StorageType.values()) { + intToEnumMap.put(type.value, type); + } + } + + public static StorageType fromInt(final int i) { + final StorageType type = intToEnumMap.get(Integer.valueOf(i)); + return type; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java similarity index 59% rename from mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java rename to mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java index 4c1f864e1..a45d00e64 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/DocumentManagementException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/exception/ContentManagementException.java @@ -7,13 +7,14 @@ package org.mifosplatform.infrastructure.documentmanagement.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; -public class DocumentManagementException extends AbstractPlatformDomainRuleException { +public class ContentManagementException extends AbstractPlatformDomainRuleException { - public DocumentManagementException(final String name) { - super("error.msg.document.save", "Error while manipulating file " + name + " due to a File system / Amazon S3 issue", name); + public ContentManagementException(final String filename, final String message) { + super("error.msg.document.save", + "Error while manipulating file " + filename + " due to a File system / Amazon S3 issue " + message, filename, message); } - public DocumentManagementException(final String name, final Long fileSize, final int maxFileSize) { + public ContentManagementException(final String name, final Long fileSize, final int maxFileSize) { super("error.msg.document.file.too.big", "Unable to save the document with name" + name + " since its file Size of " + fileSize / (1024 * 1024) + " MB exceeds the max permissable file size of " + maxFileSize + " MB", name, fileSize); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java index f6b3f6692..20a803cc2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformService.java @@ -5,13 +5,17 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.util.Collection; - import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; + +import java.util.Collection; public interface DocumentReadPlatformService { Collection retrieveAllDocuments(String entityType, Long entityId); + FileData retrieveFileData(String entityType, Long entityId, Long documentId); + DocumentData retrieveDocument(String entityType, Long entityId, Long documentId); + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java index 15fd7dcf9..dfedcce55 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentReadPlatformServiceImpl.java @@ -5,13 +5,12 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collection; - import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData; +import org.mifosplatform.infrastructure.documentmanagement.data.FileData; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; @@ -20,16 +19,23 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + @Service public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformService { private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; + private final ContentRepositoryFactory contentRepositoryFactory; @Autowired - public DocumentReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource) { + public DocumentReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, + final ContentRepositoryFactory documentStoreFactory) { this.context = context; this.jdbcTemplate = new JdbcTemplate(dataSource); + this.contentRepositoryFactory = documentStoreFactory; } @Override @@ -40,40 +46,60 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ // TODO verify if the entities are valid and a user // has data // scope for the particular entities - final DocumentMapper mapper = new DocumentMapper(true); + final DocumentMapper mapper = new DocumentMapper(true, true); final String sql = "select " + mapper.schema() + " order by d.id"; return this.jdbcTemplate.query(sql, mapper, new Object[] { entityType, entityId }); } @Override - public DocumentData retrieveDocument(final String entityType, final Long entityId, final Long documentId) { - + public FileData retrieveFileData(final String entityType, final Long entityId, final Long documentId) { try { - this.context.authenticatedUser(); - - // TODO verify if the entities are valid and a - // user has data - // scope for the particular entities - final DocumentMapper mapper = new DocumentMapper(false); - final String sql = "select " + mapper.schema() + " and d.id=? "; - return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); + final DocumentMapper mapper = new DocumentMapper(false, false); + DocumentData documentData = fetchDocumentDetails(entityType, entityId, documentId, mapper); + ContentRepository contentRepository = contentRepositoryFactory.getRepository(documentData.storageType()); + return contentRepository.fetchFile(documentData); } catch (final EmptyResultDataAccessException e) { throw new DocumentNotFoundException(entityType, entityId, documentId); } } + @Override + public DocumentData retrieveDocument(String entityType, Long entityId, Long documentId) { + try { + final DocumentMapper mapper = new DocumentMapper(true, true); + return fetchDocumentDetails(entityType, entityId, documentId, mapper); + } catch (final EmptyResultDataAccessException e) { + throw new DocumentNotFoundException(entityType, entityId, documentId); + } + } + + /** + * @param entityType + * @param entityId + * @param documentId + * @param mapper + * @return + */ + private DocumentData fetchDocumentDetails(final String entityType, final Long entityId, final Long documentId, + final DocumentMapper mapper) { + final String sql = "select " + mapper.schema() + " and d.id=? "; + return this.jdbcTemplate.queryForObject(sql, mapper, new Object[] { entityType, entityId, documentId }); + } + private static final class DocumentMapper implements RowMapper { private final boolean hideLocation; + private final boolean hideStorageType; - public DocumentMapper(final boolean hideLocation) { + public DocumentMapper(final boolean hideLocation, final boolean hideStorageType) { this.hideLocation = hideLocation; + this.hideStorageType = hideStorageType; } public String schema() { return "d.id as id, d.parent_entity_type as parentEntityType, d.parent_entity_id as parentEntityId, d.name as name, " + " d.file_name as fileName, d.size as fileSize, d.type as fileType, " - + " d.description as description, d.location as location" + + " d.description as description, d.location as location," + " d.storage_type_enum as storageType" + " from m_document d where d.parent_entity_type=? and d.parent_entity_id=?"; } @@ -89,11 +115,16 @@ public class DocumentReadPlatformServiceImpl implements DocumentReadPlatformServ final String fileType = rs.getString("fileType"); final String description = rs.getString("description"); String location = null; + Integer storageType = null; if (!this.hideLocation) { location = rs.getString("location"); } - - return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location); + if (!this.hideStorageType) { + storageType = rs.getInt("storageType"); + } + return new DocumentData(id, parentEntityType, parentEntityId, name, fileName, fileSize, fileType, description, location, + storageType); } } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java index 84a1ca41e..e7734dc9e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java @@ -5,18 +5,18 @@ */ package org.mifosplatform.infrastructure.documentmanagement.service; -import java.io.File; -import java.io.IOException; import java.io.InputStream; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand; import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommandValidator; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; import org.mifosplatform.infrastructure.documentmanagement.domain.Document; import org.mifosplatform.infrastructure.documentmanagement.domain.DocumentRepository; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException; import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException; import org.mifosplatform.infrastructure.documentmanagement.exception.InvalidEntityTypeForDocumentManagementException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; @@ -34,11 +34,14 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr private final PlatformSecurityContext context; private final DocumentRepository documentRepository; + private final ContentRepositoryFactory contentRepositoryFactory; @Autowired - public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DocumentRepository documentRepository) { + public DocumentWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, + final DocumentRepository documentRepository, final ContentRepositoryFactory documentStoreFactory) { this.context = context; this.documentRepository = documentRepository; + this.contentRepositoryFactory = documentStoreFactory; } @Transactional @@ -53,19 +56,13 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr validator.validateForCreate(); - final String fileUploadLocation = FileUtils.generateFileParentDirectory(documentCommand.getParentEntityType(), - documentCommand.getParentEntityId()); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); - /** Recursively create the directory if it does not exist **/ - if (!new File(fileUploadLocation).isDirectory()) { - new File(fileUploadLocation).mkdirs(); - } - - final String fileLocation = FileUtils.saveToFileSystem(inputStream, fileUploadLocation, documentCommand.getFileName()); + final String fileLocation = contentRepository.saveFile(inputStream, documentCommand); final Document document = Document.createNew(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getName(), documentCommand.getFileName(), documentCommand.getSize(), documentCommand.getType(), - documentCommand.getDescription(), fileLocation); + documentCommand.getDescription(), fileLocation, contentRepository.getStorageType()); this.documentRepository.save(document); @@ -74,9 +71,6 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final IOException ioe) { - logger.error(ioe.getMessage(), ioe); - throw new DocumentManagementException(documentCommand.getName()); } } @@ -94,28 +88,20 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr final Document documentForUpdate = this.documentRepository.findOne(documentCommand.getId()); if (documentForUpdate == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } + + StorageType documentStoreType = documentForUpdate.storageType(); oldLocation = documentForUpdate.getLocation(); - // if a new file is also passed in if (inputStream != null && documentCommand.isFileNameChanged()) { - final String fileUploadLocation = FileUtils.generateFileParentDirectory(documentCommand.getParentEntityType(), - documentCommand.getParentEntityId()); - - /** Recursively create the directory if it does not exist **/ - if (!new File(fileUploadLocation).isDirectory()) { - new File(fileUploadLocation).mkdirs(); - } - - // TODO provide switch to toggle between file system appender - // and Amazon S3 appender - final String fileLocation = FileUtils.saveToFileSystem(inputStream, fileUploadLocation, documentCommand.getFileName()); - documentCommand.setLocation(fileLocation); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); + documentCommand.setLocation(contentRepository.saveFile(inputStream, documentCommand)); + documentCommand.setStorageType(contentRepository.getStorageType().getValue()); } documentForUpdate.update(documentCommand); if (inputStream != null && documentCommand.isFileNameChanged()) { - // delete previous file - deleteFile(documentCommand.getName(), oldLocation); + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(documentStoreType); + contentRepository.deleteFile(documentCommand.getName(), oldLocation); } this.documentRepository.saveAndFlush(documentForUpdate); @@ -125,9 +111,9 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr logger.error(dve.getMessage(), dve); throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); - } catch (final IOException ioe) { - logger.error(ioe.getMessage(), ioe); - throw new DocumentManagementException(documentCommand.getName()); + } catch (final ContentManagementException cme) { + logger.error(cme.getMessage(), cme); + throw new ContentManagementException(documentCommand.getName(), cme.getMessage()); } } @@ -141,16 +127,11 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr final Document document = this.documentRepository.findOne(documentCommand.getId()); if (document == null) { throw new DocumentNotFoundException(documentCommand.getParentEntityType(), documentCommand.getParentEntityId(), documentCommand.getId()); } - this.documentRepository.delete(document); - deleteFile(document.getName(), document.getLocation()); - return new CommandProcessingResult(document.getId()); - } - private void deleteFile(final String documentName, final String location) { - final File fileToBeDeleted = new File(location); - final boolean fileDeleted = fileToBeDeleted.delete(); - if (!fileDeleted) { throw new DocumentManagementException(documentName); } + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(document.storageType()); + contentRepository.deleteFile(document.getName(), document.getLocation()); + return new CommandProcessingResult(document.getId()); } private void validateParentEntityType(final DocumentCommand documentCommand) { @@ -159,7 +140,6 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr } private static boolean checkValidEntityType(final String entityType) { - for (final DOCUMENT_MANAGEMENT_ENTITY entities : DOCUMENT_MANAGEMENT_ENTITY.values()) { if (entities.name().equalsIgnoreCase(entityType)) { return true; } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java new file mode 100755 index 000000000..eca7a90c6 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformService.java @@ -0,0 +1,14 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.service; + +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; + +public interface ImageReadPlatformService { + + ImageData retrieveClientImage(Long clientId); + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java new file mode 100755 index 000000000..c4e560658 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageReadPlatformServiceImpl.java @@ -0,0 +1,79 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.service; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.mifosplatform.infrastructure.core.domain.JdbcSupport; +import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; +import org.mifosplatform.infrastructure.documentmanagement.data.ImageData; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; +import org.mifosplatform.portfolio.client.exception.ImageNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class ImageReadPlatformServiceImpl implements ImageReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final ContentRepositoryFactory contentRepositoryFactory; + private final ClientRepositoryWrapper clientRepositoryWrapper; + + @Autowired + public ImageReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource, final ContentRepositoryFactory documentStoreFactory, + final ClientRepositoryWrapper clientRepositoryWrapper) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.contentRepositoryFactory = documentStoreFactory; + this.clientRepositoryWrapper = clientRepositoryWrapper; + } + + private static final class ImageMapper implements RowMapper { + + private final String entityDisplayName; + + public ImageMapper(final String entityDisplayName) { + this.entityDisplayName = entityDisplayName; + } + + public String schema() { + return " image.id as id, image.location as location, image.storage_type_enum as storageType " + + " from m_image image , m_client client " + " where client.image_id = image.id and client.id=?"; + } + + @Override + public ImageData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final String location = rs.getString("location"); + final Integer storageType = JdbcSupport.getInteger(rs, "storageType"); + return new ImageData(id, location, storageType, entityDisplayName); + } + } + + @Override + public ImageData retrieveClientImage(Long clientId) { + try { + Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + + final ImageMapper imageMapper = new ImageMapper(client.getDisplayName()); + + final String sql = "select " + imageMapper.schema(); + + ImageData imageData = this.jdbcTemplate.queryForObject(sql, imageMapper, new Object[] { clientId }); + ContentRepository contentRepository = contentRepositoryFactory.getRepository(imageData.storageType()); + return contentRepository.fetchImage(imageData); + } catch (EmptyResultDataAccessException e) { + throw new ImageNotFoundException("clients", clientId); + } + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java new file mode 100755 index 000000000..950fbe126 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformService.java @@ -0,0 +1,24 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.service; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.springframework.security.access.prepost.PreAuthorize; + +public interface ImageWritePlatformService { + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") + CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream, Long fileSize); + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") + CommandProcessingResult saveOrUpdateClientImage(Long clientId, Base64EncodedImage encodedImage); + + @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE')") + CommandProcessingResult deleteClientImage(Long clientId); +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java new file mode 100755 index 000000000..cb5a1b3ce --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java @@ -0,0 +1,103 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.infrastructure.documentmanagement.service; + +import java.io.InputStream; + +import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; +import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepository; +import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryFactory; +import org.mifosplatform.infrastructure.documentmanagement.domain.Image; +import org.mifosplatform.infrastructure.documentmanagement.domain.ImageRepository; +import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType; +import org.mifosplatform.portfolio.client.domain.Client; +import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ImageWritePlatformServiceJpaRepositoryImpl implements ImageWritePlatformService { + + private final ContentRepositoryFactory contentRepositoryFactory; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final ImageRepository imageRepository; + + @Autowired + public ImageWritePlatformServiceJpaRepositoryImpl(final ContentRepositoryFactory documentStoreFactory, + final ClientRepositoryWrapper clientRepositoryWrapper, final ImageRepository imageRepository) { + this.contentRepositoryFactory = documentStoreFactory; + this.clientRepositoryWrapper = clientRepositoryWrapper; + this.imageRepository = imageRepository; + } + + @Transactional + @Override + public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream, + Long fileSize) { + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + deletePreviousClientImage(client); + + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(); + String imageLocation = contentRepository.saveImage(inputStream, clientId, imageName, fileSize); + return updateClientImage(client, imageLocation, contentRepository.getStorageType()); + } + + @Transactional + @Override + public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final Base64EncodedImage encodedImage) { + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + deletePreviousClientImage(client); + + ContentRepository contenRepository = this.contentRepositoryFactory.getRepository(); + final String imageLocation = contenRepository.saveImage(encodedImage, clientId, "image"); + + return updateClientImage(client, imageLocation, contenRepository.getStorageType()); + } + + @Transactional + @Override + public CommandProcessingResult deleteClientImage(final Long clientId) { + + final Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); + + Image image = client.getImage(); + // delete image from the file system + if (image != null) { + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(StorageType.fromInt(image.getStorageType())); + contentRepository.deleteImage(clientId, image.getLocation()); + client.setImage(null); + this.imageRepository.delete(image); + this.clientRepositoryWrapper.save(client); + } + + return new CommandProcessingResult(clientId); + } + + private void deletePreviousClientImage(final Client client) { + Image image = client.getImage(); + if (image != null) { + ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(StorageType.fromInt(image.getStorageType())); + contentRepository.deleteImage(client.getId(), image.getLocation()); + } + } + + private CommandProcessingResult updateClientImage(final Client client, final String imageLocation, final StorageType storageType) { + Image image = client.getImage(); + if (image == null) { + image = new Image(imageLocation, storageType); + } else { + image.setLocation(imageLocation); + image.setStorageType(storageType.getValue()); + } + this.imageRepository.save(image); + client.setImage(image); + this.clientRepositoryWrapper.save(client); + return new CommandProcessingResult(client.getId()); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java index 0edea3483..e2a56432e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/api/AuthenticationApiResource.java @@ -9,13 +9,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Set; -import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer; import org.mifosplatform.infrastructure.security.data.AuthenticatedUserData; import org.mifosplatform.useradministration.data.RoleData; @@ -49,7 +49,6 @@ public class AuthenticationApiResource { } @POST - @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String authenticate(@QueryParam("username") final String username, @QueryParam("password") final String password) { @@ -73,8 +72,17 @@ public class AuthenticationApiResource { roles.add(role.toData()); } - authenticatedUserData = new AuthenticatedUserData(username, roles, permissions, principal.getId(), new String( - base64EncodedAuthenticationKey)); + final Long officeId = principal.getOffice().getId(); + final String officeName = principal.getOffice().getName(); + + final Long staffId = principal.getStaffId(); + final String staffDisplayName = principal.getStaffDisplayName(); + + final EnumOptionData organisationalRole = principal.organisationalRoleData(); + + authenticatedUserData = new AuthenticatedUserData(username, officeId, officeName, staffId, staffDisplayName, + organisationalRole, roles, permissions, + principal.getId(), new String(base64EncodedAuthenticationKey)); } return this.apiJsonSerializerService.serialize(authenticatedUserData); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java index d60f0b883..b787217c6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/security/data/AuthenticatedUserData.java @@ -7,6 +7,7 @@ package org.mifosplatform.infrastructure.security.data; import java.util.Collection; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.useradministration.data.RoleData; /** @@ -23,6 +24,16 @@ public class AuthenticatedUserData { @SuppressWarnings("unused") private final boolean authenticated; @SuppressWarnings("unused") + private final Long officeId; + @SuppressWarnings("unused") + private final String officeName; + @SuppressWarnings("unused") + private final Long staffId; + @SuppressWarnings("unused") + private final String staffDisplayName; + @SuppressWarnings("unused") + private final EnumOptionData organisationalRole; + @SuppressWarnings("unused") private final Collection roles; @SuppressWarnings("unused") private final Collection permissions; @@ -32,13 +43,24 @@ public class AuthenticatedUserData { this.userId = null; this.base64EncodedAuthenticationKey = null; this.authenticated = false; + this.officeId = null; + this.officeName = null; + this.staffId = null; + this.staffDisplayName = null; + this.organisationalRole = null; this.roles = null; this.permissions = permissions; } - public AuthenticatedUserData(final String username, final Collection roles, final Collection permissions, - final Long userId, final String base64EncodedAuthenticationKey) { + public AuthenticatedUserData(final String username, final Long officeId, final String officeName, final Long staffId, + final String staffDisplayName, final EnumOptionData organisationalRole, final Collection roles, + final Collection permissions, final Long userId, final String base64EncodedAuthenticationKey) { this.username = username; + this.officeId = officeId; + this.officeName = officeName; + this.staffId = staffId; + this.staffDisplayName = staffDisplayName; + this.organisationalRole = organisationalRole; this.userId = userId; this.base64EncodedAuthenticationKey = base64EncodedAuthenticationKey; this.authenticated = true; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java index e7d5e189d..bdf28007d 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/ApplicationCurrency.java @@ -77,6 +77,10 @@ public class ApplicationCurrency extends AbstractPersistable { return new CurrencyData(this.code, this.name, this.decimalPlaces, this.displaySymbol, this.nameCode); } + public CurrencyData toData(final int digitsAfterDecimalSupported) { + return new CurrencyData(this.code, this.name, digitsAfterDecimalSupported, this.displaySymbol, this.nameCode); + } + public OrganisationCurrency toOrganisationCurrency() { return new OrganisationCurrency(this.code, this.name, this.decimalPlaces, this.nameCode, this.displaySymbol); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java index 9a66837ee..9b2f2402c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/monetary/domain/Money.java @@ -62,8 +62,52 @@ public class Money implements Comparable { this.currencyDigitsAfterDecimal = digitsAfterDecimal; final BigDecimal amountZeroed = defaultToZeroIfNull(amount); - BigDecimal amountStripped = amountZeroed.stripTrailingZeros(); - this.amount = amountStripped.setScale(this.currencyDigitsAfterDecimal, RoundingMode.HALF_EVEN); + final BigDecimal amountStripped = amountZeroed.stripTrailingZeros(); + final BigDecimal amountScaled = amountStripped.setScale(this.currencyDigitsAfterDecimal, RoundingMode.HALF_EVEN); + this.amount = amountScaled; + + // TODO - KW - below was test of using floor/ceil functions to support + // round monetary amounts into multiples of say 20/50. + // if (this.currencyDigitsAfterDecimal == 0) { + // double existingVal = amountScaled.doubleValue(); + // double ceilingOfValue = ceiling(existingVal, Double.valueOf("20.0")); + // double floorOfValue = floor(existingVal, Double.valueOf("20.0")); + // + // double floorDiff = existingVal - floorOfValue; + // double ceilDiff = ceilingOfValue - existingVal; + // + // if (ceilDiff > floorDiff) { + // this.amount = BigDecimal.valueOf(floorOfValue); + // } else { + // this.amount = BigDecimal.valueOf(ceilingOfValue); + // } + // } else { + // this.amount = amountScaled; + // } + } + + public static double ceiling(final double n, final double s) { + double c; + + if ((n < 0 && s > 0) || (n > 0 && s < 0)) { + c = Double.NaN; + } else { + c = (n == 0 || s == 0) ? 0 : Math.ceil(n / s) * s; + } + + return c; + } + + public static double floor(final double n, final double s) { + double f; + + if ((n < 0 && s > 0) || (n > 0 && s < 0) || (s == 0 && n != 0)) { + f = Double.NaN; + } else { + f = (n == 0 || s == 0) ? 0 : Math.floor(n / s) * s; + } + + return f; } private static BigDecimal defaultToZeroIfNull(final BigDecimal value) { @@ -256,4 +300,8 @@ public class Money implements Comparable { private MonetaryCurrency monetaryCurrency() { return new MonetaryCurrency(this.currencyCode, this.currencyDigitsAfterDecimal); } + + public Money zero() { + return Money.zero(getCurrency()); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java index fcefcffe2..69d4c2df4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/Staff.java @@ -17,6 +17,7 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.office.domain.Office; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -24,14 +25,12 @@ import org.springframework.data.jpa.domain.AbstractPersistable; @Table(name = "m_staff", uniqueConstraints = { @UniqueConstraint(columnNames = { "display_name" }, name = "display_name") }) public class Staff extends AbstractPersistable { - @Column(name = "firstname", length = 50) private String firstname; @Column(name = "lastname", length = 50) private String lastname; - @SuppressWarnings("unused") @Column(name = "display_name", length = 100) private String displayName; @@ -44,6 +43,14 @@ public class Staff extends AbstractPersistable { @Column(name = "is_loan_officer ", nullable = false) private boolean loanOfficer; + @Column(name = "organisational_role_enum", nullable = true) + private Integer organisationalRoleType; + + @SuppressWarnings("unused") + @ManyToOne + @JoinColumn(name = "organisational_role_parent_staff_id", nullable = true) + private Staff organisationalRoleParentStaff; + public static Staff fromJson(final Office staffOffice, final JsonCommand command) { final String firstnameParamName = "firstname"; @@ -58,7 +65,7 @@ public class Staff extends AbstractPersistable { return new Staff(staffOffice, firstname, lastname, isLoanOfficer); } - public static Staff createNew(Office staffOffice, final String firstname, final String lastname, boolean isLoanOfficer) { + public static Staff createNew(final Office staffOffice, final String firstname, final String lastname, final boolean isLoanOfficer) { return new Staff(staffOffice, firstname, lastname, isLoanOfficer); } @@ -74,6 +81,14 @@ public class Staff extends AbstractPersistable { deriveDisplayName(firstname); } + public EnumOptionData organisationalRoleData() { + EnumOptionData organisationalRole = null; + if (this.organisationalRoleType != null) { + organisationalRole = StaffEnumerations.organisationalRole(this.organisationalRoleType); + } + return organisationalRole; + } + public void changeOffice(final Office newOffice) { this.office = newOffice; } @@ -143,4 +158,8 @@ public class Staff extends AbstractPersistable { public Long officeId() { return this.office.getId(); } + + public String displayName() { + return this.displayName; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java new file mode 100644 index 000000000..f6f58236b --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffEnumerations.java @@ -0,0 +1,37 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.organisation.staff.domain; + +import org.mifosplatform.infrastructure.core.data.EnumOptionData; + +public class StaffEnumerations { + + public static EnumOptionData organisationalRole(final Integer id) { + return organisationalRole(StaffOrganisationalRoleType.fromInt(id)); + } + + public static EnumOptionData organisationalRole(final StaffOrganisationalRoleType type) { + EnumOptionData optionData = new EnumOptionData(StaffOrganisationalRoleType.INVALID.getValue().longValue(), + StaffOrganisationalRoleType.INVALID.getCode(), "Invalid"); + switch (type) { + case PROGRAM_DIRECTOR: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Program Director"); + break; + case BRANCH_MANAGER: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Branch Manager"); + break; + case FIELD_OFFICER_COORDINATOR: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Field Officer Coordinator"); + break; + case FIELD_OFFICER: + optionData = new EnumOptionData(type.getValue().longValue(), type.getCode(), "Field Officer"); + break; + case INVALID: + break; + } + return optionData; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java new file mode 100644 index 000000000..14382496c --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/organisation/staff/domain/StaffOrganisationalRoleType.java @@ -0,0 +1,50 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.organisation.staff.domain; + +public enum StaffOrganisationalRoleType { + + INVALID(0, "staffOrganisationalRoleType.invalid"), // + PROGRAM_DIRECTOR(100, "staffOrganisationalRoleType.programDirector"), // + BRANCH_MANAGER(200, "staffOrganisationalRoleType.branchManager"), // + FIELD_OFFICER_COORDINATOR(300, "staffOrganisationalRoleType.coordinator"), // + FIELD_OFFICER(400, "staffOrganisationalRoleType.fieldAgent"); + + private final Integer value; + private final String code; + + private StaffOrganisationalRoleType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return value; + } + + public String getCode() { + return code; + } + + public static StaffOrganisationalRoleType fromInt(final Integer chargeCalculation) { + StaffOrganisationalRoleType chargeCalculationType = StaffOrganisationalRoleType.INVALID; + switch (chargeCalculation) { + case 100: + chargeCalculationType = PROGRAM_DIRECTOR; + break; + case 200: + chargeCalculationType = BRANCH_MANAGER; + break; + case 300: + chargeCalculationType = FIELD_OFFICER_COORDINATOR; + break; + case 400: + chargeCalculationType = FIELD_OFFICER; + break; + } + return chargeCalculationType; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java index 9d0a4c67b..8069b7463 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java @@ -140,7 +140,7 @@ public class CalendarReadPlatformServiceImpl implements CalendarReadPlatformServ String sqlCalendarTypeOptions = getSqlCalendarTypeOptionsInString(calendarTypeOptions); sql = rm.schema() + " " + parentHeirarchyCondition + " and ci.entity_type_enum = ? and c.calendar_type_enum in (" + sqlCalendarTypeOptions + ") order by c.start_date "; - result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, entityTypeId }); + result = this.jdbcTemplate.query(sql, rm, new Object[] { entityId, CalendarEntityType.CENTERS.getValue() }); } return result; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformService.java index 1ffd4a1f7..802a54779 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformService.java @@ -19,5 +19,7 @@ public interface ChargeReadPlatformService { Collection retrieveLoanApplicableCharges(boolean feeChargesOnly); + Collection retrieveLoanApplicablePenalties(); + Collection retrieveLoanProductCharges(Long loanProductId); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 9a5d891f5..4031760b9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -111,6 +111,17 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService return this.jdbcTemplate.query(sql, rm, new Object[] { ChargeAppliesTo.LOAN.getValue() }); } + + @Override + public Collection retrieveLoanApplicablePenalties() { + this.context.authenticatedUser(); + + final ChargeMapper rm = new ChargeMapper(); + + String sql = "select " + rm.chargeSchema() + + " where c.is_deleted=0 and c.is_active=1 and c.is_penalty=1 and c.charge_applies_to_enum=? order by c.name "; + return this.jdbcTemplate.query(sql, rm, new Object[] { ChargeAppliesTo.LOAN.getValue() }); + } private static final class ChargeMapper implements RowMapper { @@ -157,4 +168,5 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService active); } } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java index d3efb9eca..4495360df 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientAccountSummaryData.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.client.data; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; @@ -26,6 +27,8 @@ public class ClientAccountSummaryData { private final Integer accountStatusId; private final LoanStatusEnumData status; + @SuppressWarnings("unused") + private final EnumOptionData loanType; private final SavingsAccountStatusEnumData savingAccountStatus; public ClientAccountSummaryData(final Long id, final String externalId, final Long productId, final String loanProductName, @@ -37,11 +40,12 @@ public class ClientAccountSummaryData { this.productName = loanProductName; this.accountStatusId = loanStatusId; this.status = null; + this.loanType = null; this.savingAccountStatus = null; } public ClientAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, - final String loanProductName, final LoanStatusEnumData loanStatus) { + final String loanProductName, final LoanStatusEnumData loanStatus, final EnumOptionData loanType) { this.id = id; this.accountNo = accountNo; this.externalId = externalId; @@ -49,9 +53,10 @@ public class ClientAccountSummaryData { this.productName = loanProductName; this.accountStatusId = null; this.status = loanStatus; + this.loanType = loanType; this.savingAccountStatus = null; } - + public ClientAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, final String loanProductName, final SavingsAccountStatusEnumData savingAccountStatus) { this.id = id; @@ -61,6 +66,7 @@ public class ClientAccountSummaryData { this.productName = loanProductName; this.accountStatusId = null; this.status = null; + this.loanType = null; this.savingAccountStatus = savingAccountStatus; } @@ -68,7 +74,7 @@ public class ClientAccountSummaryData { Integer accountStatus = this.accountStatusId; if (accountStatus == null && this.status != null) { accountStatus = this.status.id().intValue(); - }else if (accountStatus == null && this.savingAccountStatus != null) { + } else if (accountStatus == null && this.savingAccountStatus != null) { accountStatus = this.savingAccountStatus.id().intValue(); } return accountStatus; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java index d4ee1e225..1463f0473 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/data/ClientData.java @@ -22,6 +22,7 @@ import org.mifosplatform.portfolio.group.data.GroupGeneralData; */ final public class ClientData implements Comparable { + private final Long id; private final String accountNo; private final String externalId; @@ -40,8 +41,7 @@ final public class ClientData implements Comparable { private final Long officeId; private final String officeName; - private final String imageKey; - @SuppressWarnings("unused") + private final Long imageId; private final Boolean imagePresent; // associations @@ -58,13 +58,13 @@ final public class ClientData implements Comparable { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageKey, allowedOffices, clientData.groups); + clientData.externalId, clientData.activationDate, clientData.imageId, allowedOffices, clientData.groups); } public static ClientData setParentGroups(final ClientData clientData, final Collection parentGroups) { return new ClientData(clientData.accountNo, clientData.status, clientData.officeId, clientData.officeName, clientData.id, clientData.firstname, clientData.middlename, clientData.lastname, clientData.fullname, clientData.displayName, - clientData.externalId, clientData.activationDate, clientData.imageKey, clientData.officeOptions, parentGroups); + clientData.externalId, clientData.activationDate, clientData.imageId, clientData.officeOptions, parentGroups); } public static ClientData clientIdentifier(final Long id, final String accountNo, final EnumOptionData status, final String firstname, @@ -81,14 +81,14 @@ final public class ClientData implements Comparable { public static ClientData instance(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, - final String displayName, final String externalId, final LocalDate activationDate, final String imageKey) { + final String displayName, final String externalId, final LocalDate activationDate, final Long imageId) { return new ClientData(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey, null, null); + externalId, activationDate, imageId, null, null); } private ClientData(final String accountNo, final EnumOptionData status, final Long officeId, final String officeName, final Long id, final String firstname, final String middlename, final String lastname, final String fullname, final String displayName, - final String externalId, final LocalDate activationDate, final String imageKey, final Collection allowedOffices, + final String externalId, final LocalDate activationDate, final Long imageId, final Collection allowedOffices, final Collection groups) { this.accountNo = accountNo; this.status = status; @@ -107,11 +107,11 @@ final public class ClientData implements Comparable { this.displayName = StringUtils.defaultIfEmpty(displayName, null); this.externalId = StringUtils.defaultIfEmpty(externalId, null); this.activationDate = activationDate; - this.imageKey = imageKey; - if (imageKey != null) { + this.imageId = imageId; + if (imageId != null) { this.imagePresent = Boolean.TRUE; } else { - this.imagePresent = null; + this.imagePresent = Boolean.FALSE; } // associations @@ -137,16 +137,12 @@ final public class ClientData implements Comparable { return this.officeName; } - public String imageKey() { - return this.imageKey; + public Long getImageId() { + return this.imageId; } - public boolean imageKeyDoesNotExist() { - return !imageKeyExists(); - } - - private boolean imageKeyExists() { - return StringUtils.isNotBlank(this.imageKey); + public Boolean getImagePresent() { + return this.imagePresent; } @Override @@ -194,4 +190,5 @@ final public class ClientData implements Comparable { public LocalDate getActivationDate() { return this.activationDate; } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java index 4a1ebff7f..b8408c84a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/domain/Client.java @@ -5,26 +5,6 @@ */ package org.mifosplatform.portfolio.client.domain; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; -import javax.persistence.Transient; -import javax.persistence.UniqueConstraint; - import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormatter; @@ -33,12 +13,17 @@ import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.infrastructure.documentmanagement.domain.Image; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.portfolio.client.api.ClientApiConstants; import org.mifosplatform.portfolio.group.domain.Group; import org.springframework.data.jpa.domain.AbstractPersistable; +import javax.persistence.*; + +import java.util.*; + @Entity @Table(name = "m_client", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "account_no_UNIQUE") }) public final class Client extends AbstractPersistable { @@ -50,6 +35,10 @@ public final class Client extends AbstractPersistable { @JoinColumn(name = "office_id", nullable = false) private Office office; + @OneToOne(optional = true) + @JoinColumn(name = "image_id", nullable = true) + private Image image; + /** * A value from {@link ClientStatus}. */ @@ -72,16 +61,12 @@ public final class Client extends AbstractPersistable { @Column(name = "fullname", length = 100) private String fullname; - @SuppressWarnings("unused") @Column(name = "display_name", length = 100, nullable = false) private String displayName; @Column(name = "external_id", length = 100, nullable = true, unique = true) private String externalId; - @Column(name = "image_key", length = 500, nullable = true) - private String imageKey; - @ManyToMany @JoinTable(name = "m_group_client", joinColumns = @JoinColumn(name = "client_id"), inverseJoinColumns = @JoinColumn(name = "group_id")) private Set groups; @@ -385,16 +370,24 @@ public final class Client extends AbstractPersistable { return this.office.identifiedBy(officeId); } - public String imageKey() { - return imageKey; - } - - public void updateImageKey(final String imageKey) { - this.imageKey = imageKey; - } - public Long officeId() { return this.office.getId(); } + public void setImage(Image image) { + this.image = image; + } + + public Image getImage() { + return this.image; + } + + public String getDisplayName() { + return this.displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java index fc87e6cdf..33d6808f0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformService.java @@ -5,6 +5,7 @@ */ package org.mifosplatform.portfolio.client.service; + import java.util.Collection; import org.mifosplatform.infrastructure.core.service.Page; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java index 729e4a995..9389613a4 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientReadPlatformServiceImpl.java @@ -32,7 +32,6 @@ import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.loanaccount.data.LoanStatusEnumData; import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; -import org.mifosplatform.portfolio.savings.domain.SavingsAccountStatusType; import org.mifosplatform.portfolio.savings.service.SavingStatusMapper; import org.mifosplatform.portfolio.savings.service.SavingsEnumerations; import org.mifosplatform.useradministration.domain.AppUser; @@ -42,7 +41,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; -@Service(value = "clientReadPlatformService") +@Service public class ClientReadPlatformServiceImpl implements ClientReadPlatformService { private final JdbcTemplate jdbcTemplate; @@ -96,7 +95,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService if (searchParameters.isOrderByRequested()) { sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); - + if (searchParameters.isSortOrderProvided()) { sqlBuilder.append(' ').append(searchParameters.getSortOrder()); } @@ -113,7 +112,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), new Object[] { hierarchySearchString }, this.clientMapper); } - + private String buildSqlStringFromClientCriteria(final SearchParameters searchParameters) { final String sqlSearch = searchParameters.getSqlSearch(); @@ -163,7 +162,6 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService @Override public ClientData retrieveOne(final Long clientId) { - try { AppUser currentUser = context.authenticatedUser(); String hierarchy = currentUser.getOffice().getHierarchy(); @@ -177,7 +175,9 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService Collection parentGroups = this.jdbcTemplate.query(clientGroupsSql, this.clientGroupsMapper, new Object[] { clientId }); + return ClientData.setParentGroups(clientData, parentGroups); + } catch (EmptyResultDataAccessException e) { throw new ClientNotFoundException(clientId); } @@ -210,8 +210,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String hierarchy = currentUser.getOffice().getHierarchy(); final String hierarchySearchString = hierarchy + "%"; - final String sql = "select " + this.membersOfGroupMapper.schema() - + " where o.hierarchy like ? and pgc.group_id = ?"; + final String sql = "select " + this.membersOfGroupMapper.schema() + " where o.hierarchy like ? and pgc.group_id = ?"; return this.jdbcTemplate.query(sql, this.membersOfGroupMapper, new Object[] { hierarchySearchString, groupId }); } @@ -227,7 +226,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService sqlBuilder.append("c.office_id as officeId, o.name as officeName, "); sqlBuilder.append("c.firstname as firstname, c.middlename as middlename, c.lastname as lastname, "); sqlBuilder.append("c.fullname as fullname, c.display_name as displayName, "); - sqlBuilder.append("c.activation_date as activationDate, c.image_key as imagekey "); + sqlBuilder.append("c.activation_date as activationDate, c.image_id as imageId "); sqlBuilder.append("from m_client c "); sqlBuilder.append("join m_office o on o.id = c.office_id "); sqlBuilder.append("join m_group_client pgc on pgc.client_id = c.id"); @@ -255,11 +254,11 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String displayName = rs.getString("displayName"); final String externalId = rs.getString("externalId"); final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate"); - final String imageKey = rs.getString("imageKey"); + final Long imageId = JdbcSupport.getLong(rs, "imageId"); final String officeName = rs.getString("officeName"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey); + externalId, activationDate, imageId); } } @@ -275,7 +274,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService builder.append("c.office_id as officeId, o.name as officeName, "); builder.append("c.firstname as firstname, c.middlename as middlename, c.lastname as lastname, "); builder.append("c.fullname as fullname, c.display_name as displayName, "); - builder.append("c.activation_date as activationDate, c.image_key as imagekey "); + builder.append("c.activation_date as activationDate, c.image_id as imageId "); builder.append("from m_client c "); builder.append("join m_office o on o.id = c.office_id "); @@ -303,13 +302,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String displayName = rs.getString("displayName"); final String externalId = rs.getString("externalId"); final LocalDate activationDate = JdbcSupport.getLocalDate(rs, "activationDate"); - final String imageKey = rs.getString("imageKey"); final String officeName = rs.getString("officeName"); + final Long imageId = JdbcSupport.getLong(rs, "imageId"); return ClientData.instance(accountNo, status, officeId, officeName, id, firstname, middlename, lastname, fullname, displayName, - externalId, activationDate, imageKey); + externalId, activationDate, imageId); } - } private static final class ParentGroupsMapper implements RowMapper { @@ -418,12 +416,12 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService if (results != null) { for (ClientAccountSummaryData row : savingsAccounts) { - SavingStatusMapper statusMapper = new SavingStatusMapper(row.accountStatusId()); + SavingStatusMapper statusMapper = new SavingStatusMapper(row.accountStatusId()); if (statusMapper.isOpen()) { - approvedSavingAccounts.add(row); + approvedSavingAccounts.add(row); } else { - pendingApprovalSavingAccounts.add(row); + pendingApprovalSavingAccounts.add(row); } } } @@ -492,8 +490,9 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService public String loanAccountSummarySchema() { StringBuilder accountsSummary = new StringBuilder("l.id as id, l.account_no as accountNo, l.external_id as externalId,"); - accountsSummary.append("l.product_id as productId, lp.name as productName,").append("l.loan_status_id as statusId ") - .append("from m_loan l ").append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id "); + accountsSummary.append("l.product_id as productId, lp.name as productName,") + .append("l.loan_status_id as statusId, l.loan_type_enum as loanType ").append("from m_loan l ") + .append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id "); return accountsSummary.toString(); } @@ -508,8 +507,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService final String loanProductName = rs.getString("productName"); final Integer loanStatusId = JdbcSupport.getInteger(rs, "statusId"); final LoanStatusEnumData loanStatus = LoanEnumerations.status(loanStatusId); + final Integer loanTypeId = JdbcSupport.getInteger(rs, "loanType"); + final EnumOptionData loanType = LoanEnumerations.loanType(loanTypeId); - return new ClientAccountSummaryData(id, accountNo, externalId, productId, loanProductName, loanStatus); + return new ClientAccountSummaryData(id, accountNo, externalId, productId, loanProductName, loanStatus, loanType); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java index 9f8d5b43e..bdad04f22 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformService.java @@ -5,12 +5,8 @@ */ package org.mifosplatform.portfolio.client.service; -import java.io.InputStream; - import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; -import org.springframework.security.access.prepost.PreAuthorize; public interface ClientWritePlatformService { @@ -22,12 +18,4 @@ public interface ClientWritePlatformService { CommandProcessingResult deleteClient(Long clientId); - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") - CommandProcessingResult saveOrUpdateClientImage(Long clientId, String imageName, InputStream inputStream); - - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'CREATE_CLIENTIMAGE')") - CommandProcessingResult saveOrUpdateClientImage(Long clientId, Base64EncodedImage encodedImage); - - @PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'DELETE_CLIENTIMAGE')") - CommandProcessingResult deleteClientImage(Long clientId); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 0bc4fb35f..52cf5ad4b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -5,24 +5,17 @@ */ package org.mifosplatform.portfolio.client.service; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.util.List; import java.util.Locale; import java.util.Map; -import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; -import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; -import org.mifosplatform.infrastructure.core.service.FileUtils; -import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.office.domain.Office; import org.mifosplatform.organisation.office.domain.OfficeRepository; @@ -34,7 +27,6 @@ import org.mifosplatform.portfolio.client.domain.AccountNumberGeneratorFactory; import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.ClientRepositoryWrapper; import org.mifosplatform.portfolio.client.exception.ClientMustBePendingToBeDeletedException; -import org.mifosplatform.portfolio.client.exception.ClientNotFoundException; import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.group.domain.GroupRepository; import org.mifosplatform.portfolio.group.exception.GroupNotFoundException; @@ -221,73 +213,6 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP } } - @Transactional - @Override - public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final String imageName, final InputStream inputStream) { - try { - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - String imageUploadLocation = setupForClientImageUpdate(clientId, client); - - String imageLocation = FileUtils.saveToFileSystem(inputStream, imageUploadLocation, imageName); - - return updateClientImage(clientId, client, imageLocation); - } catch (IOException ioe) { - logger.error(ioe.getMessage(), ioe); - throw new DocumentManagementException(imageName); - } - } - - @Transactional - @Override - public CommandProcessingResult deleteClientImage(final Long clientId) { - - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - - // delete image from the file system - if (StringUtils.isNotEmpty(client.imageKey())) { - FileUtils.deleteClientImage(clientId, client.imageKey()); - } - return updateClientImage(clientId, client, null); - } - - @Override - public CommandProcessingResult saveOrUpdateClientImage(final Long clientId, final Base64EncodedImage encodedImage) { - try { - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - final String imageUploadLocation = setupForClientImageUpdate(clientId, client); - - final String imageLocation = FileUtils.saveToFileSystem(encodedImage, imageUploadLocation, "image"); - - return updateClientImage(clientId, client, imageLocation); - } catch (IOException ioe) { - logger.error(ioe.getMessage(), ioe); - throw new DocumentManagementException("image"); - } - } - - private String setupForClientImageUpdate(final Long clientId, final Client client) { - if (client == null) { throw new ClientNotFoundException(clientId); } - - final String imageUploadLocation = FileUtils.generateClientImageParentDirectory(clientId); - // delete previous image from the file system - if (StringUtils.isNotEmpty(client.imageKey())) { - FileUtils.deleteClientImage(clientId, client.imageKey()); - } - - /** Recursively create the directory if it does not exist **/ - if (!new File(imageUploadLocation).isDirectory()) { - new File(imageUploadLocation).mkdirs(); - } - return imageUploadLocation; - } - - private CommandProcessingResult updateClientImage(final Long clientId, final Client client, final String imageLocation) { - client.updateImageKey(imageLocation); - this.clientRepository.save(client); - - return new CommandProcessingResult(clientId); - } - private void logAsErrorUnexpectedDataIntegrityException(final DataIntegrityViolationException dve) { logger.error(dve.getMessage(), dve); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java index 18493e3c7..d02c746fd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/group/data/GroupAccountSummaryData.java @@ -15,6 +15,7 @@ public class GroupAccountSummaryData { private final Long productId; private final String productName; private final Integer accountStatusId; + @SuppressWarnings("unused") private final String accountNo; public GroupAccountSummaryData(final Long id, final String externalId, final Long productId, final String productName, @@ -46,4 +47,4 @@ public class GroupAccountSummaryData { public Integer getAccountStatusId() { return this.accountStatusId; } -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java index 3059f5df6..3e9437e05 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoansApiResource.java @@ -64,6 +64,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.NotSupportedLoanTemplat import org.mifosplatform.portfolio.loanaccount.guarantor.data.GuarantorData; import org.mifosplatform.portfolio.loanaccount.guarantor.service.GuarantorReadPlatformService; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.mifosplatform.portfolio.loanaccount.service.LoanReadPlatformService; @@ -85,15 +86,15 @@ public class LoansApiResource { private final Set LOAN_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "accountNo", "status", "externalId", "clientId", "group", "loanProductId", "loanProductName", "loanProductDescription", "fundId", "fundName", "loanPurposeId", - "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", "inArrearsTolerance", "termFrequency", - "termPeriodFrequencyType", "numberOfRepayments", "repaymentEvery", "interestRatePerPeriod", "annualInterestRate", - "repaymentFrequencyType", "transactionProcessingStrategyId", "interestRateFrequencyType", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedFirstRepaymentOnDate", "interestChargedFromDate", "timeline", - "totalFeeChargesAtDisbursement", "summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors","meeting", - "productOptions", "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", - "repaymentFrequencyTypeOptions", "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", - "repaymentStrategyOptions", "chargeOptions", "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", - "chargeTemplate","calendarOptions")); + "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", "totalOverpaid", "inArrearsTolerance", + "termFrequency", "termPeriodFrequencyType", "numberOfRepayments", "repaymentEvery", "interestRatePerPeriod", + "annualInterestRate", "repaymentFrequencyType", "transactionProcessingStrategyId", "interestRateFrequencyType", + "amortizationType", "interestType", "interestCalculationPeriodType", "expectedFirstRepaymentOnDate", "graceOnPrincipalPayment", + "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "timeline", "totalFeeChargesAtDisbursement", + "summary", "repaymentSchedule", "transactions", "charges", "collateral", "guarantors", "meeting", "productOptions", + "amortizationTypeOptions", "interestTypeOptions", "interestCalculationPeriodTypeOptions", "repaymentFrequencyTypeOptions", + "termFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", + "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions")); private final String resourceNameForPermissions = "LOAN"; @@ -126,8 +127,7 @@ public class LoansApiResource { final CollateralReadPlatformService loanCollateralReadPlatformService, final LoanScheduleCalculationPlatformService calculationPlatformService, final StaffReadPlatformService staffReadPlatformService, final GuarantorReadPlatformService guarantorReadPlatformService, - final CodeValueReadPlatformService codeValueReadPlatformService, - final GroupReadPlatformService groupReadPlatformService, + final CodeValueReadPlatformService codeValueReadPlatformService, final GroupReadPlatformService groupReadPlatformService, final DefaultToApiJsonSerializer toApiJsonSerializer, final DefaultToApiJsonSerializer loanScheduleToApiJsonSerializer, final DefaultToApiJsonSerializer loanOfficeTransferToApiJsonSerializer, @@ -161,7 +161,8 @@ public class LoansApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String template(@QueryParam("clientId") final Long clientId, @QueryParam("groupId") final Long groupId, - @QueryParam("productId") final Long productId, @QueryParam("templateType") final String templateType, @Context final UriInfo uriInfo) { + @QueryParam("productId") final Long productId, @QueryParam("templateType") final String templateType, + @Context final UriInfo uriInfo) { context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions); @@ -174,24 +175,24 @@ public class LoansApiResource { Collection calendarOptions = null; LoanAccountData newLoanAccount = null; Long officeId = null; - + if (productId != null) { newLoanAccount = this.loanReadPlatformService.retrieveLoanProductDetailsTemplate(productId); } - - if(templateType == null){ + + if (templateType == null) { final String errorMsg = "Loan template type must be provided"; throw new LoanTemplateTypeRequiredException(errorMsg); - }else if (templateType.equals("collateral")) { + } else if (templateType.equals("collateral")) { newLoanAccount = LoanAccountData.collateralTemplate(loanCollateralOptions); } else { - //for JLG loan both client and group details are required + // for JLG loan both client and group details are required if (templateType.equals("individual") || templateType.equals("jlg")) { LoanAccountData loanAccountClientDetails = this.loanReadPlatformService.retrieveClientDetailsTemplate(clientId); officeId = loanAccountClientDetails.officeId(); - + newLoanAccount = (newLoanAccount == null) ? loanAccountClientDetails : LoanAccountData.populateClientDefaults( newLoanAccount, loanAccountClientDetails); @@ -211,7 +212,7 @@ public class LoansApiResource { loanAccountGroupData); } else if (templateType.equals("jlgbulk")) { - //get group details along with members in that group + // get group details along with members in that group final LoanAccountData loanAccountGroupData = this.loanReadPlatformService.retrieveGroupAndMembersDetailsTemplate(groupId); officeId = loanAccountGroupData.groupOfficeId(); calendarOptions = this.loanReadPlatformService.retrieveCalendars(groupId); @@ -223,7 +224,8 @@ public class LoansApiResource { } if (officeId != null) allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(officeId); - // add product options, allowed loan officers and calendar options (calendar options will be null in individual loan) + // add product options, allowed loan officers and calendar options + // (calendar options will be null in individual loan) newLoanAccount = LoanAccountData.associationsAndTemplate(newLoanAccount, productOptions, allowedLoanOfficers, calendarOptions); } final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); @@ -246,7 +248,7 @@ public class LoansApiResource { Collection guarantors = null; Collection collateral = null; CalendarData meeting = null; - + final Set mandatoryResponseParameters = new HashSet(); final Set associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters()); if (!associationParameters.isEmpty()) { @@ -293,7 +295,7 @@ public class LoansApiResource { collateral = null; } } - + if (associationParameters.contains("meeting")) { mandatoryResponseParameters.add("meeting"); meeting = this.calendarReadPlatformService.retrieveLoanCalendar(loanId); @@ -332,20 +334,20 @@ public class LoansApiResource { final boolean feeChargesOnly = false; chargeOptions = this.chargeReadPlatformService.retrieveLoanApplicableCharges(feeChargesOnly); chargeTemplate = this.loanChargeReadPlatformService.retrieveLoanChargeTemplate(); - - if(loanBasicDetails.officeId() != null){ + + if (loanBasicDetails.officeId() != null) { allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(loanBasicDetails.officeId()); - }else if(loanBasicDetails.groupOfficeId() != null){ + } else if (loanBasicDetails.groupOfficeId() != null) { allowedLoanOfficers = this.staffReadPlatformService.retrieveAllLoanOfficersByOffice(loanBasicDetails.groupOfficeId()); } - + loanPurposeOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode("LoanPurpose"); loanCollateralOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode("LoanCollateral"); - - if(loanBasicDetails.groupId() != null){ + + if (loanBasicDetails.groupId() != null) { calendarOptions = this.loanReadPlatformService.retrieveCalendars(loanBasicDetails.groupId()); } - + } final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments, @@ -376,7 +378,7 @@ public class LoansApiResource { final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, loanBasicDetails, LOAN_DATA_PARAMETERS); } - + @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @@ -388,10 +390,10 @@ public class LoansApiResource { final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); - final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); + final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule, new HashSet()); + return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule.toData(), new HashSet()); } final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanApplication().withJson(apiRequestBodyAsJson).build(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java index d0a97a4c9..f1f9378c9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanAccountData.java @@ -82,8 +82,11 @@ public class LoanAccountData { private final EnumOptionData interestCalculationPeriodType; private final BigDecimal inArrearsTolerance; private final Long transactionProcessingStrategyId; - private final LocalDate expectedFirstRepaymentOnDate; + private final Integer graceOnPrincipalPayment; + private final Integer graceOnInterestPayment; + private final Integer graceOnInterestCharged; private final LocalDate interestChargedFromDate; + private final LocalDate expectedFirstRepaymentOnDate; // timeline private final LoanApplicationTimelineData timeline; @@ -117,6 +120,7 @@ public class LoanAccountData { @Transient private final BigDecimal feeChargesAtDisbursementCharged; + private final BigDecimal totalOverpaid; /** * Used to produce a {@link LoanAccountData} with only collateral options @@ -143,6 +147,7 @@ public class LoanAccountData { final String loanOfficerName = null; final CurrencyData currencyData = null; final BigDecimal principal = null; + final BigDecimal totalOverpaid = null; final BigDecimal inArrearsTolerance = null; final Integer termFrequency = null; final EnumOptionData termPeriodFrequencyType = null; @@ -157,6 +162,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = null; final LoanSummaryData summary = null; @@ -185,14 +193,14 @@ public class LoanAccountData { return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, - repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, + currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, - transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, - interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, - loanCollateralOptions, calendarOptions); + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, + interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } /** @@ -219,6 +227,7 @@ public class LoanAccountData { final String loanOfficerName = null; final CurrencyData currencyData = null; final BigDecimal principal = null; + final BigDecimal totalOverpaid = null; final BigDecimal inArrearsTolerance = null; final Integer termFrequency = null; final EnumOptionData termPeriodFrequencyType = null; @@ -233,6 +242,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = LoanApplicationTimelineData.templateDefault(expectedDisbursementDate); final LoanSummaryData summary = null; @@ -262,11 +274,12 @@ public class LoanAccountData { return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, - repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, + currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -274,19 +287,20 @@ public class LoanAccountData { public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, clientAcc.clientId, clientAcc.clientName, - clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, - acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, - acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, - acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, - acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, - acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, clientAcc.timeline, acc.summary, - acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + clientAcc.clientOfficeId, acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, + acc.fundId, acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, + acc.principal, acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, + acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, + acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, + acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, clientAcc.timeline, acc.summary, + acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + /** * Used to produce a {@link LoanAccountData} with only group information * defaulted. @@ -312,6 +326,7 @@ public class LoanAccountData { final String loanOfficerName = null; final CurrencyData currencyData = null; final BigDecimal principal = null; + final BigDecimal totalOverpaid = null; final BigDecimal inArrearsTolerance = null; final Integer termFrequency = null; final EnumOptionData termPeriodFrequencyType = null; @@ -326,6 +341,9 @@ public class LoanAccountData { final EnumOptionData interestType = null; final EnumOptionData interestCalculationPeriodType = null; final LocalDate expectedFirstRepaymentOnDate = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final LocalDate interestChargedFromDate = null; final LoanApplicationTimelineData timeline = LoanApplicationTimelineData.templateDefault(expectedDisbursementDate); final LoanSummaryData summary = null; @@ -355,31 +373,33 @@ public class LoanAccountData { return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, - repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, + currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentFrequencyType, transactionProcessingStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, - interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, - collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, summary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { - return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, - acc.clientOfficeId, groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, - acc.fundName, acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, - acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, - acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, - acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, - acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, groupAcc.timeline, acc.summary, - acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, + groupAcc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, + acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, + acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, groupAcc.timeline, acc.summary, + acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, final Collection termFrequencyTypeOptions, final Collection repaymentFrequencyTypeOptions, final Collection repaymentStrategyOptions, @@ -387,7 +407,7 @@ public class LoanAccountData { final Collection interestTypeOptions, final Collection interestCalculationPeriodTypeOptions, final Collection fundOptions, final Collection chargeOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions) { - + final Long id = null; final String accountNo = null; final LoanStatusEnumData status = null; @@ -406,6 +426,7 @@ public class LoanAccountData { final String loanOfficerName = null; final CurrencyData currencyData = null; final BigDecimal principal = null; + final BigDecimal totalOverpaid = null; final BigDecimal inArrearsTolerance = null; final Integer numberOfRepayments = null; final Integer repaymentEvery = null; @@ -423,7 +444,7 @@ public class LoanAccountData { final LoanSummaryData summary = null; final BigDecimal feeChargesDueAtDisbursementCharged = null; final ChargeData chargeTemplate = null; - + final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; final Collection collateral = null; @@ -432,7 +453,7 @@ public class LoanAccountData { final Collection productOptions = null; final Collection calendarOptions = null; final Collection loanOfficerOptions = null; - + final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); @@ -441,20 +462,21 @@ public class LoanAccountData { charges.add(charge.toLoanChargeData()); } - return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, - group, loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), - loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), - product.getPrincipal(), product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, - product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), - product.getTransactionProcessingStrategyId(), product.getAmortizationType(), product.getInterestRatePerPeriod(), - product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.getInterestCalculationPeriodType(), expectedFirstRepaymentOnDate, interestChargedFromDate, timeline, - summary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, - productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, - interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, - fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, + product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), loanPurposeId, + loanPurposeName, loanOfficerId, loanOfficerName, product.getCurrency(), product.getPrincipal(), totalOverpaid, + product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, product.getNumberOfRepayments(), + product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), + product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), + product.getAnnualInterestRate(), product.getInterestType(), product.getInterestCalculationPeriodType(), + expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), product.getGraceOnInterestPayment(), + product.getGraceOnInterestCharged(), interestChargedFromDate, timeline, summary, feeChargesDueAtDisbursementCharged, + repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, + repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, + interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, + loanPurposeOptions, loanCollateralOptions, calendarOptions); } - + public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { final LoanScheduleData repaymentSchedule = null; @@ -477,7 +499,7 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; - + final Integer termFrequency = product.getNumberOfRepayments() * product.getRepaymentEvery(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); @@ -487,15 +509,16 @@ public class LoanAccountData { } return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), product.getFundName(), - acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, product.getCurrency(), - product.getPrincipal(), product.getInArrearsTolerance(), termFrequency, termPeriodFrequencyType, - product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), + acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), product.getFundId(), + product.getFundName(), acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, + product.getCurrency(), product.getPrincipal(), acc.totalOverpaid, product.getInArrearsTolerance(), termFrequency, + termPeriodFrequencyType, product.getNumberOfRepayments(), product.getRepaymentEvery(), product.getRepaymentFrequencyType(), product.getTransactionProcessingStrategyId(), product.getAmortizationType(), product.getInterestRatePerPeriod(), product.getInterestRateFrequencyType(), product.getAnnualInterestRate(), product.getInterestType(), - product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, acc.interestChargedFromDate, acc.timeline, - acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, - productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + product.getInterestCalculationPeriodType(), acc.expectedFirstRepaymentOnDate, product.getGraceOnPrincipalPayment(), + product.getGraceOnInterestPayment(), product.getGraceOnInterestCharged(), acc.interestChargedFromDate, acc.timeline, + acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, + calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -506,15 +529,16 @@ public class LoanAccountData { */ public static LoanAccountData basicLoanDetails(final Long id, final String accountNo, final LoanStatusEnumData status, final String externalId, final Long clientId, final String clientName, final Long clientOfficeId, final GroupGeneralData group, - final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, final Long fundId, - final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, - final String loanOfficerName, final CurrencyData currencyData, final BigDecimal principal, final BigDecimal inArrearsTolerance, - final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, final Integer numberOfRepayments, - final Integer repaymentEvery, final EnumOptionData repaymentFrequencyType, final Long transactionStrategyId, - final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, - final BigDecimal annualInterestRate, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, - final LocalDate expectedFirstRepaymentOnDate, final LocalDate interestChargedFromDate, - final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, + final EnumOptionData loanType, final Long loanProductId, final String loanProductName, final String loanProductDescription, + final Long fundId, final String fundName, final Long loanPurposeId, final String loanPurposeName, final Long loanOfficerId, + final String loanOfficerName, final CurrencyData currencyData, final BigDecimal principal, final BigDecimal totalOverpaid, + final BigDecimal inArrearsTolerance, final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, + final Integer numberOfRepayments, final Integer repaymentEvery, final EnumOptionData repaymentFrequencyType, + final Long transactionStrategyId, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, + final EnumOptionData interestRateFrequencyType, final BigDecimal annualInterestRate, final EnumOptionData interestType, + final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, + final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData loanSummary, final BigDecimal feeChargesDueAtDisbursementCharged) { final LoanScheduleData repaymentSchedule = null; @@ -538,14 +562,15 @@ public class LoanAccountData { final Collection loanPurposeOptions = null; final Collection loanCollateralOptions = null; final Collection calendarOptions = null; - + return new LoanAccountData(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, - currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, repaymentEvery, - repaymentFrequencyType, transactionStrategyId, amortizationType, interestRatePerPeriod, interestRateFrequencyType, - annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, interestChargedFromDate, - timeline, loanSummary, feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, - guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, + currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, termPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentFrequencyType, transactionStrategyId, amortizationType, interestRatePerPeriod, + interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, expectedFirstRepaymentOnDate, + graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, + feeChargesDueAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } @@ -566,41 +591,44 @@ public class LoanAccountData { final Collection loanOfficerOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions, final Collection calendarOptions) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, acc.loanPurposeId, - acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, acc.inArrearsTolerance, - acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, repaymentSchedule, - transactions, charges, collateral, guarantors, calendarData, productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, - transactionProcessingStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, - interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, - loanCollateralOptions, calendarOptions); + acc.group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, + acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, + acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, acc.timeline, acc.summary, + acc.feeChargesAtDisbursementCharged, repaymentSchedule, transactions, charges, collateral, guarantors, calendarData, + productOptions, termFrequencyTypeOptions, repaymentFrequencyTypeOptions, transactionProcessingStrategyOptions, + interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, + fundOptions, chargeOptions, chargeTemplate, loanOfficerOptions, loanPurposeOptions, loanCollateralOptions, calendarOptions); } - public static LoanAccountData associationsAndTemplate(LoanAccountData acc, Collection productOptions, - Collection allowedLoanOfficers, final Collection calendarOptions) { - return associationsAndTemplate(acc, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, - productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, acc.transactionProcessingStrategyOptions, - acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, - acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, allowedLoanOfficers, - acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions); + public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, + final Collection allowedLoanOfficers, final Collection calendarOptions) { + return associationsAndTemplate(acc, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, + acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, + allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions); } - + public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientName, acc.clientOfficeId, - group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, acc.loanPurposeId, - acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, acc.inArrearsTolerance, - acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, acc.repaymentEvery, acc.repaymentFrequencyType, - acc.transactionProcessingStrategyId, acc.amortizationType, acc.interestRatePerPeriod, acc.interestRateFrequencyType, - acc.annualInterestRate, acc.interestType, acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, - acc.interestChargedFromDate, acc.timeline, acc.summary, acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, - acc.transactions, acc.charges, acc.collateral, acc.guarantors, acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, - acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, acc.interestTypeOptions, - acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.loanOfficerOptions, acc.loanPurposeOptions, - acc.loanCollateralOptions, acc.calendarOptions); + group, acc.loanType, acc.loanProductId, acc.loanProductName, acc.loanProductDescription, acc.fundId, acc.fundName, + acc.loanPurposeId, acc.loanPurposeName, acc.loanOfficerId, acc.loanOfficerName, acc.currency, acc.principal, + acc.totalOverpaid, acc.inArrearsTolerance, acc.termFrequency, acc.termPeriodFrequencyType, acc.numberOfRepayments, + acc.repaymentEvery, acc.repaymentFrequencyType, acc.transactionProcessingStrategyId, acc.amortizationType, + acc.interestRatePerPeriod, acc.interestRateFrequencyType, acc.annualInterestRate, acc.interestType, + acc.interestCalculationPeriodType, acc.expectedFirstRepaymentOnDate, acc.graceOnPrincipalPayment, + acc.graceOnInterestPayment, acc.graceOnInterestCharged, acc.interestChargedFromDate, acc.timeline, acc.summary, + acc.feeChargesAtDisbursementCharged, acc.repaymentSchedule, acc.transactions, acc.charges, acc.collateral, acc.guarantors, + acc.meeting, acc.productOptions, acc.termFrequencyTypeOptions, acc.repaymentFrequencyTypeOptions, + acc.transactionProcessingStrategyOptions, acc.interestRateFrequencyTypeOptions, acc.amortizationTypeOptions, + acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, + acc.loanOfficerOptions, acc.loanPurposeOptions, acc.loanCollateralOptions, acc.calendarOptions); } - + private LoanAccountData( final Long id, // final String accountNo, // @@ -622,6 +650,7 @@ public class LoanAccountData { final String loanOfficerName, // final CurrencyData currency, final BigDecimal principal, + final BigDecimal totalOverpaid, final BigDecimal inArrearsTolerance, // final Integer termFrequency, final EnumOptionData termPeriodFrequencyType, // @@ -631,6 +660,7 @@ public class LoanAccountData { final Long transactionProcessingStrategyId, final EnumOptionData amortizationType, final BigDecimal interestRatePerPeriod, final EnumOptionData interestRateFrequencyType, final BigDecimal annualInterestRate, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, final LocalDate expectedFirstRepaymentOnDate, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, final LoanApplicationTimelineData timeline, final LoanSummaryData summary, final BigDecimal feeChargesDueAtDisbursementCharged, final LoanScheduleData repaymentSchedule, final Collection transactions, final Collection charges, @@ -663,6 +693,7 @@ public class LoanAccountData { this.loanOfficerName = loanOfficerName; this.currency = currency; this.principal = principal; + this.totalOverpaid = totalOverpaid; this.inArrearsTolerance = inArrearsTolerance; this.termFrequency = termFrequency; this.termPeriodFrequencyType = termPeriodFrequencyType; @@ -677,6 +708,9 @@ public class LoanAccountData { this.interestType = interestType; this.interestCalculationPeriodType = interestCalculationPeriodType; this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate; + this.graceOnPrincipalPayment = graceOnPrincipalPayment; + this.graceOnInterestPayment = graceOnInterestPayment; + this.graceOnInterestCharged = graceOnInterestCharged; this.interestChargedFromDate = interestChargedFromDate; this.timeline = timeline; this.feeChargesAtDisbursementCharged = feeChargesDueAtDisbursementCharged; @@ -736,7 +770,7 @@ public class LoanAccountData { } else { this.loanCollateralOptions = loanCollateralOptions; } - + if (CollectionUtils.isEmpty(calendarOptions)) { this.calendarOptions = null; } else { @@ -760,12 +794,12 @@ public class LoanAccountData { public Collection charges() { return this.charges; } - - public Long groupOfficeId(){ - return group == null? null : group.officeId(); + + public Long groupOfficeId() { + return group == null ? null : group.officeId(); } - - public Long groupId(){ - return group == null? null : group.getId(); + + public Long groupId() { + return group == null ? null : group.getId(); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java deleted file mode 100644 index 9501569b7..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java +++ /dev/null @@ -1,128 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.domain; - -import java.util.List; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; - -/** - * Creocore style {@link LoanRepaymentScheduleTransactionProcessor}. - * - * For standard transactions, pays off components in order of interest, then - * principal. - * - * If a transaction results in an advance payment or over-payment for a given - * installment, the over paid amount is pay off on the principal component of - * subsequent installments. - * - * If the entire principal of an installment is paid in advance then the - * interest component is waived. - */ -public class CreocoreLoanRepaymentScheduleTransactionProcessor extends - AbstractLoanRepaymentScheduleTransactionProcessor { - - /** - * For creocore, early is defined as any date before the installment due - * date - */ - @SuppressWarnings("unused") - @Override - protected boolean isTransactionInAdvanceOfInstallment( - final int currentInstallmentIndex, - final List installments, - final LocalDate transactionDate, final Money transactionAmount) { - - LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - - return transactionDate.isBefore(currentInstallment.getDueDate()); - } - - /** - * For early/'in advance' repayments, pay off in the same way as on-time payments, - * interest first then principal. - */ - @SuppressWarnings("unused") - @Override - protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final LocalDate transactionDate, final Money paymentInAdvance) { - - return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); - } - - /** - * For late repayments, pay off in the same way as on-time payments, - * interest first then principal. - */ - @SuppressWarnings("unused") - @Override - protected Money handleTransactionThatIsALateRepaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - return handleTransactionThatIsOnTimePaymentOfInstallment( - currentInstallment, loanTransaction, - transactionAmountUnprocessed); - } - - /** - * For normal on-time repayments, pays off interest first, then principal. - */ - @Override - protected Money handleTransactionThatIsOnTimePaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - - if (loanTransaction.isChargesWaiver()) { - penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency)); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency)); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - } else if (loanTransaction.isInterestWaiver()) { - interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - } else { - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - } - - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - return transactionAmountRemaining; - } - - @SuppressWarnings("unused") - @Override - protected void onLoanOverpayment(final LoanTransaction loanTransaction, - final Money loanOverPaymentAmount) { - // dont do anything for with loan over-payment - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index 19a2ec498..509bc5d4f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -6,6 +6,8 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -56,19 +58,18 @@ import org.mifosplatform.portfolio.collateral.domain.LoanCollateral; import org.mifosplatform.portfolio.fund.domain.Fund; import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.loanaccount.command.LoanChargeCommand; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.mifosplatform.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DefaultLoanScheduleGeneratorFactory; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; @@ -101,7 +102,7 @@ public class Loan extends AbstractPersistable { @SuppressWarnings("unused") @Column(name = "loan_type_enum", nullable = false) private Integer loanType; - + @ManyToOne @JoinColumn(name = "product_id", nullable = false) private LoanProduct loanProduct; @@ -223,6 +224,10 @@ public class Loan extends AbstractPersistable { @Column(name = "interest_calculated_from_date") private Date interestChargedFromDate; + @SuppressWarnings("unused") + @Column(name = "total_overpaid_derived", scale = 6, precision = 19) + private BigDecimal totalOverpaid; + @LazyCollection(LazyCollectionOption.FALSE) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true) private Set charges = new HashSet(); @@ -264,23 +269,22 @@ public class Loan extends AbstractPersistable { @Transient private LoanSummaryWrapper loanSummaryWrapper; - public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, - final Fund fund, final Staff officer, final CodeValue loanPurpose, - final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, - final Set loanCharges, final Set collateral) { + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, + final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Set collateral) { final LoanStatus status = null; final Group group = null; - LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, status, loanCharges, collateral); } public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, - final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, - final Set loanCharges) { + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { final LoanStatus status = null; - final LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); final CodeValue loanPurpose = null; final Set collateral = null; final Client client = null; @@ -290,10 +294,9 @@ public class Loan extends AbstractPersistable { public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, - final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, - final Set loanCharges) { + final LoanTransactionProcessingStrategy transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges) { final LoanStatus status = null; - final LoanProductRelatedDetail loanRepaymentScheduleDetail = loanSchedule.loanProductRelatedDetail(); final CodeValue loanPurpose = null; final Set collateral = null; return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, @@ -304,10 +307,14 @@ public class Loan extends AbstractPersistable { this.client = null; } - private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, - final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, + private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, + final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set loanCharges, final Set collateral) { + + this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; + this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); + if (StringUtils.isBlank(accountNo)) { this.accountNumber = new RandomPasswordGenerator(19).generate(); this.accountNumberRequiresAutoGeneration = true; @@ -323,7 +330,6 @@ public class Loan extends AbstractPersistable { this.transactionProcessingStrategy = transactionProcessingStrategy; this.loanProduct = loanProduct; - this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; if (loanStatus != null) { this.loanStatus = loanStatus.getValue(); } else { @@ -677,10 +683,10 @@ public class Loan extends AbstractPersistable { return this.client; } - public LoanProduct loanProduct(){ + public LoanProduct loanProduct() { return this.loanProduct; } - + public LoanProductRelatedDetail repaymentScheduleDetail() { return this.loanRepaymentScheduleDetail; } @@ -735,10 +741,10 @@ public class Loan extends AbstractPersistable { this.collateral.addAll(associateWithThisLoan(loanCollateral)); } - public void updateLoanSchedule(final LoanScheduleData modifiedLoanSchedule) { + public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { this.repaymentScheduleInstallments.clear(); - for (LoanSchedulePeriodData scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { + for (LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { if (scheduledLoanInstallment.isRepaymentPeriod()) { LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this, @@ -750,24 +756,6 @@ public class Loan extends AbstractPersistable { } } - // if the loan application/contract is modified when repayments are - // already against it - then need to re-process it - - /**** - * TODO Vishwas Currently we do not allow modifying a Loan contract once - * the loan is approved (hence there is no need to adjust any loan - * transactions). - * - * Consider removing this block of code or logically completing it for - * the future by getting the list of affected Transactions - ***/ - final List repaymentsOrWaivers = retreiveListOfTransactionsPostDisbursement(); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory - .determineProcessor(this.transactionProcessingStrategy); - loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), repaymentsOrWaivers, getCurrency(), - this.repaymentScheduleInstallments, setOfLoanCharges()); - updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); } @@ -782,7 +770,11 @@ public class Loan extends AbstractPersistable { if (isNotDisbursed()) { this.summary.zeroFields(); this.summaryArrearsAging = null; + this.totalOverpaid = null; } else { + final Money overpaidBy = calculateTotalOverpayment(); + this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero(); + final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); this.summary.updateSummary(loanCurrency(), principal, this.repaymentScheduleInstallments, this.loanSummaryWrapper, isDisbursed()); @@ -822,16 +814,18 @@ public class Loan extends AbstractPersistable { this.externalId = StringUtils.defaultIfEmpty(newValue, null); } - //add clientId, groupId and loanType changes to actual changes - //FIXME: AA - We may require separate api command to move loan from one client to another + // add clientId, groupId and loanType changes to actual changes + // FIXME: AA - We may require separate api command to move loan from one + // client to another final String clientIdParamName = "clientId"; final Long clientId = this.client == null ? null : this.client.getId(); if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { final Long newValue = command.longValueOfParameterNamed(clientIdParamName); actualChanges.put(clientIdParamName, newValue); } - - //FIXME: AA - We may require separate api command to move loan from one group to another + + // FIXME: AA - We may require separate api command to move loan from one + // group to another final String groupIdParamName = "groupId"; final Long groupId = this.group == null ? null : this.group.getId(); if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) { @@ -942,17 +936,19 @@ public class Loan extends AbstractPersistable { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate()); } - - if (!(client == null)){ - if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, getSubmittedOnDate()); - } - }else if (!(group == null)){ - if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, getSubmittedOnDate()); - } + + if (!(client == null)) { + if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + getSubmittedOnDate()); + } + } else if (!(group == null)) { + if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + getSubmittedOnDate()); + } } if (getSubmittedOnDate().isAfter(getExpectedDisbursedOnLocalDate())) { @@ -986,7 +982,7 @@ public class Loan extends AbstractPersistable { actualChanges.put(collateralParamName, listOfLoanCollateralData(possiblyModifedLoanCollateralItems)); } } - + return actualChanges; } @@ -1038,18 +1034,11 @@ public class Loan extends AbstractPersistable { } } - public void loanApplicationSubmittal(final AppUser currentUser, final LoanSchedule loanSchedule, - final LoanLifecycleStateMachine lifecycleStateMachine, final LocalDate submittedOn, final String externalId) { + public void loanApplicationSubmittal(final AppUser currentUser, final LoanScheduleModel loanSchedule, + final LoanApplicationTerms loanApplicationTerms, final LoanLifecycleStateMachine lifecycleStateMachine, + final LocalDate submittedOn, final String externalId) { - final LoanScheduleData loanScheduleData = loanSchedule.generate(); - - // Have to set expectedDisbursementDate to avoid nullPointer so should - // be passed down to updateLoanSchedule method - if (loanSchedule.getDisbursementDate() != null) { - this.expectedDisbursementDate = loanSchedule.getDisbursementDate().toDate(); - } - - updateLoanSchedule(loanScheduleData); + updateLoanSchedule(loanSchedule); LoanStatus from = null; if (this.loanStatus != null) { @@ -1060,37 +1049,33 @@ public class Loan extends AbstractPersistable { this.loanStatus = statusEnum.getValue(); this.externalId = externalId; - - this.termFrequency = loanSchedule.getLoanTermFrequency(); - this.termPeriodFrequencyType = loanSchedule.getLoanTermPeriodFrequencyType().getValue(); - + this.termFrequency = loanApplicationTerms.getLoanTermFrequency(); + this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue(); this.submittedOnDate = submittedOn.toDate(); this.submittedBy = currentUser; + this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate().toDate(); + this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate(); + this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate(); + updateLoanScheduleDependentDerivedFields(); - if (loanSchedule.getRepaymentStartFromDate() != null) { - this.expectedFirstRepaymentOnDate = loanSchedule.getRepaymentStartFromDate().toDate(); - } - - if (loanSchedule.getInterestChargedFromDate() != null) { - this.interestChargedFromDate = loanSchedule.getInterestChargedFromDate().toDate(); - } - if (submittedOn.isAfter(DateUtils.getLocalDateOfTenant())) { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); } - if (!(client == null)){ - if (submittedOn.isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn); - } - }else if (!(group == null)){ - if (submittedOn.isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn); - } + if (!(client == null)) { + if (submittedOn.isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + submittedOn); + } + } else if (!(group == null)) { + if (submittedOn.isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + submittedOn); + } } if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) { @@ -1291,9 +1276,9 @@ public class Loan extends AbstractPersistable { return ids; } - public Map disburse(final AppUser currentUser, final JsonCommand command, final ApplicationCurrency currency, - final List existingTransactionIds, final List existingReversedTransactionIds, - final Map actualChanges, final PaymentDetail paymentDetail) { + public Map disburse(final LoanScheduleGeneratorFactory loanScheduleFactory, final AppUser currentUser, + final JsonCommand command, final ApplicationCurrency currency, final List existingTransactionIds, + final List existingReversedTransactionIds, final Map actualChanges, final PaymentDetail paymentDetail) { updateLoanToPreDisbursalState(); @@ -1319,18 +1304,13 @@ public class Loan extends AbstractPersistable { handleDisbursementTransaction(paymentDetail, actualDisbursementDate); if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) { - regenerateRepaymentSchedule(currency); + regenerateRepaymentSchedule(loanScheduleFactory, currency); + updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); } else { + updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanSummaryDerivedFields(); } - /*** - * Adding a temporary Interest applied transaction (applying the - * full Interest as soon as the loan is disbursed), will have to - * re-look into this logic once loans with Interest calculation need - * to be created or more advances accrual accounting support has to - * be provided - ***/ Money interestApplied = Money.of(this.getCurrency(), this.summary.getTotalInterestCharged()); final LoanTransaction interestAppliedTransaction = LoanTransaction.applyInterest(this, interestApplied, actualDisbursementDate); this.loanTransactions.add(interestAppliedTransaction); @@ -1339,39 +1319,36 @@ public class Loan extends AbstractPersistable { return actualChanges; } + private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { + + for (LoanRepaymentScheduleInstallment repaymentPeriod : this.repaymentScheduleInstallments) { + repaymentPeriod.updateDerivedFields(this.loanCurrency(), actualDisbursementDate); + } + } + /* * Ability to regenerate the repayment schedule based on the loans current * details/state. */ - private void regenerateRepaymentSchedule(final ApplicationCurrency applicationCurrency) { + private void regenerateRepaymentSchedule(final LoanScheduleGeneratorFactory loanScheduleFactory, + final ApplicationCurrency applicationCurrency) { final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); - final LoanScheduleGenerator loanScheduleGenerator = new DefaultLoanScheduleGeneratorFactory().create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = loanScheduleFactory.create(interestMethod); - final BigDecimal principal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount(); - final BigDecimal inArrearsTolerance = this.loanRepaymentScheduleDetail.getInArrearsTolerance().getAmount(); - final BigDecimal interestRatePerPeriod = this.loanRepaymentScheduleDetail.getNominalInterestRatePerPeriod(); - final PeriodFrequencyType interestRatePeriodFrequencyType = this.loanRepaymentScheduleDetail.getInterestPeriodFrequencyType(); - final BigDecimal defaultAnnualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate(); + final RoundingMode roundingMode = RoundingMode.HALF_EVEN; + final MathContext mc = new MathContext(8, roundingMode); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = this.loanRepaymentScheduleDetail - .getInterestCalculationPeriodMethod(); - final Integer repaymentEvery = this.loanRepaymentScheduleDetail.getRepayEvery(); - final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); - final Integer numberOfRepayments = this.loanRepaymentScheduleDetail.getNumberOfRepayments(); - final AmortizationMethod amortizationMethod = this.loanRepaymentScheduleDetail.getAmortizationMethod(); final Integer loanTermFrequency = this.termFrequency; final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType); - final LoanSchedule loanSchedule = new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod, - interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, - repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency, - loanTermPeriodFrequencyType, setOfLoanCharges(), this.getDisbursementDate(), this.getExpectedFirstRepaymentOnDate(), - this.getInterestChargedFromDate(), inArrearsTolerance); + LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, + loanTermPeriodFrequencyType, getExpectedDisbursedOnLocalDate(), getExpectedFirstRepaymentOnDate(), getInArrearsTolerance(), + this.loanRepaymentScheduleDetail); - final LoanScheduleData generatedData = loanSchedule.generate(); + LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, this.charges); - updateLoanSchedule(generatedData); + updateLoanSchedule(loanSchedule); } private LoanTransaction handleDisbursementTransaction(final PaymentDetail paymentDetail, final LocalDate disbursedOn) { @@ -1405,6 +1382,9 @@ public class Loan extends AbstractPersistable { for (LoanCharge charge : setOfLoanCharges()) { if (charge.isDueAtDisbursement()) { charge.markAsFullyPaid(); + // Add "Loan Charge Paid By" details to this transaction + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount()); + chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); } else { handleChargeAppliedTransaction(charge, disbursedOn); } @@ -1419,18 +1399,11 @@ public class Loan extends AbstractPersistable { } if (disbursedOn.isAfter(new LocalDate())) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be in the future."; + final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + + " is disbursed cannot be in the future."; throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } - LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate(); - if (disbursedOn.isAfter(firstRepaymentDueDate)) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be after the first expected repayment date: " - + firstRepaymentDueDate.toString(); - throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, - this.accountNumber, disbursedOn, firstRepaymentDueDate); - } - return disbursementTransaction; } @@ -1593,12 +1566,9 @@ public class Loan extends AbstractPersistable { private void doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) { if (this.isOverPaid()) { + // FIXME - kw - update account balance to negative amount. handleLoanOverpayment(loanLifecycleStateMachine); } else if (this.summary.isRepaidInFull(loanCurrency())) { - // TODO - KW - probably should not close the loan automatically but - // let user decide if loan is closed or not and provide closing - // date. - // - need to dig into loan closure scenarios with MFIs handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine); } } @@ -1661,7 +1631,7 @@ public class Loan extends AbstractPersistable { public LocalDate possibleNextRepaymentDate() { LocalDate earliestUnpaidInstallmentDate = new LocalDate(); for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (installment.isNotFullyCompleted()) { + if (installment.isNotFullyPaidOff()) { earliestUnpaidInstallmentDate = installment.getDueDate(); break; } @@ -1687,7 +1657,7 @@ public class Loan extends AbstractPersistable { Money possibleNextRepaymentAmount = Money.zero(currency); for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (installment.isNotFullyCompleted()) { + if (installment.isNotFullyPaidOff()) { possibleNextRepaymentAmount = installment.getTotalOutstanding(currency); break; } @@ -1708,7 +1678,7 @@ public class Loan extends AbstractPersistable { for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) { final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency()); - if (scheduledRepayment.isOverdueOn(new LocalDate()) && scheduledRepayment.isNotFullyCompleted() + if (scheduledRepayment.isOverdueOn(new LocalDate()) && scheduledRepayment.isNotFullyPaidOff() && outstandingForPeriod.isGreaterThanZero()) { transactionDate = scheduledRepayment.getDueDate(); possibleInterestToWaive = outstandingForPeriod; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index 1ebc1d960..c6000a1e0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -449,6 +449,11 @@ public class LoanCharge extends AbstractPersistable { return Money.of(currency, this.amountWrittenOff); } + /** + * @param incrementBy + * Amount used to pay off this charge + * @return Actual amount paid on this charge + */ public Money updatePaidAmountBy(final Money incrementBy) { Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); @@ -471,7 +476,7 @@ public class LoanCharge extends AbstractPersistable { this.paid = determineIfFullyPaid(); - return incrementBy.minus(amountPaidOnThisCharge); + return amountPaidOnThisCharge; } public String name() { @@ -482,6 +487,10 @@ public class LoanCharge extends AbstractPersistable { return this.charge.getCurrencyCode(); } + public Charge getCharge() { + return this.charge; + } + @Override public boolean equals(final Object obj) { if (obj == null) { return false; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java new file mode 100755 index 000000000..c2bd409b8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java @@ -0,0 +1,62 @@ +package org.mifosplatform.portfolio.loanaccount.domain; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_charge_paid_by") +public class LoanChargePaidBy extends AbstractPersistable { + + @ManyToOne + @JoinColumn(name = "loan_transaction_id", nullable = false) + private LoanTransaction loanTransaction; + + @ManyToOne + @JoinColumn(name = "loan_charge_id", nullable = false) + private LoanCharge loanCharge; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + protected LoanChargePaidBy() { + + } + + public LoanChargePaidBy(LoanTransaction loanTransaction, LoanCharge loanCharge, BigDecimal amount) { + this.loanTransaction = loanTransaction; + this.loanCharge = loanCharge; + this.amount = amount; + } + + public LoanTransaction getLoanTransaction() { + return this.loanTransaction; + } + + public void setLoanTransaction(LoanTransaction loanTransaction) { + this.loanTransaction = loanTransaction; + } + + public LoanCharge getLoanCharge() { + return this.loanCharge; + } + + public void setLoanCharge(LoanCharge loanCharge) { + this.loanCharge = loanCharge; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java new file mode 100755 index 000000000..5a84a0d49 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java @@ -0,0 +1,13 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanChargePaidByRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 9ab626877..05ff88a47 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -86,14 +86,25 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus @Column(name = "penalty_charges_waived_derived", scale = 6, precision = 19, nullable = true) private BigDecimal penaltyChargesWaived; + @Column(name = "total_paid_in_advance_derived", scale = 6, precision = 19, nullable = true) + private BigDecimal totalPaidInAdvance; + + @Column(name = "total_paid_late_derived", scale = 6, precision = 19, nullable = true) + private BigDecimal totalPaidLate; + @Column(name = "completed_derived", nullable = false) - private boolean completed; + private boolean obligationsMet; + + @SuppressWarnings("unused") + @Temporal(TemporalType.DATE) + @Column(name = "obligations_met_on_date") + private Date obligationsMetOnDate; protected LoanRepaymentScheduleInstallment() { this.installmentNumber = null; this.fromDate = null; this.dueDate = null; - this.completed = false; + this.obligationsMet = false; } public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate, @@ -107,7 +118,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.interestCharged = defaultToNullIfZero(interest); this.feeChargesCharged = defaultToNullIfZero(feeCharges); this.penaltyCharges = defaultToNullIfZero(penaltyCharges); - this.completed = false; + this.obligationsMet = false; } private BigDecimal defaultToNullIfZero(final BigDecimal value) { @@ -173,8 +184,8 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus } public Money getInterestOutstanding(final MonetaryCurrency currency) { - final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency)).plus( - getInterestWrittenOff(currency)); + final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency)) + .plus(getInterestWrittenOff(currency)); return getInterestCharged(currency).minus(interestAccountedFor); } @@ -239,12 +250,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.loan = loan; } - public boolean isFullyCompleted() { - return this.completed; + public boolean isObligationsMet() { + return this.obligationsMet; } - public boolean isNotFullyCompleted() { - return !this.completed; + public boolean isNotFullyPaidOff() { + return !this.obligationsMet; } public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) { @@ -267,11 +278,14 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.penaltyChargesPaid = null; this.penaltyChargesWaived = null; this.penaltyChargesWrittenOff = null; + this.totalPaidInAdvance = null; + this.totalPaidLate = null; - this.completed = false; + this.obligationsMet = false; + this.obligationsMetOnDate = null; } - public Money payPenaltyChargesComponent(final Money transactionAmountRemaining) { + public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money penaltyPortionOfTransaction = Money.zero(currency); @@ -287,12 +301,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.penaltyChargesPaid = defaultToNullIfZero(this.penaltyChargesPaid); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return penaltyPortionOfTransaction; } - public Money payFeeChargesComponent(final Money transactionAmountRemaining) { + public Money payFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money feePortionOfTransaction = Money.zero(currency); @@ -308,12 +322,14 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.feeChargesPaid = defaultToNullIfZero(this.feeChargesPaid); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + trackAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, feePortionOfTransaction); return feePortionOfTransaction; } - public Money payInterestComponent(final Money transactionAmountRemaining) { + public Money payInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money interestPortionOfTransaction = Money.zero(currency); @@ -329,12 +345,14 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.interestPaid = defaultToNullIfZero(this.interestPaid); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + trackAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, interestPortionOfTransaction); return interestPortionOfTransaction; } - public Money payPrincipalComponent(final Money transactionAmountRemaining) { + public Money payPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money principalPortionOfTransaction = Money.zero(currency); @@ -350,12 +368,14 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.principalCompleted = defaultToNullIfZero(this.principalCompleted); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); + + trackAdvanceAndLateTotalsForRepaymentPeriod(transactionDate, currency, principalPortionOfTransaction); return principalPortionOfTransaction; } - public Money waiveInterestComponent(final Money transactionAmountRemaining) { + public Money waiveInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedInterestPortionOfTransaction = Money.zero(currency); @@ -370,12 +390,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.interestWaived = defaultToNullIfZero(this.interestWaived); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return waivedInterestPortionOfTransaction; } - public Money waivePenaltyChargesComponent(final Money transactionAmountRemaining) { + public Money waivePenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedPenaltyChargesPortionOfTransaction = Money.zero(currency); @@ -390,12 +410,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.penaltyChargesWaived = defaultToNullIfZero(this.penaltyChargesWaived); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return waivedPenaltyChargesPortionOfTransaction; } - public Money waiveFeeChargesComponent(final Money transactionAmountRemaining) { + public Money waiveFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedFeeChargesPortionOfTransaction = Money.zero(currency); @@ -410,47 +430,51 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.feeChargesWaived = defaultToNullIfZero(this.feeChargesWaived); - this.completed = getTotalOutstanding(currency).isZero(); + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return waivedFeeChargesPortionOfTransaction; } - public Money writeOffOutstandingPrincipal(final MonetaryCurrency currency) { + public Money writeOffOutstandingPrincipal(final LocalDate transactionDate, final MonetaryCurrency currency) { final Money principalDue = getPrincipalOutstanding(currency); this.principalWrittenOff = defaultToNullIfZero(principalDue.getAmount()); - this.completed = getTotalOutstanding(currency).isZero(); + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return principalDue; } - public Money writeOffOutstandingInterest(final MonetaryCurrency currency) { + public Money writeOffOutstandingInterest(final LocalDate transactionDate, final MonetaryCurrency currency) { final Money interestDue = getInterestOutstanding(currency); this.interestWrittenOff = defaultToNullIfZero(interestDue.getAmount()); - this.completed = getTotalOutstanding(currency).isZero(); + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return interestDue; } - public Money writeOffOutstandingFeeCharges(final MonetaryCurrency currency) { + public Money writeOffOutstandingFeeCharges(final LocalDate transactionDate, final MonetaryCurrency currency) { final Money feeChargesOutstanding = getFeeChargesOutstanding(currency); this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesOutstanding.getAmount()); - this.completed = getTotalOutstanding(currency).isZero(); + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return feeChargesOutstanding; } - public Money writeOffOutstandingPenaltyCharges(final MonetaryCurrency currency) { + public Money writeOffOutstandingPenaltyCharges(final LocalDate transactionDate, final MonetaryCurrency currency) { final Money penaltyChargesOutstanding = getPenaltyChargesOutstanding(currency); this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesOutstanding.getAmount()); - this.completed = getTotalOutstanding(currency).isZero(); + + checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return penaltyChargesOutstanding; } - public boolean isOverdueOn(final LocalDate transactionDate) { - return this.getDueDate().isBefore(transactionDate); + public boolean isOverdueOn(final LocalDate date) { + return this.getDueDate().isBefore(date); } public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff, @@ -462,4 +486,39 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount()); this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount()); } + + public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate actualDisbursementDate) { + if (!this.obligationsMet && getTotalOutstanding(currency).isZero()) { + this.obligationsMet = true; + this.obligationsMetOnDate = actualDisbursementDate.toDate(); + } + } + + private void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, + final Money amountPaidInRepaymentPeriod) { + if (isInAdvance(transactionDate)) { + this.totalPaidInAdvance = asMoney(this.totalPaidInAdvance, currency).plus(amountPaidInRepaymentPeriod).getAmount(); + } else if (isLatePayment(transactionDate)) { + this.totalPaidLate = asMoney(this.totalPaidLate, currency).plus(amountPaidInRepaymentPeriod).getAmount(); + } + } + + private Money asMoney(final BigDecimal decimal, final MonetaryCurrency currency) { + return Money.of(currency, decimal); + } + + private boolean isInAdvance(final LocalDate transactionDate) { + return transactionDate.isBefore(getDueDate()); + } + + private boolean isLatePayment(final LocalDate transactionDate) { + return transactionDate.isAfter(getDueDate()); + } + + private void checkIfRepaymentPeriodObligationsAreMet(final LocalDate transactionDate, final MonetaryCurrency currency) { + this.obligationsMet = getTotalOutstanding(currency).isZero(); + if (this.obligationsMet) { + this.obligationsMetOnDate = transactionDate.toDate(); + } + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessorFactory.java index 4abd71eb5..aaf44ca01 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessorFactory.java @@ -5,6 +5,13 @@ */ package org.mifosplatform.portfolio.loanaccount.domain; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.MifosStyleLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.springframework.stereotype.Component; @@ -14,7 +21,7 @@ public class LoanRepaymentScheduleTransactionProcessorFactory { public LoanRepaymentScheduleTransactionProcessor determineProcessor( final LoanTransactionProcessingStrategy transactionProcessingStrategy) { - LoanRepaymentScheduleTransactionProcessor processor = new MifosStyleLoanRepaymentScheduleTransactionProcessor(); + LoanRepaymentScheduleTransactionProcessor processor = new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(); if (transactionProcessingStrategy != null) { @@ -33,6 +40,14 @@ public class LoanRepaymentScheduleTransactionProcessorFactory { if (transactionProcessingStrategy.isIndianRBIStrategy()) { processor = new RBILoanRepaymentScheduleTransactionProcessor(); } + + if (transactionProcessingStrategy.isPrincipalInterestPenaltiesFeesOrderStrategy()) { + processor = new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(); + } + + if (transactionProcessingStrategy.isInterestPrincipalPenaltiesFeesOrderStrategy()) { + processor = new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(); + } } return processor; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java index 59ddad89f..98cdfeaf7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanSummary.java @@ -13,9 +13,13 @@ import javax.persistence.Embeddable; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.scheduledjobs.UpdateLoanSummariesScheduledJob; /** - * LoanSummary encapsulates all the summary details of a {@link Loan}. + * Encapsulates all the summary details of a {@link Loan}. + * + * {@link LoanSummary} fields are updated through a scheduled job. see - + * {@link UpdateLoanSummariesScheduledJob}. */ @Embeddable public final class LoanSummary { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index 362a8229f..56b549ea0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -6,18 +6,26 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -70,6 +78,10 @@ public final class LoanTransaction extends AbstractPersistable { @Column(name = "is_reversed", nullable = false) private boolean reversed; + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true) + private Set loanChargesPaid = new HashSet(); + protected LoanTransaction() { this.loan = null; this.dateOf = null; @@ -250,6 +262,10 @@ public final class LoanTransaction extends AbstractPersistable { return !isReversed(); } + public boolean isAnyTypeOfRepayment() { + return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment(); + } + public boolean isRepayment() { return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotReversed(); } @@ -360,6 +376,20 @@ public final class LoanTransaction extends AbstractPersistable { thisTransactionData.put("paymentTypeId", this.paymentDetail.getPaymentType().getId()); } + if (!this.loanChargesPaid.isEmpty()) { + final List> loanChargesPaidData = new ArrayList>(); + for (LoanChargePaidBy chargePaidBy : this.loanChargesPaid) { + final Map loanChargePaidData = new LinkedHashMap(); + loanChargePaidData.put("chargeId", chargePaidBy.getLoanCharge().getCharge().getId()); + loanChargePaidData.put("isPenalty", chargePaidBy.getLoanCharge().getCharge().isPenalty()); + loanChargePaidData.put("loanChargeId", chargePaidBy.getLoanCharge().getId()); + loanChargePaidData.put("amount", chargePaidBy.getAmount()); + + loanChargesPaidData.add(loanChargePaidData); + } + thisTransactionData.put("loanChargesPaid", loanChargesPaidData); + } + return thisTransactionData; } @@ -367,4 +397,12 @@ public final class LoanTransaction extends AbstractPersistable { return this.loan; } + public Set getLoanChargesPaid() { + return this.loanChargesPaid; + } + + public void setLoanChargesPaid(Set loanChargesPaid) { + this.loanChargesPaid = loanChargesPaid; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java deleted file mode 100644 index 4c402ea5f..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java +++ /dev/null @@ -1,123 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.domain; - -import java.util.List; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; - -/** - * Old Mifos style {@link LoanRepaymentScheduleTransactionProcessor}. - * - * For ALL types of transactions, pays off components in order of interest, then - * principal. - * - * Other formulas exist on mifos where you can choose 'Declining-Balance Interest Recalculation' which simply - * means, recalculate the interest component based on the how much principal is outstanding at a point in time; - * but this isnt trying to model that option only the basic one for now. - */ -@SuppressWarnings("unused") -public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends - AbstractLoanRepaymentScheduleTransactionProcessor { - - @Override - protected boolean isTransactionInAdvanceOfInstallment( - final int currentInstallmentIndex, - final List installments, - final LocalDate transactionDate, final Money transactionAmount) { - - LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - - return transactionDate.isBefore(currentInstallment.getDueDate()); - } - - /** - * For early/'in advance' repayments, pay off in the same way as on-time payments, - * interest first then principal. - */ - @Override - protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final LocalDate transactionDate, final Money paymentInAdvance) { - - return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); - } - - /** - * For late repayments, pay off in the same way as on-time payments, - * interest first then principal. - */ - @Override - protected Money handleTransactionThatIsALateRepaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - return handleTransactionThatIsOnTimePaymentOfInstallment( - currentInstallment, loanTransaction, - transactionAmountUnprocessed); - } - - /** - * For normal on-time repayments, pays off interest first, then principal. - */ - @Override - protected Money handleTransactionThatIsOnTimePaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - - if (loanTransaction.isChargesWaiver()) { -// penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency)); -// transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); -// -// feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency)); -// transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); -// - // zero this type of transaction and ignore it for now. - transactionAmountRemaining = Money.zero(currency); - } else if (loanTransaction.isInterestWaiver()) { - interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - } else { - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - } - - return transactionAmountRemaining; - } - - @Override - protected void onLoanOverpayment(final LoanTransaction loanTransaction, - final Money loanOverPaymentAmount) { - // TODO - KW - dont do anything with loan over-payment for now - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/RBILoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/RBILoanRepaymentScheduleTransactionProcessor.java deleted file mode 100644 index b5bd93db5..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/RBILoanRepaymentScheduleTransactionProcessor.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.domain; - -import java.util.List; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; - -/** - * Adhikar/RBI style {@link LoanRepaymentScheduleTransactionProcessor}. - * - * From https://mifosforge.jira.com/browse/MIFOS-5636: - * - * Per RBI regulations, all interest must be paid (both current and overdue) before principal is paid. - * - * For example on a loan with two installments due (one current and one overdue) of 220 each (200 principal + 20 interest): - * - * Partial Payment of 40 - * 20 Payment to interest on Installment #1 (200 principal remaining) - * 20 Payment to interest on Installment #2 (200 principal remaining) - */ -public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { - - /** - * For creocore, early is defined as any date before the installment due - * date - */ - @SuppressWarnings("unused") - @Override - protected boolean isTransactionInAdvanceOfInstallment( - final int currentInstallmentIndex, - final List installments, - final LocalDate transactionDate, final Money transactionAmount) { - - LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - - return transactionDate.isBefore(currentInstallment.getDueDate()); - } - - /** - * For early/'in advance' repayments, pays off principal component only. - */ - @SuppressWarnings("unused") - @Override - protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final LocalDate transactionDate, final Money paymentInAdvance) { - - return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); - } - - /** - * For late repayments, pay off in the same way as on-time payments, - * interest first then principal. - */ - @Override - protected Money handleTransactionThatIsALateRepaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - // pay of overdue and current interest due given transaction date - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money interestWaivedPortion = Money.zero(currency); - Money feeChargesPortion = Money.zero(currency); - Money penaltyChargesPortion = Money.zero(currency); - - if (loanTransaction.isInterestWaiver()) { - interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion); - - final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); - loanTransaction.updateComponents(principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion); - } else { - - LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments); - - for (LoanRepaymentScheduleInstallment installment : installments) { - if ( - installment.isInterestDue(currency) && - (installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber())) - ) { - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - Money interestPortion = installment.payInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - Money principalPortion = Money.zero(currency); - loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency)); - } - } - - // With whatever is remaining, pay off principal components of installments - for (LoanRepaymentScheduleInstallment installment : installments) { - if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) { - Money principalPortion = installment.payPrincipalComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - Money interestPortion = Money.zero(currency); - loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency)); - } - } - } - - return transactionAmountRemaining; - } - - private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate, List installments) { - - LoanRepaymentScheduleInstallment nearest = installments.get(0); - - for (LoanRepaymentScheduleInstallment installment : installments) { - if (installment.getDueDate().isAfter(transactionDate) || installment.getDueDate().isEqual(transactionDate)) { - nearest = installment; - break; - } - } - return nearest; - } - - /** - * For normal on-time repayments, pays off interest first, then principal. - */ - @Override - protected Money handleTransactionThatIsOnTimePaymentOfInstallment( - final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed) { - - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); - - if (loanTransaction.isChargesWaiver()) { - - penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency)); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency)); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - } else if (loanTransaction.isInterestWaiver()) { - interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - } else { - - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - } - - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - return transactionAmountRemaining; - } - - @SuppressWarnings("unused") - @Override - protected void onLoanOverpayment(final LoanTransaction loanTransaction, - final Money loanOverPaymentAmount) { - // dont do anything for with loan over-payment - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java similarity index 69% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java rename to mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 7412242bc..3474c6a3c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -3,19 +3,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mifosplatform.portfolio.loanaccount.domain; +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor; +import java.util.HashSet; import java.util.List; import java.util.Set; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; /** * Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} * which is more convenient for concrete implementations to extend. * + * @see InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor + * * @see HeavensFamilyLoanRepaymentScheduleTransactionProcessor * @see CreocoreLoanRepaymentScheduleTransactionProcessor */ @@ -43,6 +55,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen for (LoanRepaymentScheduleInstallment currentInstallment : installments) { currentInstallment.resetDerivedComponents(); + currentInstallment.updateDerivedFields(currency, disbursementDate); } // re-process loan charges over repayment periods (picking up on waived @@ -108,7 +121,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen for (LoanRepaymentScheduleInstallment currentInstallment : installments) { - if (currentInstallment.isNotFullyCompleted()) { + if (currentInstallment.isNotFullyPaidOff()) { // is this transaction early/late/on-time with respect to the // current installment? @@ -130,15 +143,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen installmentIndex++; } + Set loanFees = extractFeeCharges(charges); + Set loanPenalties = extractPenaltyCharges(charges); + if (loanTransaction.isNotWaiver()) { Money feeCharges = loanTransaction.getFeeChargesPortion(currency); if (feeCharges.isGreaterThanZero()) { - updateFeeChargesPaidAmountBy(feeCharges, charges); + updateFeeChargesPaidAmountBy(loanTransaction, feeCharges, loanFees); } Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); if (penaltyCharges.isGreaterThanZero()) { - updatePenaltyChargesPaidAmountBy(penaltyCharges, charges); + updatePenaltyChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties); } } @@ -147,28 +163,61 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } - private void updateFeeChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private Set extractFeeCharges(Set loanCharges) { + final Set feeCharges = new HashSet(); + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isFeeCharge()) { + feeCharges.add(loanCharge); + } + } + return feeCharges; + } + + private Set extractPenaltyCharges(Set loanCharges) { + final Set penaltyCharges = new HashSet(); + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isPenaltyCharge()) { + penaltyCharges.add(loanCharge); + } + } + return penaltyCharges; + } + + private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { if (!loanCharge.isDueAtDisbursement()) { if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } } - private void updatePenaltyChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, + final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { if (!loanCharge.isDueAtDisbursement()) { - if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) { + if (loanCharge.isPenaltyCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } @@ -192,6 +241,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen public void handleWriteOff(final LoanTransaction loanTransaction, final MonetaryCurrency currency, final List installments) { + LocalDate transactionDate = loanTransaction.getTransactionDate(); Money principalPortion = Money.zero(currency); Money interestPortion = Money.zero(currency); Money feeChargesPortion = Money.zero(currency); @@ -201,11 +251,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen // principal, interest and charges for (LoanRepaymentScheduleInstallment currentInstallment : installments) { - if (currentInstallment.isNotFullyCompleted()) { - principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(currency)); - interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(currency)); - feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(currency)); - penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.writeOffOutstandingPenaltyCharges(currency)); + if (currentInstallment.isNotFullyPaidOff()) { + principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(transactionDate, currency)); + interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(transactionDate, currency)); + feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(transactionDate, currency)); + penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.writeOffOutstandingPenaltyCharges(transactionDate, + currency)); } } @@ -238,9 +289,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen /** * This method is responsible for checking if the current transaction is 'an * advance/early payment' based on the details passed through. + * + * Default implementation is check transaction date is before installment + * due date. */ - protected abstract boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, - final List installments, final LocalDate transactionDate, final Money transactionAmount); + protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, + final List installments, final LocalDate transactionDate, + @SuppressWarnings("unused") final Money transactionAmount) { + + LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); + + return transactionDate.isBefore(currentInstallment.getDueDate()); + } /** * For early/'in advance' repayments. @@ -261,5 +321,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen * transaction amount is greater than the total expected principal and * interest of the loan. */ - protected abstract void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount); + @SuppressWarnings("unused") + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // empty implementation by default. + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java similarity index 72% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessor.java rename to mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index c659b9bed..b8f6a12ac 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -3,13 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mifosplatform.portfolio.loanaccount.domain; +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor; import java.util.List; import java.util.Set; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; public interface LoanRepaymentScheduleTransactionProcessor { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..3b12ab0b8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,123 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; + +/** + * Creocore style {@link LoanRepaymentScheduleTransactionProcessor}. + * + * For standard transactions, pays off components in order of interest, then + * principal. + * + * If a transaction results in an advance payment or over-payment for a given + * installment, the over paid amount is pay off on the principal component of + * subsequent installments. + * + * If the entire principal of an installment is paid in advance then the + * interest component is waived. + */ +public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { + + /** + * For creocore, early is defined as any date before the installment due + * date + */ + @SuppressWarnings("unused") + @Override + protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, + final List installments, final LocalDate transactionDate, final Money transactionAmount) { + + LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); + + return transactionDate.isBefore(currentInstallment.getDueDate()); + } + + /** + * For early/'in advance' repayments, pay off in the same way as on-time + * payments, interest first then principal. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final LocalDate transactionDate, final Money paymentInAdvance) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); + } + + /** + * For late repayments, pay off in the same way as on-time payments, + * interest first then principal. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed); + } + + /** + * For normal on-time repayments, pays off interest first, then principal. + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (loanTransaction.isChargesWaiver()) { + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, + loanTransaction.getPenaltyChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment + .waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } else { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + return transactionAmountRemaining; + } + + @SuppressWarnings("unused") + @Override + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // dont do anything for with loan over-payment + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java similarity index 82% rename from mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java rename to mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java index 52dc3abd9..dec0efdbb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -3,13 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mifosplatform.portfolio.loanaccount.domain; +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; import java.util.List; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; /** * Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}. @@ -55,7 +59,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst lastInstallmentDueDate = previousInstallment.getDueDate(); isInAdvance = !(transactionDate.isAfter(lastInstallmentDueDate) || (transactionDate.isEqual(lastInstallmentDueDate))); - + return isInAdvance; } @@ -75,16 +79,16 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst Money penaltyChargesPortion = Money.zero(currency); if (loanTransaction.isInterestWaiver()) { - interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); } else { if (currentInstallment.isPrincipalNotCompleted(currency)) { - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); if (currentInstallment.isPrincipalCompleted(currency)) { // FIXME - KW - if auto waiving interest need to create // another transaction to handle this. - currentInstallment.waiveInterestComponent(currentInstallment.getInterestCharged(currency)); + currentInstallment.waiveInterestComponent(transactionDate, currentInstallment.getInterestCharged(currency)); } loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); @@ -93,16 +97,16 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst } // 1. pay of principal with over payment. - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); + + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); } @@ -117,6 +121,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + final LocalDate transactionDate = loanTransaction.getTransactionDate(); final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); Money transactionAmountRemaining = transactionAmountUnprocessed; Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); @@ -125,27 +130,30 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); if (loanTransaction.isChargesWaiver()) { - penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency)); + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, + loanTransaction.getPenaltyChargesPortion(currency)); transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency)); + feeChargesPortion = currentInstallment + .waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency)); transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); } else if (loanTransaction.isInterestWaiver()) { - interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining); + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); } else { // 1. pay of principal before interest. - principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining); + + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - - feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - - penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining); + + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..c969f7ea1 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,92 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; + +/** + * This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the + * payment order of Interest first, then principal, penalties and fees. + */ +public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor extends + AbstractLoanRepaymentScheduleTransactionProcessor { + + /** + * For early/'in advance' repayments, pay off in the same way as on-time + * payments, interest first, principal, penalties and charges. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final LocalDate transactionDate, final Money paymentInAdvance) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); + } + + /** + * For late repayments, pay off in the same way as on-time payments, + * interest first then principal. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed); + } + + /** + * For normal on-time repayments, pays off interest first, then principal. + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (loanTransaction.isChargesWaiver()) { + // zero this type of transaction and ignore it for now. + transactionAmountRemaining = Money.zero(currency); + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else { + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } + + return transactionAmountRemaining; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..5e21a8f17 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/MifosStyleLoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,111 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; + +/** + * Old Mifos style {@link LoanRepaymentScheduleTransactionProcessor}. + * + * For ALL types of transactions, pays off components in order of interest, then + * principal. + * + * Other formulas exist on mifos where you can choose 'Declining-Balance + * Interest Recalculation' which simply means, recalculate the interest + * component based on the how much principal is outstanding at a point in time; + * but this isnt trying to model that option only the basic one for now. + */ +@SuppressWarnings("unused") +public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { + + @Override + protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, + final List installments, final LocalDate transactionDate, final Money transactionAmount) { + + LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); + + return transactionDate.isBefore(currentInstallment.getDueDate()); + } + + /** + * For early/'in advance' repayments, pay off in the same way as on-time + * payments, interest first then principal. + */ + @Override + protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final LocalDate transactionDate, final Money paymentInAdvance) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); + } + + /** + * For late repayments, pay off in the same way as on-time payments, + * interest first then principal. + */ + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed); + } + + /** + * For normal on-time repayments, pays off interest first, then principal. + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (loanTransaction.isChargesWaiver()) { + // zero this type of transaction and ignore it for now. + transactionAmountRemaining = Money.zero(currency); + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } + + return transactionAmountRemaining; + } + + @Override + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // TODO - KW - dont do anything with loan over-payment for now + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..f49774501 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,92 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; + +/** + * This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the + * payment order of principal first, then interest, penalties and fees. + */ +public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor extends + AbstractLoanRepaymentScheduleTransactionProcessor { + + /** + * For early/'in advance' repayments, pay off in the same way as on-time + * payments, interest first, principal, penalties and charges. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final LocalDate transactionDate, final Money paymentInAdvance) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); + } + + /** + * For late repayments, pay off in the same way as on-time payments, + * interest first then principal. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed); + } + + /** + * For normal on-time repayments, pays off interest first, then principal. + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (loanTransaction.isChargesWaiver()) { + // zero this type of transaction and ignore it for now. + transactionAmountRemaining = Money.zero(currency); + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else { + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } + + return transactionAmountRemaining; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java new file mode 100644 index 000000000..019624122 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java @@ -0,0 +1,188 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl; + +import java.util.List; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; + +/** + * Adhikar/RBI style {@link LoanRepaymentScheduleTransactionProcessor}. + * + * From https://mifosforge.jira.com/browse/MIFOS-5636: + * + * Per RBI regulations, all interest must be paid (both current and overdue) + * before principal is paid. + * + * For example on a loan with two installments due (one current and one overdue) + * of 220 each (200 principal + 20 interest): + * + * Partial Payment of 40 20 Payment to interest on Installment #1 (200 principal + * remaining) 20 Payment to interest on Installment #2 (200 principal remaining) + */ +public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { + + /** + * For creocore, early is defined as any date before the installment due + * date + */ + @SuppressWarnings("unused") + @Override + protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, + final List installments, final LocalDate transactionDate, final Money transactionAmount) { + + LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); + + return transactionDate.isBefore(currentInstallment.getDueDate()); + } + + /** + * For early/'in advance' repayments, pays off principal component only. + */ + @SuppressWarnings("unused") + @Override + protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final LocalDate transactionDate, final Money paymentInAdvance) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance); + } + + /** + * For late repayments, pay off in the same way as on-time payments, + * interest first then principal. + */ + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed) { + + // pay of overdue and current interest due given transaction date + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money interestWaivedPortion = Money.zero(currency); + Money feeChargesPortion = Money.zero(currency); + Money penaltyChargesPortion = Money.zero(currency); + + if (loanTransaction.isInterestWaiver()) { + interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion); + + final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + loanTransaction.updateComponents(principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion); + } else { + + LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment( + loanTransaction.getTransactionDate(), installments); + + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.isInterestDue(currency) + && (installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals( + currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))) { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + Money interestPortion = installment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + Money principalPortion = Money.zero(currency); + loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency)); + } + } + + // With whatever is remaining, pay off principal components of + // installments + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) { + Money principalPortion = installment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + Money interestPortion = Money.zero(currency); + loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency)); + } + } + } + + return transactionAmountRemaining; + } + + private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate, + final List installments) { + + LoanRepaymentScheduleInstallment nearest = installments.get(0); + + for (LoanRepaymentScheduleInstallment installment : installments) { + if (installment.getDueDate().isAfter(transactionDate) || installment.getDueDate().isEqual(transactionDate)) { + nearest = installment; + break; + } + } + return nearest; + } + + /** + * For normal on-time repayments, pays off interest first, then principal. + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency()); + + if (loanTransaction.isChargesWaiver()) { + + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, + loanTransaction.getPenaltyChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment + .waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } else { + + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + } + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + return transactionAmountRemaining; + } + + @SuppressWarnings("unused") + @Override + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // dont do anything for with loan over-payment + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java index aa8e6076f..baa33502e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanScheduleData.java @@ -38,6 +38,8 @@ public class LoanScheduleData { private final BigDecimal totalWrittenOff; private final BigDecimal totalRepaymentExpected; private final BigDecimal totalRepayment; + private final BigDecimal totalPaidInAdvance; + private final BigDecimal totalPaidLate; private final BigDecimal totalOutstanding; /** @@ -51,7 +53,8 @@ public class LoanScheduleData { final BigDecimal totalPrincipalDisbursed, final BigDecimal totalPrincipalExpected, final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalRepaymentExpected, - final BigDecimal totalRepayment, final BigDecimal totalOutstanding) { + final BigDecimal totalRepayment, final BigDecimal totalPaidInAdvance, final BigDecimal totalPaidLate, + final BigDecimal totalOutstanding) { this.currency = currency; this.periods = periods; this.loanTermInDays = loanTermInDays; @@ -65,6 +68,8 @@ public class LoanScheduleData { this.totalWrittenOff = totalWrittenOff; this.totalRepaymentExpected = totalRepaymentExpected; this.totalRepayment = totalRepayment; + this.totalPaidInAdvance = totalPaidInAdvance; + this.totalPaidLate = totalPaidLate; this.totalOutstanding = totalOutstanding; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index 17bed9155..0c7ba56cf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -19,6 +19,10 @@ public class LoanSchedulePeriodData { private final Integer period; private final LocalDate fromDate; private final LocalDate dueDate; + @SuppressWarnings("unused") + private final LocalDate obligationsMetOnDate; + @SuppressWarnings("unused") + private final Boolean complete; private final Integer daysInPeriod; private final BigDecimal principalDisbursed; @SuppressWarnings("unused") @@ -54,6 +58,10 @@ public class LoanSchedulePeriodData { @SuppressWarnings("unused") private final BigDecimal totalPaidForPeriod; @SuppressWarnings("unused") + private final BigDecimal totalPaidInAdvanceForPeriod; + @SuppressWarnings("unused") + private final BigDecimal totalPaidLateForPeriod; + @SuppressWarnings("unused") private final BigDecimal totalWaivedForPeriod; @SuppressWarnings("unused") private final BigDecimal totalWrittenOffForPeriod; @@ -79,22 +87,25 @@ public class LoanSchedulePeriodData { } public static LoanSchedulePeriodData repaymentPeriodWithPayments(@SuppressWarnings("unused") final Long loanId, - final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principalOriginalDue, - final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding, + final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final LocalDate obligationsMetOnDate, + final boolean complete, final BigDecimal principalOriginalDue, final BigDecimal principalPaid, + final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding, final BigDecimal outstandingPrincipalBalanceOfLoan, final BigDecimal interestDueOnPrincipalOutstanding, final BigDecimal interestPaid, final BigDecimal interestWaived, final BigDecimal interestWrittenOff, final BigDecimal interestOutstanding, final BigDecimal feeChargesDue, final BigDecimal feeChargesPaid, final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff, final BigDecimal feeChargesOutstanding, final BigDecimal penaltyChargesDue, final BigDecimal penaltyChargesPaid, final BigDecimal penaltyChargesWaived, final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding, final BigDecimal totalDueForPeriod, - final BigDecimal totalPaid, final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding, + final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod, final BigDecimal totalPaidLateForPeriod, + final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding, final BigDecimal totalActualCostOfLoanForPeriod) { - return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, principalOriginalDue, principalPaid, principalWrittenOff, - principalOutstanding, outstandingPrincipalBalanceOfLoan, interestDueOnPrincipalOutstanding, interestPaid, interestWaived, - interestWrittenOff, interestOutstanding, feeChargesDue, feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, - feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, - penaltyChargesOutstanding, totalDueForPeriod, totalPaid, totalWaived, totalWrittenOff, totalOutstanding, + return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, obligationsMetOnDate, complete, principalOriginalDue, + principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan, + interestDueOnPrincipalOutstanding, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesDue, + feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid, + penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid, + totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding, totalActualCostOfLoanForPeriod); } @@ -107,6 +118,8 @@ public class LoanSchedulePeriodData { this.period = periodNumber; this.fromDate = fromDate; this.dueDate = dueDate; + this.obligationsMetOnDate = null; + this.complete = null; if (fromDate != null) { this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays(); } else { @@ -149,6 +162,8 @@ public class LoanSchedulePeriodData { this.totalOriginalDueForPeriod = chargesDueAtTimeOfDisbursement; this.totalDueForPeriod = chargesDueAtTimeOfDisbursement; this.totalPaidForPeriod = this.feeChargesPaid; + this.totalPaidInAdvanceForPeriod = null; + this.totalPaidLateForPeriod = null; this.totalWaivedForPeriod = null; this.totalWrittenOffForPeriod = null; this.totalOutstandingForPeriod = this.feeChargesOutstanding; @@ -160,6 +175,10 @@ public class LoanSchedulePeriodData { } } + /* + * used for repayment only period when creating an empty loan schedule for + * preview etc + */ private LoanSchedulePeriodData(final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principalOriginalDue, final BigDecimal principalOutstanding, final BigDecimal interestDueOnPrincipalOutstanding, final BigDecimal feeChargesDueForPeriod, @@ -167,6 +186,8 @@ public class LoanSchedulePeriodData { this.period = periodNumber; this.fromDate = fromDate; this.dueDate = dueDate; + this.obligationsMetOnDate = null; + this.complete = null; if (fromDate != null) { this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays(); } else { @@ -202,6 +223,8 @@ public class LoanSchedulePeriodData { this.totalOriginalDueForPeriod = totalDueForPeriod; this.totalDueForPeriod = totalDueForPeriod; this.totalPaidForPeriod = BigDecimal.ZERO; + this.totalPaidInAdvanceForPeriod = null; + this.totalPaidLateForPeriod = null; this.totalWaivedForPeriod = null; this.totalWrittenOffForPeriod = null; this.totalOutstandingForPeriod = totalDueForPeriod; @@ -219,18 +242,22 @@ public class LoanSchedulePeriodData { * principal, interest & charges along with what portion of each is paid. */ private LoanSchedulePeriodData(final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, - final BigDecimal principalOriginalDue, final BigDecimal principalPaid, final BigDecimal principalWrittenOff, - final BigDecimal principalOutstanding, final BigDecimal principalLoanBalanceOutstanding, - final BigDecimal interestDueOnPrincipalOutstanding, final BigDecimal interestPaid, final BigDecimal interestWaived, - final BigDecimal interestWrittenOff, final BigDecimal interestOutstanding, final BigDecimal feeChargesDue, - final BigDecimal feeChargesPaid, final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff, - final BigDecimal feeChargesOutstanding, final BigDecimal penaltyChargesDue, final BigDecimal penaltyChargesPaid, - final BigDecimal penaltyChargesWaived, final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding, - final BigDecimal totalDueForPeriod, final BigDecimal totalPaid, final BigDecimal totalWaived, final BigDecimal totalWrittenOff, - final BigDecimal totalOutstanding, final BigDecimal totalActualCostOfLoanForPeriod) { + final LocalDate obligationsMetOnDate, final boolean complete, final BigDecimal principalOriginalDue, + final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding, + final BigDecimal principalLoanBalanceOutstanding, final BigDecimal interestDueOnPrincipalOutstanding, + final BigDecimal interestPaid, final BigDecimal interestWaived, final BigDecimal interestWrittenOff, + final BigDecimal interestOutstanding, final BigDecimal feeChargesDue, final BigDecimal feeChargesPaid, + final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff, final BigDecimal feeChargesOutstanding, + final BigDecimal penaltyChargesDue, final BigDecimal penaltyChargesPaid, final BigDecimal penaltyChargesWaived, + final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding, final BigDecimal totalDueForPeriod, + final BigDecimal totalPaid, final BigDecimal totalPaidInAdvanceForPeriod, final BigDecimal totalPaidLateForPeriod, + final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding, + final BigDecimal totalActualCostOfLoanForPeriod) { this.period = periodNumber; this.fromDate = fromDate; this.dueDate = dueDate; + this.obligationsMetOnDate = obligationsMetOnDate; + this.complete = complete; if (fromDate != null) { this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays(); } else { @@ -266,6 +293,8 @@ public class LoanSchedulePeriodData { this.totalOriginalDueForPeriod = totalDueForPeriod; this.totalDueForPeriod = totalDueForPeriod; this.totalPaidForPeriod = totalPaid; + this.totalPaidInAdvanceForPeriod = totalPaidInAdvanceForPeriod; + this.totalPaidLateForPeriod = totalPaidLateForPeriod; this.totalWaivedForPeriod = totalWaived; this.totalWrittenOffForPeriod = totalWrittenOff; this.totalOutstandingForPeriod = totalOutstanding; @@ -286,14 +315,6 @@ public class LoanSchedulePeriodData { return value; } - public boolean isRepaymentPeriod() { - boolean isRepaymentPeriod = false; - if (principalDue != null && this.interestDue != null) { - isRepaymentPeriod = BigDecimal.ZERO.compareTo(this.principalDue) == -1 || BigDecimal.ZERO.compareTo(this.interestDue) == -1; - } - return isRepaymentPeriod; - } - public Integer periodNumber() { return this.period; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java new file mode 100644 index 000000000..8a342c5cd --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -0,0 +1,188 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.joda.time.Days; +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; + +/** + * + */ +public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); + + @Override + public LoanScheduleModel generate(final MathContext mc, final ApplicationCurrency applicationCurrency, + final LoanApplicationTerms loanApplicationTerms, final Set loanCharges) { + + // 1. generate list of proposed schedule due dates + final List scheduledDates = this.scheduledDateGenerator.generate(loanApplicationTerms); + + // 2. determine the total charges due at time of disbursement + final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); + + // 3. setup variables for tracking important facts required for loan + // schedule generation. + final Money principalDisbursed = loanApplicationTerms.getPrincipal(); + final MonetaryCurrency currency = principalDisbursed.getCurrency(); + final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); + + // variables for cumulative totals + int loanTermInDays = Integer.valueOf(0); + BigDecimal totalPrincipalExpected = BigDecimal.ZERO; + BigDecimal totalPrincipalPaid = BigDecimal.ZERO; + BigDecimal totalInterestCharged = BigDecimal.ZERO; + BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; + BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; + BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; + BigDecimal totalOutstanding = BigDecimal.ZERO; + + final Collection periods = createNewLoanScheduleListWithDisbursementDetails(numberOfRepayments, + loanApplicationTerms, chargesDueAtTimeOfDisbursement); + + // 4. Determine the total interest owed over the full loan. + final Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( + this.paymentPeriodsInOneYearCalculator, mc); + + // 5. loop over the known set of 'scheduled due dates' + LocalDate repaymentPeriodStartDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( + loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), scheduledDates); + + LocalDate startDate = loanApplicationTerms.getExpectedDisbursementDate(); + int periodNumber = 1; + Money totalCumulativePrincipal = principalDisbursed.zero(); + Money totalCumulativeInterest = principalDisbursed.zero(); + Money totalOutstandingInterestPaymentDueToGrace = principalDisbursed.zero(); + Money outstandingBalance = principalDisbursed; + for (LocalDate scheduledDueDate : scheduledDates) { + + final int daysInPeriod = Days.daysBetween(startDate, scheduledDueDate).getDays(); + + double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator + .calculatePortionOfRepaymentPeriodInterestChargingGrace(repaymentPeriodStartDate, scheduledDueDate, + loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), + loanApplicationTerms.getRepaymentEvery()); + + // 5 determine principal,interest of repayment period + PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( + this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalCumulativePrincipal, + totalCumulativeInterest, totalInterestChargedForFullLoanTerm, totalOutstandingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance, loanApplicationTerms, periodNumber, mc); + + // update cumulative fields for principal & interest + totalCumulativePrincipal = totalCumulativePrincipal.plus(principalInterestForThisPeriod.principal()); + totalCumulativeInterest = totalCumulativeInterest.plus(principalInterestForThisPeriod.interest()); + totalOutstandingInterestPaymentDueToGrace = principalInterestForThisPeriod.interestPaymentDueToGrace(); + + // 6. update outstandingLoanBlance using correct 'principalDue' + outstandingBalance = outstandingBalance.minus(principalInterestForThisPeriod.principal()); + + // 7. determine fees and penalties + final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, currency); + final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, currency); + + // 8. sum up real totalInstallmentDue from components + final Money totalInstallmentDue = principalInterestForThisPeriod.principal() // + .plus(principalInterestForThisPeriod.interest()) // + .plus(feeChargesForInstallment) // + .plus(penaltyChargesForInstallment); + + // 9. create repayment period from parts + final LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(periodNumber, startDate, + scheduledDueDate, principalInterestForThisPeriod.principal(), outstandingBalance, + principalInterestForThisPeriod.interest(), feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue); + periods.add(installment); + + // handle cumulative fields + loanTermInDays += daysInPeriod; + totalPrincipalExpected = totalPrincipalExpected.add(principalInterestForThisPeriod.principal().getAmount()); + totalInterestCharged = totalInterestCharged.add(principalInterestForThisPeriod.interest().getAmount()); + totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); + totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); + totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); + startDate = scheduledDueDate; + repaymentPeriodStartDate = startDate; + + periodNumber++; + } + + return LoanScheduleModel.from(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected, + totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, + totalOutstanding); + } + + public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, + double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, + Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, int daysInPeriod, Money outstandingBalance, + LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc); + + protected final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { + return periodNumber == numberOfRepayments; + } + + private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { + BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueAtDisbursement()) { + chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); + } + } + return chargesDueAtTimeOfDisbursement; + } + + private Collection createNewLoanScheduleListWithDisbursementDetails(final int numberOfRepayments, + final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) { + + final Collection periods = new ArrayList(numberOfRepayments + 1); + + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + loanApplicationTerms, chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + + return periods; + } + + private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, + final MonetaryCurrency monetaryCurrency) { + + Money cumulative = Money.zero(monetaryCurrency); + + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { + cumulative = cumulative.plus(loanCharge.amount()); + } + } + + return cumulative; + } + + private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, + final Set loanCharges, final MonetaryCurrency monetaryCurrency) { + + Money cumulative = Money.zero(monetaryCurrency); + + for (LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { + cumulative = cumulative.plus(loanCharge.amount()); + } + } + + return cumulative; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java deleted file mode 100644 index 4a9f11b0e..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public interface AmortizationLoanScheduleGenerator { - - LoanScheduleData generate(ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo, - LocalDate disbursementDate, LocalDate interestCalculatedFrom, BigDecimal periodInterestRateForRepaymentPeriod, - LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, List scheduledDates, Set loanCharges); -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java deleted file mode 100644 index f2030fd0b..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AmortizationLoanScheduleGeneratorFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; - -public class AmortizationLoanScheduleGeneratorFactory { - - public AmortizationLoanScheduleGenerator createGenerator(final AmortizationMethod amortizationMethod) { - - AmortizationLoanScheduleGenerator generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - - switch (amortizationMethod) { - case INVALID: - generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - break; - case EQUAL_INSTALLMENTS: - generator = new EqualInstallmentsAmortizationLoanScheduleGenerator(); - break; - case EQUAL_PRINCIPAL: - generator = new EqualPrincipalAmortizationLoanScheduleGenerator(); - break; - } - - return generator; - } - -} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java new file mode 100644 index 000000000..a6b9e95ac --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java @@ -0,0 +1,67 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.MathContext; + +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; + +/** + *

+ * Declining balance can be amortized (see {@link AmortizationMethod}) in two + * ways at present: + *

    + *
  1. Equal principal payments
  2. + *
  3. Equal installment payments
  4. + *
+ *

+ * + *

+ * When amortized using equal principal payments, the principal + * component of each installment is fixed and interest due is + * calculated from the outstanding principal balance resulting in a + * different total payment due for each installment. + *

+ * + *

+ * When amortized using equal installments, the total payment due + * for each installment is fixed and is calculated using the excel like + * pmt function. The interest due is calculated from the + * outstanding principal balance which results in a principal + * component that is total payment due minus interest due. + *

+ */ +public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { + + @Override + public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + @SuppressWarnings("unused") final Money totalCumulativeInterest, + @SuppressWarnings("unused") final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, + final int daysInPeriod, final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc) { + + PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance); + + Money interestForThisInstallment = result.interest(); + + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, daysInPeriod, + outstandingBalance, periodNumber, mc); + + // update cumulative fields for principal & interest + Money interestBroughtFowardDueToGrace = result.interestPaymentDueToGrace(); + Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment); + + // adjust if needed + principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, + totalCumulativePrincipalToDate, periodNumber); + + return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtFowardDueToGrace); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java deleted file mode 100644 index f23e11a64..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DecliningBalanceMethodLoanScheduleGenerator.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -/** - *

- * Declining balance can be amortized (see {@link AmortizationMethod}) in two ways - * at present: - *

    - *
  1. Equal principal payments
  2. - *
  3. Equal installment payments
  4. - *
- *

- * - *

- * When amortized using equal principal payments, the principal - * component of each installment is fixed and interest due is - * calculated from the outstanding principal balance resulting in a - * different total payment due for each installment. - *

- * - *

- * When amortized using equal installments, the total payment due - * for each installment is fixed and is calculated using the excel like - * pmt function. The interest due is calculated from the - * outstanding principal balance which results in a principal - * component that is total payment due minus interest due. - *

- */ -public class DecliningBalanceMethodLoanScheduleGenerator implements LoanScheduleGenerator { - - private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final AmortizationLoanScheduleGeneratorFactory amortizationLoanScheduleGeneratorFactory = new AmortizationLoanScheduleGeneratorFactory(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency applicationCurrency, final LoanProductRelatedDetail loanScheduleInfo, - final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, - final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set loanCharges) { - - // 1. generate valid set of 'due dates' based on some of the 'loan - // attributes' - final List scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate); - - final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate = this.scheduledDateGenerator - .idealDisbursementDateBasedOnFirstRepaymentDate(loanScheduleInfo, scheduledDates); - - // 2. determine the 'periodic' interest rate based on the 'repayment - // periods' so we can use - final BigDecimal periodInterestRateForRepaymentPeriod = this.periodicInterestRateCalculator.calculateFrom(loanScheduleInfo); - - // Determine with 'amortisation' approach to use - final AmortizationLoanScheduleGenerator generator = this.amortizationLoanScheduleGeneratorFactory.createGenerator(loanScheduleInfo - .getAmortizationMethod()); - - return generator.generate(applicationCurrency, loanScheduleInfo, disbursementDate, interestCalculatedFrom, - periodInterestRateForRepaymentPeriod, idealDisbursementDateBasedOnFirstRepaymentDate, scheduledDates, loanCharges); - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java index 8bfe8c39a..875fdb0b6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -18,10 +18,10 @@ public class DefaultLoanScheduleGeneratorFactory implements LoanScheduleGenerato switch (interestMethod) { case FLAT: - loanScheduleGenerator = new FlatMethodLoanScheduleGenerator(); + loanScheduleGenerator = new FlatInterestLoanScheduleGenerator(); break; case DECLINING_BALANCE: - loanScheduleGenerator = new DecliningBalanceMethodLoanScheduleGenerator(); + loanScheduleGenerator = new DecliningBalanceInterestLoanScheduleGenerator(); break; case INVALID: break; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java index 476450dd8..914697e9f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java @@ -5,98 +5,80 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; -import java.util.List; - import org.joda.time.Days; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; -public class DefaultPaymentPeriodsInOneYearCalculator implements - PaymentPeriodsInOneYearCalculator { +public class DefaultPaymentPeriodsInOneYearCalculator implements PaymentPeriodsInOneYearCalculator { - @Override - public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { + @Override + public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { - Integer paymentPeriodsInOneYear = Integer.valueOf(0); - switch (repaymentFrequencyType) { - case DAYS: - paymentPeriodsInOneYear = Integer.valueOf(365); - break; - case WEEKS: - paymentPeriodsInOneYear = Integer.valueOf(52); - break; - case MONTHS: - paymentPeriodsInOneYear = Integer.valueOf(12); - break; - case YEARS: - paymentPeriodsInOneYear = Integer.valueOf(1); - break; - case INVALID: - paymentPeriodsInOneYear = Integer.valueOf(0); - break; - } - return paymentPeriodsInOneYear; - } + Integer paymentPeriodsInOneYear = Integer.valueOf(0); + switch (repaymentFrequencyType) { + case DAYS: + paymentPeriodsInOneYear = Integer.valueOf(365); + break; + case WEEKS: + paymentPeriodsInOneYear = Integer.valueOf(52); + break; + case MONTHS: + paymentPeriodsInOneYear = Integer.valueOf(12); + break; + case YEARS: + paymentPeriodsInOneYear = Integer.valueOf(1); + break; + case INVALID: + paymentPeriodsInOneYear = Integer.valueOf(0); + break; + } + return paymentPeriodsInOneYear; + } - /** - * calculates the number of grace repayment periods and can be a fraction of a repayment period. - */ - @Override - public double calculateRepaymentPeriodAsAFractionOfDays( - PeriodFrequencyType repaymentPeriodFrequencyType, - Integer every, LocalDate interestCalculatedFrom, - List scheduledDates, LocalDate disbursementDate) { - - Double periodFraction = Double.valueOf("0"); - - if (interestCalculatedFrom != null && !interestCalculatedFrom.isBefore(disbursementDate)) { - Integer repaymentPeriodIndex = 0; - LocalDate lastRepaymentPeriod = disbursementDate; - for (LocalDate repaymentPeriodDueDate : scheduledDates) { - if (interestCalculatedFrom.isBefore(repaymentPeriodDueDate)) { - // fraction is - int numberOfDaysInterestCalculationGraceInPeriod = Days - .daysBetween( - lastRepaymentPeriod.toDateMidnight() - .toDateTime(), - interestCalculatedFrom.toDateMidnight() - .toDateTime()).getDays(); - periodFraction = calculateRepaymentPeriodFraction( - repaymentPeriodFrequencyType, every, - numberOfDaysInterestCalculationGraceInPeriod); - periodFraction = periodFraction - + repaymentPeriodIndex.doubleValue(); - break; - } - repaymentPeriodIndex++; - lastRepaymentPeriod = repaymentPeriodDueDate; - } - } - - return periodFraction; - } - - private double calculateRepaymentPeriodFraction(PeriodFrequencyType repaymentPeriodFrequencyType, Integer every, Integer numberOfDaysInterestCalculationGrace) { - - Double fraction = Double.valueOf("0"); - switch (repaymentPeriodFrequencyType) { - case DAYS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); - break; - case WEEKS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue()); - break; - case MONTHS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue()); - break; - case YEARS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue()); - break; - case INVALID: - fraction = Double.valueOf("0"); - break; - } - return fraction; - } + @Override + public double calculatePortionOfRepaymentPeriodInterestChargingGrace(final LocalDate repaymentPeriodStartDate, + final LocalDate scheduledDueDate, final LocalDate interestChargedFromLocalDate, + final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer repaidEvery) { -} + Double periodFraction = Double.valueOf("0.0"); + + LocalDateInterval repaymentPeriod = new LocalDateInterval(repaymentPeriodStartDate, scheduledDueDate); + + if (interestChargedFromLocalDate != null && repaymentPeriod.fallsBefore(interestChargedFromLocalDate.plusDays(1))) { + periodFraction = Double.valueOf("1.0"); + } else if (interestChargedFromLocalDate != null && repaymentPeriod.contains(interestChargedFromLocalDate)) { + + int numberOfDaysInterestCalculationGraceInPeriod = Days.daysBetween(repaymentPeriodStartDate, interestChargedFromLocalDate) + .getDays(); + periodFraction = calculateRepaymentPeriodFraction(repaymentPeriodFrequencyType, repaidEvery, + numberOfDaysInterestCalculationGraceInPeriod); + } + + return periodFraction; + } + + private double calculateRepaymentPeriodFraction(final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer every, + final Integer numberOfDaysInterestCalculationGrace) { + + Double fraction = Double.valueOf("0"); + switch (repaymentPeriodFrequencyType) { + case DAYS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); + break; + case WEEKS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("7.0") * every.doubleValue()); + break; + case MONTHS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("30.0") * every.doubleValue()); + break; + case YEARS: + fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.valueOf("365.0") * every.doubleValue()); + break; + case INVALID: + fraction = Double.valueOf("0"); + break; + } + return fraction; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 95cb12b45..575ee2c00 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -9,46 +9,47 @@ import java.util.ArrayList; import java.util.List; import org.joda.time.LocalDate; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { @Override - public List generate(final LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, final LocalDate disbursementDate, final LocalDate firstRepaymentPeriodDate) { + public List generate(final LoanApplicationTerms loanApplicationTerms) { - List dueRepaymentPeriodDates = new ArrayList(loanScheduleDateInfo.getNumberOfRepayments()); + final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); - LocalDate startDate = disbursementDate; + final List dueRepaymentPeriodDates = new ArrayList(numberOfRepayments); - for (int repaymentPeriod = 1; repaymentPeriod <= loanScheduleDateInfo.getNumberOfRepayments(); repaymentPeriod++) { + LocalDate startDate = loanApplicationTerms.getExpectedDisbursementDate(); + final LocalDate firstRepaymentPeriodDate = loanApplicationTerms.getRepaymentsStartingFromLocalDate(); + + for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { LocalDate dueRepaymentPeriodDate = startDate; - - if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { - dueRepaymentPeriodDate = firstRepaymentPeriodDate; - } else { - switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { - case DAYS: - dueRepaymentPeriodDate = startDate.plusDays(loanScheduleDateInfo - .getRepayEvery()); - break; - case WEEKS: - dueRepaymentPeriodDate = startDate.plusWeeks(loanScheduleDateInfo - .getRepayEvery()); - break; - case MONTHS: - dueRepaymentPeriodDate = startDate.plusMonths(loanScheduleDateInfo - .getRepayEvery()); - break; - case YEARS: - dueRepaymentPeriodDate = startDate.plusYears(loanScheduleDateInfo - .getRepayEvery()); - break; - case INVALID: - break; - } - } + + if (repaymentPeriod == 1 && firstRepaymentPeriodDate != null) { + dueRepaymentPeriodDate = firstRepaymentPeriodDate; + } else { + + final int repaidEvery = loanApplicationTerms.getRepaymentEvery(); + + switch (loanApplicationTerms.getRepaymentPeriodFrequencyType()) { + case DAYS: + dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); + break; + case WEEKS: + dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); + break; + case MONTHS: + dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); + break; + case YEARS: + dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); + break; + case INVALID: + break; + } + } dueRepaymentPeriodDates.add(dueRepaymentPeriodDate); startDate = dueRepaymentPeriodDate; @@ -57,35 +58,31 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { return dueRepaymentPeriodDates; } - @Override - public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( - LoanProductRelatedDetail loanScheduleDateInfo, - List scheduledDates) { - - LocalDate firstRepaymentDate = scheduledDates.get(0); - - LocalDate idealDisbursementDate = null; - - switch (loanScheduleDateInfo.getRepaymentPeriodFrequencyType()) { - case DAYS: - idealDisbursementDate = firstRepaymentDate.minusDays(loanScheduleDateInfo.getRepayEvery()); - break; - case WEEKS: - idealDisbursementDate = firstRepaymentDate.minusWeeks(loanScheduleDateInfo - .getRepayEvery()); - break; - case MONTHS: - idealDisbursementDate = firstRepaymentDate.minusMonths(loanScheduleDateInfo - .getRepayEvery()); - break; - case YEARS: - idealDisbursementDate = firstRepaymentDate.minusYears(loanScheduleDateInfo - .getRepayEvery()); - break; - case INVALID: - break; - } - - return idealDisbursementDate; - } + @Override + public LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(final PeriodFrequencyType repaymentPeriodFrequencyType, + final int repaidEvery, final List scheduledDates) { + + LocalDate firstRepaymentDate = scheduledDates.get(0); + + LocalDate idealDisbursementDate = null; + + switch (repaymentPeriodFrequencyType) { + case DAYS: + idealDisbursementDate = firstRepaymentDate.minusDays(repaidEvery); + break; + case WEEKS: + idealDisbursementDate = firstRepaymentDate.minusWeeks(repaidEvery); + break; + case MONTHS: + idealDisbursementDate = firstRepaymentDate.minusMonths(repaidEvery); + break; + case YEARS: + idealDisbursementDate = firstRepaymentDate.minusYears(repaidEvery); + break; + case INVALID: + break; + } + + return idealDisbursementDate; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java deleted file mode 100644 index 2114147fd..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualInstallmentsAmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,190 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class EqualInstallmentsAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator { - - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - private final PmtCalculator pmtCalculator = new PmtCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final LocalDate disbursementDate, final LocalDate interestCalculatedFrom, - final BigDecimal periodInterestRateForRepaymentPeriod, final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, - final List scheduledDates, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - // determine 'total payment' for each repayment based on pmt function - // (and hence the total due overall) - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - final Money totalDuePerInstallment = this.pmtCalculator.calculatePaymentForOnePeriodFrom(loanScheduleInfo, - periodInterestRateForRepaymentPeriod, monetaryCurrency); - final Money totalRepaymentDueForLoanTerm = this.pmtCalculator.calculateTotalRepaymentFrom(loanScheduleInfo, - periodInterestRateForRepaymentPeriod, monetaryCurrency); - - Money totalInterestDue = totalRepaymentDueForLoanTerm.minus(loanScheduleInfo.getPrincipal()); - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(monetaryCurrency); - Money totalInterest = Money.zero(monetaryCurrency); - - double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator - .calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), - loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, - idealDisbursementDateBasedOnFirstRepaymentDate); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate, scheduledDueDate).getDays(); - - Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, - periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo); - Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, - interestForInstallment, loanScheduleInfo); - - if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment; - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - - Integer.valueOf(1).doubleValue(); - } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") - && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment - .multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - totalInterestDue = totalInterestDue.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); - } - - totalPrincipal = totalPrincipal.plus(principalForInstallment); - totalInterest = totalInterest.plus(interestForInstallment); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (principalDifference.isLessThanZero()) { - principalForInstallment = principalForInstallment.plus(principalDifference.abs()); - } else if (principalDifference.isGreaterThanZero()) { - principalForInstallment = principalForInstallment.minus(principalDifference.abs()); - } - - final Money interestDifference = totalInterest.minus(totalInterestDue); - if (interestDifference.isLessThanZero()) { - interestForInstallment = interestForInstallment.plus(interestDifference.abs()); - } else if (interestDifference.isGreaterThanZero()) { - interestForInstallment = interestForInstallment.minus(interestDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalForInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - final Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalForInstallment.getAmount(), outstandingBalance.getAmount(), interestForInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalForInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestForInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java deleted file mode 100644 index 6766ce2f3..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/EqualPrincipalAmortizationLoanScheduleGenerator.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class EqualPrincipalAmortizationLoanScheduleGenerator implements AmortizationLoanScheduleGenerator { - - private final PeriodicInterestRateCalculator periodicInterestRateCalculator = new PeriodicInterestRateCalculator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final LocalDate disbursementDate, final LocalDate interestCalculatedFrom, - final BigDecimal periodInterestRateForRepaymentPeriod, final LocalDate idealDisbursementDateBasedOnFirstRepaymentDate, - final List scheduledDates, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - // 1. we know 'principal' must be equal over repayments so use this to - // calculate interest and thus total repayment due. - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - Money totalDuePerInstallment = Money.zero(monetaryCurrency); - - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(monetaryCurrency); - Money totalInterest = Money.zero(monetaryCurrency); - - double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator - .calculateRepaymentPeriodAsAFractionOfDays(loanScheduleInfo.getRepaymentPeriodFrequencyType(), - loanScheduleInfo.getRepayEvery(), interestCalculatedFrom, scheduledDates, - idealDisbursementDateBasedOnFirstRepaymentDate); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()) - .getDays(); - - Money interestForInstallment = this.periodicInterestRateCalculator.calculateInterestOn(outstandingBalance, - periodInterestRateForRepaymentPeriod, daysInPeriod, loanScheduleInfo); - Money principalForInstallment = this.periodicInterestRateCalculator.calculatePrincipalOn(totalDuePerInstallment, - interestForInstallment, loanScheduleInfo); - - if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment; - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction - - Integer.valueOf(1).doubleValue(); - } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") - && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { - Money graceOnInterestForRepaymentPeriod = interestForInstallment - .multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); - interestForInstallment = interestForInstallment.minus(graceOnInterestForRepaymentPeriod); - interestCalculationGraceOnRepaymentPeriodFraction = Double.valueOf("0"); - } - - totalPrincipal = totalPrincipal.plus(principalForInstallment); - totalInterest = totalInterest.plus(interestForInstallment); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - Money principalDifference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (principalDifference.isLessThanZero()) { - principalForInstallment = principalForInstallment.plus(principalDifference.abs()); - } else if (principalDifference.isGreaterThanZero()) { - principalForInstallment = principalForInstallment.minus(principalDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalForInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - - final Money totalInstallmentDue = principalForInstallment.plus(interestForInstallment).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalForInstallment.getAmount(), outstandingBalance.getAmount(), interestForInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalForInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestForInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java new file mode 100644 index 000000000..f24bbbab2 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FinanicalFunctions.java @@ -0,0 +1,41 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +public class FinanicalFunctions { + + /** + * PMT calculates a fixed monthly payment to be paid by borrower every + * 'period' to ensure loan is paid off in full (with interest). + * + * This monthly payment c depends upon the monthly interest rate r + * (expressed as a fraction, not a percentage, i.e., divide the quoted + * yearly percentage rate by 100 and by 12 to obtain the monthly interest + * rate), the number of monthly payments N called the loan's term, and the + * amount borrowed P known as the loan's principal; c is given by the + * formula: + * + * c = (r / (1 - (1 + r)^-N))P + * + * @param interestRateFraction + * @param numberOfPayments + * @param principal + * @param futureValue + * @param type + */ + public static double pmt(final double interestRateFraction, final double numberOfPayments, final double principal, + final double futureValue, final boolean type) { + double payment = 0; + if (interestRateFraction == 0) { + payment = -1 * (futureValue + principal) / numberOfPayments; + } else { + double r1 = interestRateFraction + 1; + payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction + / ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments))); + } + return payment; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java new file mode 100644 index 000000000..50ed49518 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java @@ -0,0 +1,43 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.MathContext; + +import org.mifosplatform.organisation.monetary.domain.Money; + +public class FlatInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { + + @Override + public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + final Money totalCumulativeInterest, final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, + final int daysInPeriod, final Money outstandingBalance, + final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc) { + + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, daysInPeriod, + outstandingBalance, periodNumber, mc); + + PrincipalInterest result = loanApplicationTerms.calculateTotalInterestForPeriod(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestPaymentDueToGrace, daysInPeriod, + outstandingBalance); + Money interestForThisInstallment = result.interest(); + + // update cumulative fields for principal & interest + Money interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); + Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment); + Money totalCumulativeInterestToDate = totalCumulativeInterest.plus(interestForThisInstallment); + + // adjust if needed + principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, + totalCumulativePrincipalToDate, periodNumber); + + interestForThisInstallment = loanApplicationTerms.adjustInterestIfLastRepaymentPeriod(interestForThisInstallment, + totalCumulativeInterestToDate, totalInterestDueForLoan, periodNumber); + + return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtForwardDueToGrace); + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java deleted file mode 100644 index 9244aea8c..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/FlatMethodLoanScheduleGenerator.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import org.joda.time.Days; -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator { - - private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - @Override - public LoanScheduleData generate(final ApplicationCurrency currency, final LoanProductRelatedDetail loanScheduleInfo, - final Integer loanTermFrequency, final PeriodFrequencyType loanTermFrequencyType, final LocalDate disbursementDate, - final LocalDate firstRepaymentDate, final LocalDate interestCalculatedFrom, final Set loanCharges) { - - final Collection periods = new ArrayList(); - - final List scheduledDates = this.scheduledDateGenerator.generate(loanScheduleInfo, disbursementDate, firstRepaymentDate); - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - BigDecimal loanTermPeriodsInYear = BigDecimal.valueOf(this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType)); - BigDecimal interestRateForLoanTerm = loanScheduleInfo.getAnnualNominalInterestRate().divide(loanTermPeriodsInYear, mc) - .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc).multiply(BigDecimal.valueOf(loanTermFrequency)); - - final MonetaryCurrency monetaryCurrency = loanScheduleInfo.getPrincipal().getCurrency(); - Money totalInterestForLoanTerm = loanScheduleInfo.getPrincipal().multiplyRetainScale(interestRateForLoanTerm, - RoundingMode.HALF_EVEN); - - Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(loanScheduleInfo.getNumberOfRepayments()), - RoundingMode.HALF_EVEN); - - Money principalPerInstallment = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(), - RoundingMode.HALF_EVEN); - - Money outstandingBalance = loanScheduleInfo.getPrincipal(); - Money principalDisbursed = loanScheduleInfo.getPrincipal(); - Money totalPrincipal = Money.zero(outstandingBalance.getCurrency()); - Money totalInterest = Money.zero(outstandingBalance.getCurrency()); - - BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueAtDisbursement()) { - chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); - } - } - - // create entries of disbursement period on loan schedule - final LoanSchedulePeriodData disbursementPeriod = LoanSchedulePeriodData.disbursementOnlyPeriod(disbursementDate, - principalDisbursed.getAmount(), chargesDueAtTimeOfDisbursement, false); - periods.add(disbursementPeriod); - - int loanTermInDays = Integer.valueOf(0); - BigDecimal totalPrincipalDisbursed = principalDisbursed.getAmount(); - BigDecimal totalPrincipalExpected = BigDecimal.ZERO; - BigDecimal totalPrincipalPaid = BigDecimal.ZERO; - BigDecimal totalInterestCharged = BigDecimal.ZERO; - BigDecimal totalFeeChargesCharged = chargesDueAtTimeOfDisbursement; - BigDecimal totalPenaltyChargesCharged = BigDecimal.ZERO; - BigDecimal totalWaived = BigDecimal.ZERO; - BigDecimal totalWrittenOff = BigDecimal.ZERO; - BigDecimal totalRepaymentExpected = chargesDueAtTimeOfDisbursement; - BigDecimal totalRepayment = BigDecimal.ZERO; - BigDecimal totalOutstanding = BigDecimal.ZERO; - - LocalDate startDate = disbursementDate; - int periodNumber = 1; - for (LocalDate scheduledDueDate : scheduledDates) { - totalPrincipal = totalPrincipal.plus(principalPerInstallment); - totalInterest = totalInterest.plus(interestPerInstallment); - - // number of days from startDate to this scheduledDate - int daysInPeriod = Days.daysBetween(startDate.toDateMidnight().toDateTime(), scheduledDueDate.toDateMidnight().toDateTime()) - .getDays(); - - if (periodNumber == loanScheduleInfo.getNumberOfRepayments()) { - final Money difference = totalPrincipal.minus(loanScheduleInfo.getPrincipal()); - if (difference.isLessThanZero()) { - principalPerInstallment = principalPerInstallment.plus(difference.abs()); - } else if (difference.isGreaterThanZero()) { - principalPerInstallment = principalPerInstallment.minus(difference.abs()); - } - - final Money interestDifference = totalInterest.minus(totalInterestForLoanTerm); - if (interestDifference.isLessThanZero()) { - interestPerInstallment = interestPerInstallment.plus(interestDifference.abs()); - } else if (interestDifference.isGreaterThanZero()) { - interestPerInstallment = interestPerInstallment.minus(interestDifference.abs()); - } - } - - outstandingBalance = outstandingBalance.minus(principalPerInstallment); - - final Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(startDate, scheduledDueDate, loanCharges, monetaryCurrency); - final Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(startDate, scheduledDueDate, loanCharges, - monetaryCurrency); - - final Money totalInstallmentDue = principalPerInstallment // - .plus(interestPerInstallment) // - .plus(feeChargesForInstallment) // - .plus(penaltyChargesForInstallment); - - LoanSchedulePeriodData installment = LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, startDate, scheduledDueDate, - principalPerInstallment.getAmount(), outstandingBalance.getAmount(), interestPerInstallment.getAmount(), - feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount(), totalInstallmentDue.getAmount()); - - periods.add(installment); - - // handle cumulative fields - loanTermInDays += daysInPeriod; - totalPrincipalExpected = totalPrincipalExpected.add(principalPerInstallment.getAmount()); - totalInterestCharged = totalInterestCharged.add(interestPerInstallment.getAmount()); - totalFeeChargesCharged = totalFeeChargesCharged.add(feeChargesForInstallment.getAmount()); - totalPenaltyChargesCharged = totalPenaltyChargesCharged.add(penaltyChargesForInstallment.getAmount()); - totalRepaymentExpected = totalRepaymentExpected.add(totalInstallmentDue.getAmount()); - startDate = scheduledDueDate; - - periodNumber++; - } - - final CurrencyData currencyData = new CurrencyData(currency.getCode(), currency.getName(), - monetaryCurrency.getDigitsAfterDecimal(), currency.getDisplaySymbol(), currency.getNameCode()); - - return new LoanScheduleData(currencyData, periods, loanTermInDays, totalPrincipalDisbursed, totalPrincipalExpected, - totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalWaived, totalWrittenOff, - totalRepaymentExpected, totalRepayment, totalOutstanding); - } - - private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, - final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } - - private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, - final Set loanCharges, final MonetaryCurrency monetaryCurrency) { - - Money cumulative = Money.zero(monetaryCurrency); - - for (LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.amount()); - } - } - - return cumulative; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java new file mode 100644 index 000000000..f1711f334 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -0,0 +1,691 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Date; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; +import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; +import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; + +public final class LoanApplicationTerms { + + private final ApplicationCurrency currency; + private final Integer loanTermFrequency; + private final PeriodFrequencyType loanTermPeriodFrequencyType; + private final Integer numberOfRepayments; + private final Integer repaymentEvery; + private final PeriodFrequencyType repaymentPeriodFrequencyType; + private final AmortizationMethod amortizationMethod; + + private final InterestMethod interestMethod; + private final BigDecimal interestRatePerPeriod; + private final PeriodFrequencyType interestRatePeriodFrequencyType; + private final BigDecimal annualNominalInterestRate; + private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; + + private final Money principal; + private final LocalDate expectedDisbursementDate; + private final LocalDate repaymentsStartingFromDate; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the principal component of a + * loans repayment period (installment). + */ + private final Integer principalGrace; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the payment of interest in a + * loans repayment period (installment). + * + * Note: Interest is still calculated taking into account the full + * loan term, the interest is simply offset to a later period. + */ + private final Integer interestPaymentGrace; + + /** + * Integer representing the number of 'repayment frequencies' or + * installments where 'grace' should apply to the charging of interest in a + * loans repayment period (installment). + * + * Note: The loan is interest-free for the period of time + * indicated. + */ + private final Integer interestChargingGrace; + + /** + * Legacy method of support 'grace' on the charging of interest on a loan. + * + *

+ * For the typical structured loan, its reasonable to use an integer to + * indicate the number of 'repayment frequency' periods the 'grace' should + * apply to but for slightly irregular loans where the period between + * disbursement and the date of the 'first repayment period' isnt doest + * match the 'repayment frequency' but can be less (15days instead of 1 + * month) or more (6 weeks instead of 1 month) - The idea was to use a date + * to indicate from whence interest should be charged. + *

+ */ + private final LocalDate interestChargedFromDate; + private final Money inArrearsTolerance; + + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, + final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod, + final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod, + final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate, + final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Money principalMoney, + final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final Integer graceOnPrincipalPayment, + final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final LocalDate interestChargedFromDate, + final Money inArrearsTolerance) { + + return new LoanApplicationTerms(currency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, + repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, + annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, expectedDisbursementDate, + repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + interestChargedFromDate, inArrearsTolerance); + } + + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final LocalDate expectedDisbursementDate, + final LocalDate repaymentsStartingFromDate, final Money inArrearsTolerance, + final LoanProductRelatedDetail loanProductRelatedDetail) { + + final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); + final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); + final PeriodFrequencyType repaymentPeriodFrequencyType = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); + final AmortizationMethod amortizationMethod = loanProductRelatedDetail.getAmortizationMethod(); + final InterestMethod interestMethod = loanProductRelatedDetail.getInterestMethod(); + final BigDecimal interestRatePerPeriod = loanProductRelatedDetail.getNominalInterestRatePerPeriod(); + final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType(); + final BigDecimal annualNominalInterestRate = loanProductRelatedDetail.getAnnualNominalInterestRate(); + final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail + .getInterestCalculationPeriodMethod(); + final Money principalMoney = loanProductRelatedDetail.getPrincipal(); + + // + final Integer graceOnPrincipalPayment = 0; + final Integer graceOnInterestPayment = 0; + final Integer graceOnInterestCharged = 0; + final LocalDate interestChargedFromDate = null; + + return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, + interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, + expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance); + } + + private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, + final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, + final PeriodFrequencyType repaymentPeriodFrequencyType, final AmortizationMethod amortizationMethod, + final InterestMethod interestMethod, final BigDecimal interestRatePerPeriod, + final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal annualNominalInterestRate, + final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Money principal, + final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate, final Integer principalGrace, + final Integer interestPaymentGrace, final Integer interestChargingGrace, final LocalDate interestChargedFromDate, + final Money inArrearsTolerance) { + this.currency = currency; + this.loanTermFrequency = loanTermFrequency; + this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; + this.numberOfRepayments = numberOfRepayments; + this.repaymentEvery = repaymentEvery; + this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType; + this.amortizationMethod = amortizationMethod; + + this.interestMethod = interestMethod; + this.interestRatePerPeriod = interestRatePerPeriod; + this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType; + this.annualNominalInterestRate = annualNominalInterestRate; + this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; + + this.principal = principal; + this.expectedDisbursementDate = expectedDisbursementDate; + this.repaymentsStartingFromDate = repaymentsStartingFromDate; + + this.principalGrace = principalGrace; + this.interestPaymentGrace = interestPaymentGrace; + this.interestChargingGrace = interestChargingGrace; + this.interestChargedFromDate = interestChargedFromDate; + + this.inArrearsTolerance = inArrearsTolerance; + } + + public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, + final int periodNumber) { + + Money adjusted = principalForPeriod; + + // adjust cumulative fields for principal & interest if needed + if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + + final Money difference = totalCumulativePrincipalToDate.minus(this.principal); + if (difference.isLessThanZero()) { + adjusted = principalForPeriod.plus(difference.abs()); + } else if (difference.isGreaterThanZero()) { + adjusted = principalForPeriod.minus(difference.abs()); + } + } + + return adjusted; + } + + public Money adjustInterestIfLastRepaymentPeriod(final Money interestForThisPeriod, final Money totalCumulativeInterestToDate, + final Money totalInterestDueForLoan, final int periodNumber) { + + Money adjusted = interestForThisPeriod; + + if (isLastRepaymentPeriod(this.numberOfRepayments, periodNumber)) { + + final Money interestDifference = totalCumulativeInterestToDate.minus(totalInterestDueForLoan); + if (interestDifference.isLessThanZero()) { + adjusted = interestForThisPeriod.plus(interestDifference.abs()); + } else if (interestDifference.isGreaterThanZero()) { + adjusted = interestForThisPeriod.minus(interestDifference.abs()); + } + } + + return adjusted; + } + + /** + * Calculates the total interest to be charged on loan taking into account + * grace settings. + * + */ + public Money calculateTotalInterestCharged(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + Money totalInterestCharged = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + final Money totalInterestChargedForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + final Money totalInterestPerInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + + final Money totalGraceOnInterestCharged = totalInterestPerInstallment.multiplyRetainScale(getInterestChargingGrace(), + mc.getRoundingMode()); + + totalInterestCharged = totalInterestChargedForLoanTerm.minus(totalGraceOnInterestCharged); + break; + case DECLINING_BALANCE: + final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc); + + final Money totalRepaymentDueForLoanTerm = calculateTotalDueForEqualInstallmentLoan(periodicInterestRateForRepaymentPeriod); + + totalInterestCharged = totalRepaymentDueForLoanTerm.minus(this.principal); + break; + case INVALID: + break; + } + + return totalInterestCharged; + } + + public Money calculateTotalPrincipalForPeriod(final PaymentPeriodsInOneYearCalculator calculator, final int daysInPeriod, + final Money outstandingBalance, final int periodNumber, final MathContext mc) { + + Money principalForInstallment = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + principalForInstallment = calculateTotalPrincipalPerPeriodWithoutGrace(mc); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principalForInstallment = Money.zero(principalForInstallment.getCurrency()); + } + break; + case DECLINING_BALANCE: + switch (this.amortizationMethod) { + case EQUAL_INSTALLMENTS: + // equal installments + int periodsElapsed = periodNumber - 1; + final BigDecimal periodicInterestRateForRepaymentPeriod = periodicInterestRate(calculator, mc); + + final Money totalPmtForThisInstallment = calculateTotalDueForEqualInstallmentRepaymentPeriod( + periodicInterestRateForRepaymentPeriod, outstandingBalance, periodsElapsed); + + principalForInstallment = calculatePrincipalDueForInstallment(calculator, mc, periodNumber, + totalPmtForThisInstallment, daysInPeriod, outstandingBalance); + break; + case EQUAL_PRINCIPAL: + principalForInstallment = calculateEqualPrincipalDueForInstallment(mc, periodNumber); + break; + case INVALID: + break; + } + break; + case INVALID: + break; + } + + return principalForInstallment; + } + + public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, + final Money cumulatingInterestPaymentDueToGrace, final int daysInPeriod, final Money outstandingBalance) { + + Money interestForInstallment = this.principal.zero(); + Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); + + switch (this.interestMethod) { + case FLAT: + + switch (this.amortizationMethod) { + case EQUAL_INSTALLMENTS: + // average out outstanding interest over remaining + // instalments where interest is applicable + interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator, periodNumber, + mc); + break; + case EQUAL_PRINCIPAL: + // interest follows time-value of money and is brought + // forward to next applicable interest payment period + PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc, + interestBroughtForwardDueToGrace); + interestForInstallment = result.interest(); + interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); + break; + case INVALID: + break; + } + break; + case DECLINING_BALANCE: + + Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, + mc, daysInPeriod, outstandingBalance); + + Money interestForThisInstallmentAfterGrace = calculateDecliningInterestDueForInstallmentAfterApplyingGrace(calculator, + interestCalculationGraceOnRepaymentPeriodFraction, mc, daysInPeriod, outstandingBalance, periodNumber); + + interestForInstallment = interestForThisInstallmentAfterGrace; + if (interestForThisInstallmentAfterGrace.isGreaterThanZero()) { + interestForInstallment = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentAfterGrace); + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { + interestForInstallment = interestForThisInstallmentAfterGrace; + } else { + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentBeforeGrace); + } + break; + case INVALID: + break; + } + + return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace); + } + + private final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { + return periodNumber == numberOfRepayments; + } + + private Money calculateTotalDueForEqualInstallmentLoan(final BigDecimal periodicInterestRate) { + + final int periodsElapsed = 0; + final double paymentPerRepaymentPeriod = paymentPerPeriod(periodicInterestRate, this.principal, periodsElapsed); + final double totalRepayment = paymentPerRepaymentPeriod * this.numberOfRepayments; + + return Money.of(principal.getCurrency(), BigDecimal.valueOf(totalRepayment)); + } + + /** + * general method to calculate totalInterestDue discounting any grace + * settings + */ + private Money calculateTotalInterestDueWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + Money totalInterestDue = this.principal.zero(); + + switch (this.interestMethod) { + case FLAT: + final BigDecimal interestRateForLoanTerm = calculateFlatInterestRateForLoanTerm(calculator, mc); + + totalInterestDue = this.principal.multiplyRetainScale(interestRateForLoanTerm, mc.getRoundingMode()); + break; + case DECLINING_BALANCE: + break; + case INVALID: + break; + } + + return totalInterestDue; + } + + private BigDecimal calculateFlatInterestRateForLoanTerm(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final BigDecimal divisor = BigDecimal.valueOf(Double.valueOf("100.0")); + + final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); + final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear); + + final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.loanTermFrequency); + + return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc) + .multiply(loanTermFrequencyBigDecimal); + } + + private Money calculateTotalInterestPerInstallmentWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final Money totalInterestForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + return totalInterestForLoanTerm.dividedBy(Long.valueOf(this.numberOfRepayments), mc.getRoundingMode()); + } + + private Money calculateTotalPrincipalPerPeriodWithoutGrace(final MathContext mc) { + final int totalRepaymentsWithCapitalPayment = calculateNumberOfRepaymentsWithPrincipalPayment(); + return this.principal.dividedBy(totalRepaymentsWithCapitalPayment, mc.getRoundingMode()); + } + + private PrincipalInterest calculateTotalFlatInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, + final int periodNumber, final MathContext mc, final Money cumulatingInterestPaymentDueToGrace) { + + Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); + + Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForInstallment); + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isFirstPeriodAfterInterestPaymentGracePeriod(periodNumber)) { + interestForInstallment = cumulatingInterestPaymentDueToGrace.plus(interestForInstallment); + interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero(); + } + + return new PrincipalInterest(null, interestForInstallment, interestBroughtForwardDueToGrace); + } + + /* + * calculates the interest that should be due for a given scheduled loan + * repayment period. It takes into account GRACE periods and calculates how + * much interest is due per period by averaging the number of periods where + * interest is due and should be paid against the total known interest that + * is due without grace. + */ + private Money calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(final PaymentPeriodsInOneYearCalculator calculator, + final int periodNumber, final MathContext mc) { + + Money interestForInstallment = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else if (isInterestFreeGracePeriod(periodNumber)) { + interestForInstallment = interestForInstallment.zero(); + } else { + + final Money totalInterestForLoanTerm = calculateTotalInterestDueWithoutGrace(calculator, mc); + + Money interestPerGracePeriod = calculateTotalInterestPerInstallmentWithoutGrace(calculator, mc); + + Money totalInterestFree = interestPerGracePeriod.multipliedBy(getInterestChargingGrace()); + Money realTotalInterestForLoan = totalInterestForLoanTerm.minus(totalInterestFree); + + Integer interestPaymentDuePeriods = calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(this.numberOfRepayments); + + interestForInstallment = realTotalInterestForLoan + .dividedBy(BigDecimal.valueOf(interestPaymentDuePeriods), mc.getRoundingMode()); + } + + return interestForInstallment; + } + + private BigDecimal periodicInterestRate(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { + + final long loanTermPeriodsInOneYear = calculatePeriodsInOneYear(calculator); + + final BigDecimal divisor = BigDecimal.valueOf(Double.valueOf("100.0")); + final BigDecimal loanTermPeriodsInYearBigDecimal = BigDecimal.valueOf(loanTermPeriodsInOneYear); + + final BigDecimal loanTermFrequencyBigDecimal = BigDecimal.valueOf(this.repaymentEvery); + + return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc) + .multiply(loanTermFrequencyBigDecimal); + } + + private long calculatePeriodsInOneYear(final PaymentPeriodsInOneYearCalculator calculator) { + return calculator.calculate(this.repaymentPeriodFrequencyType).longValue(); + } + + private int calculateNumberOfRepaymentsWithPrincipalPayment() { + return this.numberOfRepayments - getPrincipalGrace(); + } + + private Integer calculateNumberOfRepaymentPeriodsWhereInterestPaymentIsDue(final Integer totalNumberOfRepaymentPeriods) { + return totalNumberOfRepaymentPeriods - Math.max(getInterestChargingGrace(), getInterestPaymentGrace()); + } + + private Integer calculateNumberOfPrincipalPaymentPeriods(final Integer totalNumberOfRepaymentPeriods) { + return totalNumberOfRepaymentPeriods - getPrincipalGrace(); + } + + private boolean isPrincipalGraceApplicableForThisPeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getPrincipalGrace(); + } + + private boolean isInterestPaymentGraceApplicableForThisPeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getInterestPaymentGrace(); + } + + private boolean isFirstPeriodAfterInterestPaymentGracePeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber == getInterestPaymentGrace() + 1; + } + + private boolean isInterestFreeGracePeriod(final int periodNumber) { + return periodNumber > 0 && periodNumber <= getInterestChargingGrace(); + } + + private Integer getPrincipalGrace() { + Integer graceOnPrincipalPayments = Integer.valueOf(0); + if (this.principalGrace != null) { + graceOnPrincipalPayments = this.principalGrace; + } + return graceOnPrincipalPayments; + } + + private Integer getInterestPaymentGrace() { + Integer graceOnInterestPayments = Integer.valueOf(0); + if (this.interestPaymentGrace != null) { + graceOnInterestPayments = this.interestPaymentGrace; + } + return graceOnInterestPayments; + } + + private Integer getInterestChargingGrace() { + Integer graceOnInterestCharged = Integer.valueOf(0); + if (this.interestChargingGrace != null) { + graceOnInterestCharged = this.interestChargingGrace; + } + return graceOnInterestCharged; + } + + private double paymentPerPeriod(final BigDecimal periodicInterestRate, final Money balance, final int periodsElapsed) { + final double futureValue = 0; + final double principalDouble = balance.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); + + final Integer periodsRemaining = this.numberOfRepayments - periodsElapsed; + + return FinanicalFunctions.pmt(periodicInterestRate.doubleValue(), periodsRemaining.doubleValue(), principalDouble, futureValue, + false); + } + + private Money calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, + final MathContext mc, final int daysInPeriod, final Money outstandingBalance) { + + Money interestDue = Money.zero(outstandingBalance.getCurrency()); + + switch (this.interestCalculationPeriodMethod) { + case DAILY: + BigDecimal dailyInterestRate = this.annualNominalInterestRate.divide(BigDecimal.valueOf(Long.valueOf(365)), mc) + .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc).multiply(BigDecimal.valueOf(this.repaymentEvery)); + + BigDecimal equivalentInterestRateForPeriod = dailyInterestRate.multiply(BigDecimal.valueOf(Long.valueOf(daysInPeriod))); + + interestDue = outstandingBalance.multiplyRetainScale(equivalentInterestRateForPeriod, mc.getRoundingMode()); + break; + case SAME_AS_REPAYMENT_PERIOD: + final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc); + interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode()); + break; + case INVALID: + break; + } + + return interestDue; + } + + private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, + final double interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final int daysInPeriod, + final Money outstandingBalance, final int periodNumber) { + + Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, daysInPeriod, outstandingBalance); + + if (isInterestPaymentGraceApplicableForThisPeriod(periodNumber)) { + interest = interest.zero(); + } + + Double fraction = interestCalculationGraceOnRepaymentPeriodFraction; + + if (isInterestFreeGracePeriod(periodNumber)) { + interest = interest.zero(); + } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { + + if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { + interest = interest.zero(); + fraction = fraction - Integer.valueOf(1).doubleValue(); + + } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.25") + && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { + + Money graceOnInterestForRepaymentPeriod = interest.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); + interest = interest.minus(graceOnInterestForRepaymentPeriod); + fraction = Double.valueOf("0"); + } + } + + return interest; + } + + private boolean isInterestFreeGracePeriodFromDate(final double interestCalculationGraceOnRepaymentPeriodFraction) { + return this.interestChargedFromDate != null && interestCalculationGraceOnRepaymentPeriodFraction > Double.valueOf("0.0"); + } + + private Money calculateEqualPrincipalDueForInstallment(final MathContext mc, final int periodNumber) { + + final Integer numberOfPrincipalPaymentPeriods = calculateNumberOfPrincipalPaymentPeriods(this.numberOfRepayments); + + Money principal = this.principal.dividedBy(numberOfPrincipalPaymentPeriods, mc.getRoundingMode()); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principal = principal.zero(); + } + return principal; + } + + private Money calculatePrincipalDueForInstallment(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc, + final int periodNumber, final Money totalDuePerInstallment, final int daysInPeriod, final Money outstandingBalance) { + + Money interestForThisInstallmentBeforeGrace = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, + daysInPeriod, outstandingBalance); + + Money principal = totalDuePerInstallment.minus(interestForThisInstallmentBeforeGrace); + if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { + principal = principal.zero(); + } + return principal; + } + + private Money calculateTotalDueForEqualInstallmentRepaymentPeriod(final BigDecimal periodicInterestRate, final Money balance, + final int periodsElapsed) { + + final double paymentPerRepaymentPeriod = paymentPerPeriod(periodicInterestRate, balance, periodsElapsed); + + return Money.of(balance.getCurrency(), BigDecimal.valueOf(paymentPerRepaymentPeriod)); + } + + public LoanProductRelatedDetail toLoanProductRelatedDetail() { + final MonetaryCurrency currency = new MonetaryCurrency(this.currency.getCode(), this.currency.getDecimalPlaces()); + + return LoanProductRelatedDetail.createFrom(currency, this.principal.getAmount(), this.interestRatePerPeriod, + this.interestRatePeriodFrequencyType, this.annualNominalInterestRate, this.interestMethod, + this.interestCalculationPeriodMethod, this.repaymentEvery, this.repaymentPeriodFrequencyType, this.numberOfRepayments, + this.principalGrace, this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod, + this.inArrearsTolerance.getAmount()); + } + + public Integer getLoanTermFrequency() { + return this.loanTermFrequency; + } + + public PeriodFrequencyType getLoanTermPeriodFrequencyType() { + return this.loanTermPeriodFrequencyType; + } + + public Integer getRepaymentEvery() { + return this.repaymentEvery; + } + + public PeriodFrequencyType getRepaymentPeriodFrequencyType() { + return this.repaymentPeriodFrequencyType; + } + + public Date getRepaymentStartFromDate() { + Date dateValue = null; + if (this.repaymentsStartingFromDate != null) { + dateValue = this.repaymentsStartingFromDate.toDate(); + } + return dateValue; + } + + public Date getInterestChargedFromDate() { + Date dateValue = null; + if (this.interestChargedFromDate != null) { + dateValue = this.interestChargedFromDate.toDate(); + } + return dateValue; + } + + public LocalDate getInterestChargedFromLocalDate() { + return this.interestChargedFromDate; + } + + public InterestMethod getInterestMethod() { + return this.interestMethod; + } + + public AmortizationMethod getAmortizationMethod() { + return this.amortizationMethod; + } + + public MonetaryCurrency getCurrency() { + return this.principal.getCurrency(); + } + + public Integer getNumberOfRepayments() { + return this.numberOfRepayments; + } + + public LocalDate getExpectedDisbursementDate() { + return this.expectedDisbursementDate; + } + + public LocalDate getRepaymentsStartingFromLocalDate() { + return this.repaymentsStartingFromDate; + } + + public Money getPrincipal() { + return this.principal; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java deleted file mode 100644 index efa357a64..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanSchedule.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.util.Set; - -import org.joda.time.LocalDate; -import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; -import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -/** - * Domain representation of a Loan Schedule (not used for persistence) - */ -public final class LoanSchedule { - - private final LoanScheduleGenerator loanScheduleGenerator; - private final ApplicationCurrency applicationCurrency; - private final BigDecimal principal; - private final BigDecimal nominalInterestRatePerPeriod; - private final PeriodFrequencyType interestRatePeriodFrequencyType; - private final BigDecimal nominalAnnualInterestRate; - private final InterestMethod interestMethod; - private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; - private final Integer repaymentEvery; - private final PeriodFrequencyType repaymentPeriodFrequencyType; - private final Integer numberOfRepayments; - private final AmortizationMethod amortizationMethod; - private final Integer loanTermFrequency; - private final PeriodFrequencyType loanTermPeriodFrequencyType; - private final LocalDate disbursementDate; - private final LocalDate repaymentStartFromDate; - private final LocalDate interestChargedFromDate; - private final BigDecimal inArrearsTolerance; - - private final Set loanCharges; - - public LoanSchedule(final LoanScheduleGenerator loanScheduleGenerator, final ApplicationCurrency applicationCurrency, - final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, - final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, - final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, - final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments, - final AmortizationMethod amortizationMethod, final Integer loanTermFrequency, - final PeriodFrequencyType loanTermPeriodFrequencyType, final Set loanCharges, final LocalDate disbursementDate, - final LocalDate repaymentStartFromDate, final LocalDate interestChargedFromDate, final BigDecimal inArrearsTolerance) { - this.loanScheduleGenerator = loanScheduleGenerator; - this.applicationCurrency = applicationCurrency; - this.principal = principal; - this.nominalInterestRatePerPeriod = nominalInterestRatePerPeriod; - this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType; - this.nominalAnnualInterestRate = nominalAnnualInterestRate; - this.interestMethod = interestMethod; - this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; - this.repaymentEvery = repaymentEvery; - this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType; - this.numberOfRepayments = numberOfRepayments; - this.amortizationMethod = amortizationMethod; - this.loanTermFrequency = loanTermFrequency; - this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType; - this.loanCharges = loanCharges; - this.disbursementDate = disbursementDate; - this.repaymentStartFromDate = repaymentStartFromDate; - this.interestChargedFromDate = interestChargedFromDate; - this.inArrearsTolerance = inArrearsTolerance; - } - - public LoanScheduleData generate() { - return loanScheduleGenerator.generate(applicationCurrency, loanProductRelatedDetail(), loanTermFrequency, - loanTermPeriodFrequencyType, disbursementDate, repaymentStartFromDate, interestChargedFromDate, loanCharges); - } - - public LoanProductRelatedDetail loanProductRelatedDetail() { - final MonetaryCurrency currency = new MonetaryCurrency(applicationCurrency.getCode(), applicationCurrency.getDecimalPlaces()); - - return LoanProductRelatedDetail.createFrom(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, - nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType, - numberOfRepayments, amortizationMethod, this.inArrearsTolerance); - } - - public Integer getLoanTermFrequency() { - return this.loanTermFrequency; - } - - public PeriodFrequencyType getLoanTermPeriodFrequencyType() { - return this.loanTermPeriodFrequencyType; - } - - public LocalDate getDisbursementDate() { - return this.disbursementDate; - } - - public LocalDate getRepaymentStartFromDate() { - return this.repaymentStartFromDate; - } - - public LocalDate getInterestChargedFromDate() { - return this.interestChargedFromDate; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 376296be7..721fad5fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -5,19 +5,14 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; +import java.math.MathContext; import java.util.Set; -import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface LoanScheduleGenerator { - LoanScheduleData generate(ApplicationCurrency applicationCurrency, LoanProductRelatedDetail loanScheduleInfo, - Integer loanTermFrequency, PeriodFrequencyType loanTermFrequencyType, LocalDate disbursementDate, LocalDate firstRepaymentDate, - LocalDate interestCalculatedFrom, Set loanCharges); - + LoanScheduleModel generate(MathContext mc, ApplicationCurrency applicationCurrency, LoanApplicationTerms loanApplicationTerms, + Set loanCharges); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java index f8e542094..d3e946981 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java @@ -10,5 +10,4 @@ import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; public interface LoanScheduleGeneratorFactory { LoanScheduleGenerator create(InterestMethod interestMethod); - } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java new file mode 100644 index 000000000..495fcdbea --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java @@ -0,0 +1,87 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; + +import org.mifosplatform.organisation.monetary.data.CurrencyData; +import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule (not used for persistence) + */ +public final class LoanScheduleModel { + + private final Collection periods; + private final ApplicationCurrency applicationCurrency; + private final int loanTermInDays; + private final Money totalPrincipalDisbursed; + private final BigDecimal totalPrincipalExpected; + private final BigDecimal totalPrincipalPaid; + private final BigDecimal totalInterestCharged; + private final BigDecimal totalFeeChargesCharged; + private final BigDecimal totalPenaltyChargesCharged; + private final BigDecimal totalRepaymentExpected; + private final BigDecimal totalOutstanding; + + public static LoanScheduleModel from(final Collection periods, final ApplicationCurrency applicationCurrency, + final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, + final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, + final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { + + return new LoanScheduleModel(periods, applicationCurrency, loanTermInDays, principalDisbursed, totalPrincipalExpected, + totalPrincipalPaid, totalInterestCharged, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected, + totalOutstanding); + } + + private LoanScheduleModel(final Collection periods, final ApplicationCurrency applicationCurrency, + final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, + final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, + final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { + this.periods = periods; + this.applicationCurrency = applicationCurrency; + this.loanTermInDays = loanTermInDays; + this.totalPrincipalDisbursed = principalDisbursed; + this.totalPrincipalExpected = totalPrincipalExpected; + this.totalPrincipalPaid = totalPrincipalPaid; + this.totalInterestCharged = totalInterestCharged; + this.totalFeeChargesCharged = totalFeeChargesCharged; + this.totalPenaltyChargesCharged = totalPenaltyChargesCharged; + this.totalRepaymentExpected = totalRepaymentExpected; + this.totalOutstanding = totalOutstanding; + } + + public LoanScheduleData toData() { + + final int decimalPlaces = this.totalPrincipalDisbursed.getCurrencyDigitsAfterDecimal(); + final CurrencyData currency = applicationCurrency.toData(decimalPlaces); + + final Collection periodsData = new ArrayList(); + for (LoanScheduleModelPeriod modelPeriod : this.periods) { + periodsData.add(modelPeriod.toData()); + } + + BigDecimal totalWaived = null; + BigDecimal totalWrittenOff = null; + BigDecimal totalRepayment = null; + BigDecimal totalPaidInAdvance = null; + BigDecimal totalPaidLate = null; + + return new LoanScheduleData(currency, periodsData, this.loanTermInDays, this.totalPrincipalDisbursed.getAmount(), + this.totalPrincipalExpected, this.totalPrincipalPaid, this.totalInterestCharged, this.totalFeeChargesCharged, + this.totalPenaltyChargesCharged, totalWaived, totalWrittenOff, this.totalRepaymentExpected, totalRepayment, + totalPaidInAdvance, totalPaidLate, this.totalOutstanding); + } + + public Collection getPeriods() { + return this.periods; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java new file mode 100644 index 000000000..61429b3ac --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java @@ -0,0 +1,86 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule Disbursement Period (not used for + * persistence) + */ +public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleModelPeriod { + + @SuppressWarnings("unused") + private final int periodNumber; + private final LocalDate disbursementDate; + private final Money principalDisbursed; + private final BigDecimal chargesDueAtTimeOfDisbursement; + + public static LoanScheduleModelDisbursementPeriod disbursement(final LoanApplicationTerms loanApplicationTerms, + final BigDecimal chargesDueAtTimeOfDisbursement) { + + final int periodNumber = 0; + return new LoanScheduleModelDisbursementPeriod(periodNumber, loanApplicationTerms, chargesDueAtTimeOfDisbursement); + } + + private LoanScheduleModelDisbursementPeriod(final int periodNumber, final LoanApplicationTerms loanApplicationTerms, + final BigDecimal chargesDueAtTimeOfDisbursement) { + this.periodNumber = periodNumber; + this.disbursementDate = loanApplicationTerms.getExpectedDisbursementDate(); + this.principalDisbursed = loanApplicationTerms.getPrincipal(); + this.chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement; + } + + @Override + public LoanSchedulePeriodData toData() { + return LoanSchedulePeriodData.disbursementOnlyPeriod(this.disbursementDate, this.principalDisbursed.getAmount(), + this.chargesDueAtTimeOfDisbursement, false); + } + + @Override + public boolean isRepaymentPeriod() { + return false; + } + + @Override + public Integer periodNumber() { + return null; + } + + @Override + public LocalDate periodFromDate() { + return null; + } + + @Override + public LocalDate periodDueDate() { + return null; + } + + @Override + public BigDecimal principalDue() { + return null; + } + + @Override + public BigDecimal interestDue() { + return null; + } + + @Override + public BigDecimal feeChargesDue() { + return null; + } + + @Override + public BigDecimal penaltyChargesDue() { + return null; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java new file mode 100644 index 000000000..5d7780033 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelPeriod.java @@ -0,0 +1,32 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +public interface LoanScheduleModelPeriod { + + LoanSchedulePeriodData toData(); + + boolean isRepaymentPeriod(); + + Integer periodNumber(); + + LocalDate periodFromDate(); + + LocalDate periodDueDate(); + + BigDecimal principalDue(); + + BigDecimal interestDue(); + + BigDecimal feeChargesDue(); + + BigDecimal penaltyChargesDue(); +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java new file mode 100644 index 000000000..87727cb51 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -0,0 +1,118 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +/** + * Domain representation of a Loan Schedule Repayment Period (not used for + * persistence) + */ +public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod { + + private final int periodNumber; + private final LocalDate fromDate; + private final LocalDate dueDate; + private final Money principalDue; + private final Money outstandingLoanBalance; + private final Money interestDue; + private final Money feeChargesDue; + private final Money penaltyChargesDue; + private final Money totalDue; + + public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate, + final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, + final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue) { + + return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance, + interestDue, feeChargesDue, penaltyChargesDue, totalDue); + } + + public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate, + final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue, + final Money penaltyChargesDue, final Money totalDue) { + this.periodNumber = periodNumber; + this.fromDate = fromDate; + this.dueDate = dueDate; + this.principalDue = principalDue; + this.outstandingLoanBalance = outstandingLoanBalance; + this.interestDue = interestDue; + this.feeChargesDue = feeChargesDue; + this.penaltyChargesDue = penaltyChargesDue; + this.totalDue = totalDue; + } + + @Override + public LoanSchedulePeriodData toData() { + return LoanSchedulePeriodData.repaymentOnlyPeriod(periodNumber, this.fromDate, this.dueDate, this.principalDue.getAmount(), + this.outstandingLoanBalance.getAmount(), this.interestDue.getAmount(), this.feeChargesDue.getAmount(), + this.penaltyChargesDue.getAmount(), this.totalDue.getAmount()); + } + + @Override + public boolean isRepaymentPeriod() { + return true; + } + + @Override + public Integer periodNumber() { + return this.periodNumber; + } + + @Override + public LocalDate periodFromDate() { + return this.fromDate; + } + + @Override + public LocalDate periodDueDate() { + return this.dueDate; + } + + @Override + public BigDecimal principalDue() { + BigDecimal value = null; + if (this.principalDue != null) { + value = this.principalDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal interestDue() { + BigDecimal value = null; + if (this.interestDue != null) { + value = this.interestDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal feeChargesDue() { + BigDecimal value = null; + if (this.feeChargesDue != null) { + value = this.feeChargesDue.getAmount(); + } + + return value; + } + + @Override + public BigDecimal penaltyChargesDue() { + BigDecimal value = null; + if (this.penaltyChargesDue != null) { + value = this.penaltyChargesDue.getAmount(); + } + + return value; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java index a170cd71a..c8b5ae3d0 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java @@ -5,8 +5,6 @@ */ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; -import java.util.List; - import org.joda.time.LocalDate; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; @@ -14,9 +12,7 @@ public interface PaymentPeriodsInOneYearCalculator { Integer calculate(PeriodFrequencyType repaymentFrequencyType); - double calculateRepaymentPeriodAsAFractionOfDays( - PeriodFrequencyType repaymentPeriodFrequencyType, - Integer every, LocalDate interestCalculatedFrom, - List scheduledDates, LocalDate disbursementDate); + double calculatePortionOfRepaymentPeriodInterestChargingGrace(LocalDate repaymentPeriodStartDate, LocalDate scheduledDueDate, + LocalDate interestChargedFromLocalDate, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaidEvery); -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java deleted file mode 100644 index 846ad07a4..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PeriodicInterestRateCalculator.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; - -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; - -public class PeriodicInterestRateCalculator { - - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); - - public BigDecimal calculateFrom(final PeriodFrequencyType loanTermFrequencyType, final BigDecimal annualNominalInterestRate) { - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanTermFrequencyType); - - BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); - return annualNominalInterestRate.divide(divisor, mc); - } - - public BigDecimal calculateFrom(final LoanProductRelatedDetail loanScheduleInfo) { - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - - Integer paymentPeriodsInOneYear = this.paymentPeriodsInOneYearCalculator.calculate(loanScheduleInfo.getRepaymentPeriodFrequencyType()); - - BigDecimal divisor = BigDecimal.valueOf(paymentPeriodsInOneYear * 100); - BigDecimal numberOfPeriods = BigDecimal.valueOf(loanScheduleInfo.getRepayEvery()); - - return loanScheduleInfo.getAnnualNominalInterestRate().divide(divisor, mc).multiply(numberOfPeriods); - } - - public Money calculateInterestOn( - final Money outstandingBalance, - final BigDecimal periodInterestRateForRepaymentPeriod, - final int daysInPeriod, - final LoanProductRelatedDetail loanScheduleInfo) { - - MathContext mc = new MathContext(8, RoundingMode.HALF_EVEN); - Money interestDue = Money.zero(outstandingBalance.getCurrency()); - - switch (loanScheduleInfo.getInterestCalculationPeriodMethod()) { - case DAILY: - BigDecimal dailyInterestRate = loanScheduleInfo.getAnnualNominalInterestRate() - .divide(BigDecimal.valueOf(Long.valueOf(365)), mc) - .divide(BigDecimal.valueOf(Double.valueOf("100.0")), mc) - .multiply(BigDecimal.valueOf(loanScheduleInfo.getRepayEvery())); - - BigDecimal equivalentInterestRateForPeriod = dailyInterestRate.multiply(BigDecimal.valueOf(Long.valueOf(daysInPeriod))); - - interestDue = outstandingBalance.multiplyRetainScale(equivalentInterestRateForPeriod, RoundingMode.HALF_EVEN); - break; - default: - interestDue = outstandingBalance.multiplyRetainScale(periodInterestRateForRepaymentPeriod, RoundingMode.HALF_EVEN); - break; - } - - return interestDue; - } - - public Money calculatePrincipalOn(Money totalDuePerInstallment, Money interestForInstallment, LoanProductRelatedDetail loanScheduleInfo) { - - Money principalDue = Money.zero(totalDuePerInstallment.getCurrency()); - - switch (loanScheduleInfo.getAmortizationMethod()) { - case EQUAL_PRINCIPAL: - principalDue = loanScheduleInfo.getPrincipal().dividedBy(loanScheduleInfo.getNumberOfRepayments(), RoundingMode.HALF_EVEN); - break; - case EQUAL_INSTALLMENTS: - principalDue = totalDuePerInstallment.minus(interestForInstallment); - break; - case INVALID: - break; - } - - return principalDue; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java deleted file mode 100644 index 929a36ba0..000000000 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PmtCalculator.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; - -import java.math.BigDecimal; - -import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; -import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; - -public class PmtCalculator { - - public Money calculatePaymentForOnePeriodFrom(final LoanProductRelatedDetail loanScheduleInfo, - final BigDecimal periodInterestRateForRepaymentPeriod, - final MonetaryCurrency monetaryCurrency) { - double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); - double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); - - return Money.of(monetaryCurrency, BigDecimal.valueOf(paymentPerInstallment)); - } - - public Money calculateTotalRepaymentFrom(final LoanProductRelatedDetail loanScheduleInfo, - final BigDecimal periodInterestRateForRepaymentPeriod, - final MonetaryCurrency monetaryCurrency) { - double interestRateFraction = periodInterestRateForRepaymentPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanScheduleInfo.getNumberOfRepayments().doubleValue(); - double principal = loanScheduleInfo.getPrincipal().getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principal, futureValue, false); - - double totalRepayment = paymentPerInstallment * loanScheduleInfo.getNumberOfRepayments(); - - return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); - } - - public Money calculateTotalRepaymentFrom( - final Money principal, - final Integer loanTermFrequency, - final BigDecimal periodInterestRateForLoanTermPeriod, - final MonetaryCurrency monetaryCurrency) { - - double interestRateFraction = periodInterestRateForLoanTermPeriod.doubleValue(); - - double futureValue = 0; - double numberOfPeriods = loanTermFrequency.doubleValue(); - double principalAmount = principal.getAmount().multiply(BigDecimal.valueOf(-1)).doubleValue(); - - // work out the total payment (principal + interest components) for each installment due. - // use the period type and interest as expressed e.g. 2% per month or 24% per year even is 'daily' interest calculation is selected - double paymentPerInstallment = pmt(interestRateFraction, numberOfPeriods, principalAmount, futureValue, false); - - double totalRepayment = paymentPerInstallment * loanTermFrequency; - - return Money.of(monetaryCurrency, BigDecimal.valueOf(totalRepayment)); - } - - /** - * PMT calculates a fixed monthly payment to be paid by borrower every 'period' to ensure loan is paid off in full (with interest). - * - * This monthly payment c depends upon - * the monthly interest rate r (expressed as a fraction, not a percentage, - * i.e., divide the quoted yearly percentage rate by 100 and by 12 to obtain - * the monthly interest rate), the number of monthly payments N called the - * loan's term, and the amount borrowed P known as the loan's principal; c - * is given by the formula: - * - * c = (r / (1 - (1 + r)^-N))P - * - * @param interestRateFraction - * @param numberOfPayments - * @param principal - * @param futureValue - * @param type - */ - private static double pmt(double interestRateFraction, double numberOfPayments, double principal, double futureValue, boolean type) { - double payment = 0; - if (interestRateFraction == 0) { - payment = -1 * (futureValue + principal) / numberOfPayments; - } else { - double r1 = interestRateFraction + 1; - payment = (futureValue + principal * Math.pow(r1, numberOfPayments)) * interestRateFraction - / ((type ? r1 : 1) * (1 - Math.pow(r1, numberOfPayments))); - } - return payment; - } -} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java new file mode 100644 index 000000000..812263add --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java @@ -0,0 +1,33 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; + +import org.mifosplatform.organisation.monetary.domain.Money; + +public class PrincipalInterest { + + private final Money principal; + private final Money interest; + private final Money interestPaymentDueToGrace; + + public PrincipalInterest(final Money principal, final Money interest, final Money interestPaymentDueToGrace) { + this.principal = principal; + this.interest = interest; + this.interestPaymentDueToGrace = interestPaymentDueToGrace; + } + + public Money principal() { + return this.principal; + } + + public Money interest() { + return this.interest; + } + + public Money interestPaymentDueToGrace() { + return this.interestPaymentDueToGrace; + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index 2b374933d..ba8722e26 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -8,17 +8,12 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain; import java.util.List; import org.joda.time.LocalDate; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; public interface ScheduledDateGenerator { - List generate( - LoanProductMinimumRepaymentScheduleRelatedDetail loanScheduleDateInfo, - LocalDate disbursementDate, LocalDate firstRepaymentDate); - - LocalDate idealDisbursementDateBasedOnFirstRepaymentDate( - LoanProductRelatedDetail loanScheduleInfo, - List scheduledDates); + List generate(LoanApplicationTerms loanApplicationTerms); + LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, + List scheduledDates); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 8a98e7a63..980fe3554 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -6,6 +6,8 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.util.Set; import org.joda.time.LocalDate; @@ -13,16 +15,19 @@ import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeAssembler; import org.mifosplatform.portfolio.loanproduct.domain.AmortizationMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.mifosplatform.portfolio.loanproduct.domain.InterestMethod; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.PeriodFrequencyType; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; @@ -54,65 +59,96 @@ public class LoanScheduleAssembler { this.loanChargeAssembler = loanChargeAssembler; } - public LoanSchedule fromJson(final String json) { - final JsonElement element = fromApiJsonHelper.parse(json); - return fromJson(element); - } - - public LoanSchedule fromJson(final JsonElement element) { - return fromJson(element, null); - } - - public LoanSchedule fromJson(final JsonElement element, final BigDecimal inArrearsTolerance) { + public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = fromApiJsonHelper.extractLongNamed("productId", element); final LoanProduct loanProduct = this.loanProductRepository.findOne(loanProductId); if (loanProduct == null) { throw new LoanProductNotFoundException(loanProductId); } + return assembleLoanApplicationTermsFrom(element, loanProduct); + } + + private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement element, final LoanProduct loanProduct) { + final MonetaryCurrency currency = loanProduct.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - final BigDecimal principal = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); - final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); - //final Integer interestRateFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestRateFrequencyType", element); - final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); - final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", - element); - - final InterestMethod interestMethod = InterestMethod.fromInt(interestType); - //final PeriodFrequencyType interestRatePeriodFrequencyType = PeriodFrequencyType.fromInt(interestRateFrequencyType); - - // PeriodFrequencyType is copied from Loan Product - final PeriodFrequencyType interestRatePeriodFrequencyType = loanProduct.loanProductRelatedDetail().getInterestPeriodFrequencyType(); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod - .fromInt(interestCalculationPeriodType); - - final BigDecimal defaultAnnualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType, - interestRatePerPeriod); - - final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); - final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element); - final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); - final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); - final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType); - final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); - + // loan terms final Integer loanTermFrequency = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency", element); final Integer loanTermFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequencyType", element); final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(loanTermFrequencyType); - final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); + final Integer numberOfRepayments = fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); + final Integer repaymentEvery = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); + final Integer repaymentFrequencyType = fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element); + final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType); + + final Integer amortizationType = fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); + final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); + + // interest terms + final Integer interestType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); + final InterestMethod interestMethod = InterestMethod.fromInt(interestType); + + final Integer interestCalculationPeriodType = fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", + element); + final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod + .fromInt(interestCalculationPeriodType); + + final BigDecimal interestRatePerPeriod = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element); + final PeriodFrequencyType interestRatePeriodFrequencyType = loanProduct.getInterestPeriodFrequencyType(); + + final BigDecimal annualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType, + interestRatePerPeriod); + + // disbursement details + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element); + final Money principalMoney = Money.of(currency, principal); + + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("expectedDisbursementDate", element); + final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper.extractLocalDateNamed("repaymentsStartingFromDate", element); + + // grace details + final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); final LocalDate interestChargedFromDate = fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element); + // other + final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); + final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance); + + return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, + repaymentEvery, repaymentPeriodFrequencyType, amortizationMethod, interestMethod, interestRatePerPeriod, + interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, principalMoney, + expectedDisbursementDate, repaymentsStartingFromDate, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney); + } + + public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { + LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); + return loanApplicationTerms.toLoanProductRelatedDetail(); + } + + public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) { + + final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); + + return assembleLoanScheduleFrom(loanApplicationTerms, element); + } + + public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final JsonElement element) { + final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element); - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); - return new LoanSchedule(loanScheduleGenerator, applicationCurrency, principal, interestRatePerPeriod, - interestRatePeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, - repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, amortizationMethod, loanTermFrequency, - loanTermPeriodFrequencyType, loanCharges, expectedDisbursementDate, repaymentsStartingFromDate, interestChargedFromDate, - inArrearsTolerance); + final RoundingMode roundingMode = RoundingMode.HALF_EVEN; + final MathContext mc = new MathContext(8, roundingMode); + + final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); + final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + + return loanScheduleGenerator.generate(mc, applicationCurrency, loanApplicationTerms, loanCharges); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java index a8cc2b393..e95a84fe5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformService.java @@ -6,9 +6,9 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import org.mifosplatform.infrastructure.core.api.JsonQuery; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; public interface LoanScheduleCalculationPlatformService { - LoanScheduleData calculateLoanSchedule(JsonQuery query); + LoanScheduleModel calculateLoanSchedule(JsonQuery query); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index b43bc59b2..11619237b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -6,9 +6,7 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.service; import org.mifosplatform.infrastructure.core.api.JsonQuery; -import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.serialization.CalculateLoanScheduleQueryFromApiJsonHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -16,26 +14,21 @@ import org.springframework.stereotype.Service; @Service public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleCalculationPlatformService { - private final PlatformSecurityContext context; private final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer; private final LoanScheduleAssembler loanScheduleAssembler; @Autowired - public LoanScheduleCalculationPlatformServiceImpl(final PlatformSecurityContext context, - final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, + public LoanScheduleCalculationPlatformServiceImpl(final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, final LoanScheduleAssembler loanScheduleAssembler) { - this.context = context; this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.loanScheduleAssembler = loanScheduleAssembler; } @Override - public LoanScheduleData calculateLoanSchedule(final JsonQuery query) { - context.authenticatedUser(); + public LoanScheduleModel calculateLoanSchedule(final JsonQuery query) { this.fromApiJsonDeserializer.validate(query.json()); - final LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(query.parsedJson()); - return loanSchedule.generate(); + return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson()); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 8d4215b1a..585cf2186 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -40,8 +40,9 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { "productId", "accountNo", "externalId", "fundId", "loanOfficerId", "loanPurposeId", "transactionProcessingStrategyId", "principal", "inArrearsTolerance", "interestRatePerPeriod", "repaymentEvery", "numberOfRepayments", "loanTermFrequency", "loanTermFrequencyType", "repaymentFrequencyType", "interestRateFrequencyType", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", - "submittedOnDate", "submittedOnNote", "locale", "dateFormat", "charges", "collateral")); + "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", + "graceOnInterestPayment", "graceOnInterestCharged", "interestChargedFromDate", "submittedOnDate", "submittedOnNote", "locale", + "dateFormat", "charges", "collateral")); private final FromJsonHelper fromApiJsonHelper; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index f31d7b1dd..b6be5dddc 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -40,8 +40,9 @@ public final class LoanApplicationCommandFromApiJsonHelper { */ final Set supportedParameters = new HashSet(Arrays.asList("dateFormat", "locale", "id", "clientId", "groupId", "loanType", "productId", "principal", "loanTermFrequency", "loanTermFrequencyType", "numberOfRepayments", "repaymentEvery", - "repaymentFrequencyType", "interestRatePerPeriod", "amortizationType", "interestType", - "interestCalculationPeriodType", "expectedDisbursementDate", "repaymentsStartingFromDate", "interestChargedFromDate", + "repaymentFrequencyType", "interestRatePerPeriod", "amortizationType", "interestType", "interestCalculationPeriodType", + "expectedDisbursementDate", "repaymentsStartingFromDate", "graceOnPrincipalPayment", "graceOnInterestPayment", + "graceOnInterestCharged", "interestChargedFromDate", // "submittedOnDate", "submittedOnNote", // "accountNo", "externalId", "fundId", "loanOfficerId", // optional "loanPurposeId", "inArrearsTolerance", "charges", "collateral", // optional @@ -50,7 +51,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { )); private final FromJsonHelper fromApiJsonHelper; - + @Autowired public LoanApplicationCommandFromApiJsonHelper(final FromJsonHelper fromApiJsonHelper) { this.fromApiJsonHelper = fromApiJsonHelper; @@ -70,14 +71,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String loanTypeParameterName = "loanType"; final String loanTypeStr = fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanTypeStr).notNull(); - - if(!StringUtils.isBlank(loanTypeStr)){ + + if (!StringUtils.isBlank(loanTypeStr)) { final LoanType loanType = LoanType.fromName(loanTypeStr); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 3); - + final Long clientId = fromApiJsonHelper.extractLongNamed("clientId", element); final Long groupId = fromApiJsonHelper.extractLongNamed("groupId", element); - if(loanType.isIndividualLoan()){ + if (loanType.isIndividualLoan()) { baseDataValidator.reset().parameter("clientId").value(clientId).notNull().longGreaterThanZero(); baseDataValidator.reset().parameter("groupId").value(groupId).mustBeBlankWhenParameterProvided("clientId", clientId); } @@ -93,7 +94,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } - + final Long productId = fromApiJsonHelper.extractLongNamed("productId", element); baseDataValidator.reset().parameter("productId").value(productId).notNull().integerGreaterThanZero(); @@ -176,13 +177,15 @@ public final class LoanApplicationCommandFromApiJsonHelper { final LocalDate expectedDisbursementDate = fromApiJsonHelper.extractLocalDateNamed(expectedDisbursementDateParameterName, element); baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDate).notNull(); - final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; - if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, - element); - baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull() - .notNull(); - } + // grace validation + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); final String interestChargedFromDateParameterName = "interestChargedFromDate"; if (fromApiJsonHelper.parameterExists(interestChargedFromDateParameterName, element)) { @@ -192,6 +195,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notNull(); } + final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; + if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { + final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, + element); + baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull() + .notNull(); + } + final String inArrearsToleranceParameterName = "inArrearsTolerance"; if (fromApiJsonHelper.parameterExists(inArrearsToleranceParameterName, element)) { final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed(inArrearsToleranceParameterName, @@ -304,7 +315,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); final JsonElement element = fromApiJsonHelper.parse(json); boolean atLeastOneParameterPassedForUpdate = false; - + final String clientIdParameterName = "clientId"; if (fromApiJsonHelper.parameterExists(clientIdParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -459,12 +470,20 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(expectedDisbursementDateParameterName).value(expectedDisbursementDate).notNull(); } - final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; - if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, - element); - baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull(); + // grace validation + if (fromApiJsonHelper.parameterExists("graceOnPrincipalPayment", element)) { + final Integer graceOnPrincipalPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestPayment", element)) { + final Integer graceOnInterestPayment = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment).zeroOrPositiveAmount(); + } + + if (fromApiJsonHelper.parameterExists("graceOnInterestCharged", element)) { + final Integer graceOnInterestCharged = fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged).zeroOrPositiveAmount(); } final String interestChargedFromDateParameterName = "interestChargedFromDate"; @@ -475,6 +494,14 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(interestChargedFromDateParameterName).value(interestChargedFromDate).ignoreIfNull(); } + final String repaymentsStartingFromDateParameterName = "repaymentsStartingFromDate"; + if (fromApiJsonHelper.parameterExists(repaymentsStartingFromDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final LocalDate repaymentsStartingFromDate = fromApiJsonHelper.extractLocalDateNamed(repaymentsStartingFromDateParameterName, + element); + baseDataValidator.reset().parameter(repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate).ignoreIfNull(); + } + final String submittedOnDateParameterName = "submittedOnDate"; if (fromApiJsonHelper.parameterExists(submittedOnDateParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -593,7 +620,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } - + public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) { final List dataValidationErrors = new ArrayList(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index 571c8d51c..74a8c6733 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -94,10 +94,33 @@ public final class LoanEventApiJsonValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - /** - * @param baseDataValidator - * @param element - */ + public void validateNewRepaymentTransaction(final String json) { + + if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } + + final Set transactionParameters = new HashSet(Arrays.asList("transactionDate", "transactionAmount", "note", + "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + + final JsonElement element = fromApiJsonHelper.parse(json); + final LocalDate transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + final BigDecimal transactionAmount = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); + + final String note = fromApiJsonHelper.extractStringNamed("note", element); + baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + + validatePaymentDetails(baseDataValidator, element); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { // Validate all string payment detail fields for max length final Integer paymentTypeId = fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 34f0e8069..06a634447 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -51,8 +51,8 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationDateExce import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; import org.mifosplatform.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.mifosplatform.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; import org.mifosplatform.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator; @@ -60,7 +60,7 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.mifosplatform.portfolio.note.domain.Note; import org.mifosplatform.portfolio.note.domain.NoteRepository; import org.mifosplatform.useradministration.domain.AppUser; @@ -81,7 +81,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator; - private final LoanProductCommandFromApiJsonDeserializer loanProductCommandFromApiJsonDeserializer; + private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer; private final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer; private final LoanRepository loanRepository; private final NoteRepository noteRepository; @@ -102,7 +102,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator, final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer, - final LoanProductCommandFromApiJsonDeserializer loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator, + final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator, final LoanAssembler loanAssembler, final LoanChargeAssembler loanChargeAssembler, final CollateralAssembler loanCollateralAssembler, final LoanRepository loanRepository, final NoteRepository noteRepository, final LoanScheduleCalculationPlatformService calculationPlatformService, final ClientRepositoryWrapper clientRepository, @@ -294,7 +294,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final JsonElement parsedQuery = this.fromJsonHelper.parse(command.json()); final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromJsonHelper); - final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); + final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query); existingLoanApplication.updateLoanSchedule(loanSchedule); } @@ -536,14 +536,16 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final LocalDate edDate = command.localDateValueOfParameterNamed("expectedDisbursementDate"); if (edDate != null) { - //Expected disbursement date should not be before Meeting start date - if(edDate.isBefore(calendar.getStartDateLocalDate())){ + // Expected disbursement date should not be before Meeting start + // date + if (edDate.isBefore(calendar.getStartDateLocalDate())) { final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate, calendar.getStartDate()); + throw new LoanApplicationDateException("disbursement.date.cannot.be.before.meeting.start.date", errorMessage, edDate, + calendar.getStartDate()); } - - //Disbursement date should fall on a meeting date + + // Disbursement date should fall on a meeting date if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), edDate)) { final String errorMessage = "Expected disbursement date '" + edDate.toString() + "' does not fall on a meeting date."; throw new NotValidRecurringDateException("loan.expected.disbursement.date", errorMessage, edDate.toString(), @@ -553,18 +555,20 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } final LocalDate repStartingDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate"); - + if (repStartingDate != null) { - //First repayment date should not be before Meeting date - if(repStartingDate.isBefore(calendar.getStartDateLocalDate())){ - final String errorMessage = "First repayment date '" + repStartingDate.toString() + "' cannot be before meeting start date '" - + calendar.getStartDate().toString() + "' "; - throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage, repStartingDate, calendar.getStartDate()); + // First repayment date should not be before Meeting date + if (repStartingDate.isBefore(calendar.getStartDateLocalDate())) { + final String errorMessage = "First repayment date '" + repStartingDate.toString() + + "' cannot be before meeting start date '" + calendar.getStartDate().toString() + "' "; + throw new LoanApplicationDateException("first.repayment.date.cannot.be.before.meeting.start.date", errorMessage, + repStartingDate, calendar.getStartDate()); } - + if (!CalendarHelper.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repStartingDate)) { - final String errorMessage = "The First repayment date '" + repStartingDate.toString() + "' does not fall on a meeting date."; + final String errorMessage = "The First repayment date '" + repStartingDate.toString() + + "' does not fall on a meeting date."; throw new NotValidRecurringDateException("loan.first.repayment.date", errorMessage, repStartingDate.toString(), calendar.getTitle()); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java index 21d649072..b8d500a26 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanAssembler.java @@ -5,7 +5,6 @@ */ package org.mifosplatform.portfolio.loanaccount.service; -import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -40,9 +39,11 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus; import org.mifosplatform.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanSchedule; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.mifosplatform.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; +import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; @@ -131,22 +132,21 @@ public class LoanAssembler { } } - final BigDecimal inArrearsTolerance = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); - final LoanSchedule loanSchedule = this.loanScheduleAssembler.fromJson(element, inArrearsTolerance); - Loan loanApplication = null; Client client = null; Group group = null; - + + final LoanProductRelatedDetail loanProductRelatedDetail = this.loanScheduleAssembler.assembleLoanProductRelatedDetail(element); + final String loanTypeParameterName = "loanType"; final String loanTypeStr = fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); final EnumOptionData loanType = LoanEnumerations.loanType(loanTypeStr); - + if (clientId != null) { client = this.clientRepository.findOneWithNotFoundDetection(clientId); loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, - loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanSchedule, loanCharges, collateral); + loanOfficer, loanPurpose, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral); } if (groupId != null) { @@ -154,15 +154,15 @@ public class LoanAssembler { if (group == null) { throw new GroupNotFoundException(groupId); } loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanSchedule, loanCharges); + loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); } if (client != null && group != null) { if (!group.hasClientAsMember(client)) { throw new ClientNotInGroupException(clientId, groupId); } - loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanTransactionProcessingStrategy, loanSchedule, loanCharges); + loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), + loanProduct, fund, loanOfficer, loanTransactionProcessingStrategy, loanProductRelatedDetail, loanCharges); } final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element); @@ -172,8 +172,11 @@ public class LoanAssembler { loanApplication.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); - loanApplication - .loanApplicationSubmittal(currentUser, loanSchedule, defaultLoanLifecycleStateMachine(), submittedOnDate, externalId); + final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); + final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, element); + + loanApplication.loanApplicationSubmittal(currentUser, loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(), + submittedOnDate, externalId); return loanApplication; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 1212b2bfc..be244bd87 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -95,7 +95,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final PaginationHelper paginationHelper = new PaginationHelper(); private final LoanMapper loaanLoanMapper = new LoanMapper(); - @Autowired public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, final LoanRepository loanRepository, final LoanTransactionRepository loanTransactionRepository, @@ -133,7 +132,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("select "); sqlBuilder.append(rm.loanSchema()); - sqlBuilder.append(" join m_office o on o.id = c.office_id"); + sqlBuilder.append(" join m_office o on (o.id = c.office_id or o.id = g.office_id) "); sqlBuilder.append(" where l.id=? and o.hierarchy like ?"); return this.jdbcTemplate.queryForObject(sqlBuilder.toString(), rm, new Object[] { loanId, hierarchySearchString }); @@ -402,6 +401,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname," + " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.expected_maturedon_date as expectedMaturityDate, " + " l.principal_amount as principal, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + + " l.grace_on_principal_periods as graceOnPrincipalPayment, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged," + " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, " + " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, " + " l.term_frequency as termFrequency, l.term_period_frequency_enum as termPeriodFrequencyType, " @@ -436,6 +436,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.total_waived_derived as totalWaived," + " l.total_writtenoff_derived as totalWrittenOff," + " l.total_outstanding_derived as totalOutstanding," + + " l.total_overpaid_derived as totalOverpaid," + " la.principal_overdue_derived as principalOverdue," + " la.interest_overdue_derived as interestOverdue," + " la.fee_charges_overdue_derived as feeChargesOverdue," @@ -543,6 +544,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { expectedMaturityDate); final BigDecimal principal = rs.getBigDecimal("principal"); + final BigDecimal totalOverpaid = rs.getBigDecimal("totalOverpaid"); final BigDecimal inArrearsTolerance = rs.getBigDecimal("inArrearsTolerance"); final Integer numberOfRepayments = JdbcSupport.getInteger(rs, "numberOfRepayments"); @@ -550,6 +552,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final BigDecimal interestRatePerPeriod = rs.getBigDecimal("interestRatePerPeriod"); final BigDecimal annualInterestRate = rs.getBigDecimal("annualInterestRate"); + final Integer graceOnPrincipalPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestPayment"); + final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged"); + final Integer termFrequency = JdbcSupport.getInteger(rs, "termFrequency"); final Integer termPeriodFrequencyTypeInt = JdbcSupport.getInteger(rs, "termPeriodFrequencyType"); final EnumOptionData termPeriodFrequencyType = LoanEnumerations.termFrequencyType(termPeriodFrequencyTypeInt); @@ -642,10 +648,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientName, clientOfficeId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, fundId, fundName, loanPurposeId, loanPurposeName, - loanOfficerId, loanOfficerName, currencyData, principal, inArrearsTolerance, termFrequency, termPeriodFrequencyType, - numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, amortizationType, - interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, interestCalculationPeriodType, - expectedFirstRepaymentOnDate, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); + loanOfficerId, loanOfficerName, currencyData, principal, totalOverpaid, inArrearsTolerance, termFrequency, + termPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentFrequencyType, transactionStrategyId, + amortizationType, interestRatePerPeriod, interestRateFrequencyType, annualInterestRate, interestType, + interestCalculationPeriodType, expectedFirstRepaymentOnDate, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, interestChargedFromDate, timeline, loanSummary, feeChargesDueAtDisbursementCharged); } } @@ -667,11 +674,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { public String schema() { - return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, " + return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, ls.obligations_met_on_date as obligationsMetOnDate, ls.completed_derived as complete," + " ls.principal_amount as principalDue, ls.principal_completed_derived as principalPaid, ls.principal_writtenoff_derived as principalWrittenOff, " + " ls.interest_amount as interestDue, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, ls.interest_writtenoff_derived as interestWrittenOff, " + " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, " - + " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff " + + " ls.penalty_charges_amount as penaltyChargesDue, ls.penalty_charges_completed_derived as penaltyChargesPaid, ls.penalty_charges_waived_derived as penaltyChargesWaived, ls.penalty_charges_writtenoff_derived as penaltyChargesWrittenOff, " + + " ls.total_paid_in_advance_derived as totalPaidInAdvanceForPeriod, ls.total_paid_late_derived as totalPaidLateForPeriod " + " from m_loan_repayment_schedule ls "; } @@ -696,6 +704,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { Money totalWrittenOff = Money.zero(monCurrency); Money totalRepaymentExpected = Money.zero(monCurrency); Money totalRepayment = Money.zero(monCurrency); + Money totalPaidInAdvance = Money.zero(monCurrency); + Money totalPaidLate = Money.zero(monCurrency); Money totalOutstanding = Money.zero(monCurrency); // update totals with details of fees charged during disbursement @@ -711,6 +721,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Integer period = JdbcSupport.getInteger(rs, "period"); LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromDate"); final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); + final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate"); + final boolean complete = rs.getBoolean("complete"); Integer daysInPeriod = Integer.valueOf(0); if (fromDate != null) { @@ -750,6 +762,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final BigDecimal penaltyChargesWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWaived"); final BigDecimal penaltyChargesWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penaltyChargesWrittenOff"); + final BigDecimal totalPaidInAdvanceForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, + "totalPaidInAdvanceForPeriod"); + final BigDecimal totalPaidLateForPeriod = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPaidLateForPeriod"); + final BigDecimal penaltyChargesActualDue = penaltyChargesExpectedDue.subtract(penaltyChargesWaived).subtract( penaltyChargesWrittenOff); final BigDecimal penaltyChargesOutstanding = penaltyChargesActualDue.subtract(penaltyChargesPaid); @@ -771,6 +787,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { totalRepaymentExpected = totalRepaymentExpected.plus(totalDueForPeriod); totalRepayment = totalRepayment.plus(totalPaidForPeriod); + totalPaidInAdvance = totalPaidInAdvance.plus(totalPaidInAdvanceForPeriod); + totalPaidLate = totalPaidLate.plus(totalPaidLateForPeriod); totalOutstanding = totalOutstanding.plus(totalOutstandingForPeriod); if (fromDate == null) { @@ -783,12 +801,13 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { outstandingLoanPrincipalBalance = outstandingLoanPrincipalBalance.subtract(principalDue); final LoanSchedulePeriodData periodData = LoanSchedulePeriodData.repaymentPeriodWithPayments(loanId, period, fromDate, - dueDate, principalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan, - interestExpectedDue, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesExpectedDue, - feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesExpectedDue, - penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, - totalPaidForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod, totalOutstandingForPeriod, - totalActualCostOfLoanForPeriod); + dueDate, obligationsMetOnDate, complete, principalDue, principalPaid, principalWrittenOff, principalOutstanding, + outstandingPrincipalBalanceOfLoan, interestExpectedDue, interestPaid, interestWaived, interestWrittenOff, + interestOutstanding, feeChargesExpectedDue, feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, + feeChargesOutstanding, penaltyChargesExpectedDue, penaltyChargesPaid, penaltyChargesWaived, + penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaidForPeriod, + totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod, + totalOutstandingForPeriod, totalActualCostOfLoanForPeriod); periods.add(periodData); } @@ -797,7 +816,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { totalPrincipalExpected.getAmount(), totalPrincipalPaid.getAmount(), totalInterestCharged.getAmount(), totalFeeChargesCharged.getAmount(), totalPenaltyChargesCharged.getAmount(), totalWaived.getAmount(), totalWrittenOff.getAmount(), totalRepaymentExpected.getAmount(), totalRepayment.getAmount(), - totalOutstanding.getAmount()); + totalPaidInAdvance.getAmount(), totalPaidLate.getAmount(), totalOutstanding.getAmount()); } } @@ -864,7 +883,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @Override public LoanAccountData retrieveLoanProductDetailsTemplate(final Long productId) { - context.authenticatedUser(); final LoanProductData loanProduct = this.loanProductReadPlatformService.retrieveLoanProduct(productId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index b6c285350..30ec6b896 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -62,6 +62,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanNotFoundException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.mifosplatform.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; @@ -91,6 +92,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final LoanSummaryWrapper loanSummaryWrapper; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final LoanScheduleGeneratorFactory loanScheduleFactory; private final CalendarInstanceRepository calendarInstanceRepository; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; @@ -103,7 +105,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanChargeRepository loanChargeRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanSummaryWrapper loanSummaryWrapper, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - final CalendarInstanceRepository calendarInstanceRepository, + final LoanScheduleGeneratorFactory loanScheduleFactory, final CalendarInstanceRepository calendarInstanceRepository, final PaymentDetailWritePlatformService paymentDetailWritePlatformService) { this.context = context; this.loanEventApiJsonValidator = loanEventApiJsonValidator; @@ -118,6 +120,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanUpdateCommandFromApiJsonDeserializer = loanUpdateCommandFromApiJsonDeserializer; this.loanSummaryWrapper = loanSummaryWrapper; this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; + this.loanScheduleFactory = loanScheduleFactory; this.calendarInstanceRepository = calendarInstanceRepository; this.paymentDetailWritePlatformService = paymentDetailWritePlatformService; } @@ -152,8 +155,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - loan.disburse(currentUser, command, applicationCurrency, existingTransactionIds, existingReversedTransactionIds, changes, - paymentDetail); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, paymentDetail); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -178,7 +181,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - private void validateDisbursementDate(LocalDate disbursementDate, Collection calendarInstances) { + private void validateDisbursementDate(final LocalDate disbursementDate, final Collection calendarInstances) { if (!CollectionUtils.isEmpty(calendarInstances)) { Calendar calendar = null; for (CalendarInstance calendarInstance : calendarInstances) { @@ -208,16 +211,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + /**** + * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base + *****/ @Transactional @Override public Map bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand) { final AppUser currentUser = context.authenticatedUser(); - /**** - * TODO Vishwas: Pair with Ashok and re-factor collection sheet - * code-base - *****/ - SingleDisbursalCommand[] disbursalCommand = bulkDisbursalCommand.getDisburseTransactions(); Map changes = new LinkedHashMap(); if (disbursalCommand == null) { return changes; } @@ -234,8 +235,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - loan.disburse(currentUser, command, applicationCurrency, existingTransactionIds, existingReversedTransactionIds, changes, - paymentDetail); + loan.disburse(this.loanScheduleFactory, currentUser, command, applicationCurrency, existingTransactionIds, + existingReversedTransactionIds, changes, paymentDetail); if (!changes.isEmpty()) { this.loanRepository.save(loan); @@ -296,7 +297,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf context.authenticatedUser(); - this.loanEventApiJsonValidator.validateTransaction(command.json()); + this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json()); final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); @@ -337,6 +338,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds); this.loanTransactionRepository.save(newRepaymentTransaction); + /*** * TODO Vishwas Batch save is giving me a * HibernateOptimisticLockingFailureException, looping and saving for diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java index 32f0b7e45..fcdf05d18 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -7,7 +7,6 @@ package org.mifosplatform.portfolio.loanproduct.api; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -26,6 +25,7 @@ import javax.ws.rs.core.UriInfo; import org.mifosplatform.accounting.common.AccountingDropdownReadPlatformService; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService; import org.mifosplatform.commands.domain.CommandWrapper; @@ -61,13 +61,14 @@ public class LoanProductsApiResource { private final Set LOAN_PRODUCT_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "name", "description", "fundId", "fundName", "currency", "principal", "minPrincipal", "maxPrincipal", "numberOfRepayments", "minNumberOfRepayments", - "maxNumberOfRepayments", "repaymentEvery", "repaymentFrequencyType", "interestRatePerPeriod", "minInterestRatePerPeriod", - "maxInterestRatePerPeriod", "interestRateFrequencyType", "annualInterestRate", "amortizationType", "interestType", - "interestCalculationPeriodType", "inArrearsTolerance", "transactionProcessingStrategyId", "transactionProcessingStrategyName", - "charges", "accountingRule", "accountingMappings", "paymentChannelToFundSourceMappings", "fundOptions", "paymentTypeOptions", - "currencyOptions", "repaymentFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "amortizationTypeOptions", - "interestTypeOptions", "interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", - "accountingOptions", "accountingRuleOptions", "accountingMappingOptions")); + "maxNumberOfRepayments", "repaymentEvery", "repaymentFrequencyType", "graceOnPrincipalPayment", "graceOnInterestPayment", + "graceOnInterestCharged", "interestRatePerPeriod", "minInterestRatePerPeriod", "maxInterestRatePerPeriod", + "interestRateFrequencyType", "annualInterestRate", "amortizationType", "interestType", "interestCalculationPeriodType", + "inArrearsTolerance", "transactionProcessingStrategyId", "transactionProcessingStrategyName", "charges", "accountingRule", + "accountingMappings", "paymentChannelToFundSourceMappings", "fundOptions", "paymentTypeOptions", "currencyOptions", + "repaymentFrequencyTypeOptions", "interestRateFrequencyTypeOptions", "amortizationTypeOptions", "interestTypeOptions", + "interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", "accountingOptions", + "accountingRuleOptions", "accountingMappingOptions")); private final String resourceNameForPermissions = "LOANPRODUCT"; @@ -142,7 +143,7 @@ public class LoanProductsApiResource { context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions); LoanProductData loanProduct = this.loanProductReadPlatformService.retrieveNewLoanProductDetails(); - loanProduct = handleTemplate(loanProduct, new HashMap(), null); + loanProduct = handleTemplate(loanProduct); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, loanProduct, LOAN_PRODUCT_DATA_PARAMETERS); @@ -162,16 +163,21 @@ public class LoanProductsApiResource { Map accountingMappings = null; Collection paymentChannelToFundSourceMappings = null; + Collection feeToGLAccountMappings = null; + Collection penaltyToGLAccountMappings = null; if (loanProduct.hasAccountingEnabled()) { accountingMappings = accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId, loanProduct .accountingRuleType().getId().intValue()); paymentChannelToFundSourceMappings = accountMappingReadPlatformService .fetchPaymentTypeToFundSourceMappingsForLoanProduct(productId); - loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings); + feeToGLAccountMappings = accountMappingReadPlatformService.fetchFeeToIncomeAccountMappingsForLoanProduct(productId); + penaltyToGLAccountMappings = accountMappingReadPlatformService.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId); + loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings, + feeToGLAccountMappings, penaltyToGLAccountMappings); } if (settings.isTemplate()) { - loanProduct = handleTemplate(loanProduct, accountingMappings, paymentChannelToFundSourceMappings); + loanProduct = handleTemplate(loanProduct); } return this.toApiJsonSerializer.serialize(settings, loanProduct, LOAN_PRODUCT_DATA_PARAMETERS); } @@ -190,8 +196,7 @@ public class LoanProductsApiResource { return this.toApiJsonSerializer.serialize(result); } - private LoanProductData handleTemplate(final LoanProductData productData, final Map accountingMappings, - final Collection paymentChannelToFundSourceMappings) { + private LoanProductData handleTemplate(final LoanProductData productData) { final boolean feeChargesOnly = true; Collection chargeOptions = this.chargeReadPlatformService.retrieveLoanApplicableCharges(feeChargesOnly); @@ -199,6 +204,11 @@ public class LoanProductsApiResource { chargeOptions = null; } + Collection penaltyOptions = this.chargeReadPlatformService.retrieveLoanApplicablePenalties(); + if (penaltyOptions.isEmpty()) { + penaltyOptions = null; + } + final Collection currencyOptions = currencyReadPlatformService.retrieveAllowedCurrencies(); final List amortizationTypeOptions = dropdownReadPlatformService.retrieveLoanAmortizationTypeOptions(); final List interestTypeOptions = dropdownReadPlatformService.retrieveLoanInterestTypeOptions(); @@ -222,9 +232,9 @@ public class LoanProductsApiResource { List accountingRuleTypeOptions = accountingDropdownReadPlatformService.retrieveAccountingRuleTypeOptions(); - return new LoanProductData(productData, chargeOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, - interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, - fundOptions, transactionProcessingStrategyOptions, accountOptions, accountingRuleTypeOptions, accountingMappings, - paymentChannelToFundSourceMappings); + return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, + amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, + interestRateFrequencyTypeOptions, fundOptions, transactionProcessingStrategyOptions, accountOptions, + accountingRuleTypeOptions); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java index 1f91573e9..d228f65d9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/data/LoanProductData.java @@ -14,6 +14,7 @@ import java.util.Map; import org.mifosplatform.accounting.common.AccountingEnumerations; import org.mifosplatform.accounting.common.AccountingRuleType; import org.mifosplatform.accounting.glaccount.data.GLAccountData; +import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.infrastructure.core.data.EnumOptionData; @@ -60,20 +61,25 @@ public class LoanProductData { private final BigDecimal inArrearsTolerance; private final Long transactionProcessingStrategyId; private final String transactionProcessingStrategyName; + private final Integer graceOnPrincipalPayment; + private final Integer graceOnInterestPayment; + private final Integer graceOnInterestCharged; // charges private final Collection charges; // accounting - private final EnumOptionData accountingRule; - @SuppressWarnings("unused") - private final Map accountingMappings; - @SuppressWarnings("unused") - private final Collection paymentChannelToFundSourceMappings; + private EnumOptionData accountingRule; + private Map accountingMappings; + private Collection paymentChannelToFundSourceMappings; + private Collection feeToIncomeAccountMappings; + private Collection penaltyToIncomeAccountMappings; // template related private final Collection fundOptions; + @SuppressWarnings("unused") private final Collection paymentTypeOptions; + @SuppressWarnings("unused") private final Collection currencyOptions; private final List repaymentFrequencyTypeOptions; private final List interestRateFrequencyTypeOptions; @@ -82,7 +88,11 @@ public class LoanProductData { private final List interestCalculationPeriodTypeOptions; private final Collection transactionProcessingStrategyOptions; private final Collection chargeOptions; + @SuppressWarnings("unused") + private final Collection penaltyOptions; + @SuppressWarnings("unused") private final List accountingRuleOptions; + @SuppressWarnings("unused") private final Map> accountingMappingOptions; /** @@ -112,6 +122,9 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; final Collection charges = null; final EnumOptionData accountingType = null; @@ -119,7 +132,8 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData lookupWithCurrency(final Long id, final String name, final CurrencyData currency) { @@ -145,6 +159,10 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; + final Collection charges = null; final EnumOptionData accountingType = null; @@ -152,7 +170,8 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { @@ -167,6 +186,7 @@ public class LoanProductData { final Integer numberOfRepayments = null; final Integer minNumberOfRepayments = null; final Integer maxNumberOfRepayments = null; + final Integer repaymentEvery = null; final BigDecimal interestRatePerPeriod = null; final BigDecimal minInterestRatePerPeriod = null; @@ -182,6 +202,11 @@ public class LoanProductData { final String fundName = null; final Long transactionProcessingStrategyId = null; final String transactionProcessingStrategyName = null; + + final Integer graceOnPrincipalPayment = null; + final Integer graceOnInterestPayment = null; + final Integer graceOnInterestCharged = null; + final Collection charges = null; final EnumOptionData accountingType = AccountingEnumerations.accountingRuleType(AccountingRuleType.NONE); @@ -189,17 +214,19 @@ public class LoanProductData { minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, transactionProcessingStrategyId, - transactionProcessingStrategyName, charges, accountingType); + transactionProcessingStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, + accountingType); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, - final Collection paymentChannelToFundSourceMappings) { - - return new LoanProductData(productData, productData.chargeOptions, productData.paymentTypeOptions, productData.currencyOptions, - productData.amortizationTypeOptions, productData.interestTypeOptions, productData.interestCalculationPeriodTypeOptions, - productData.repaymentFrequencyTypeOptions, productData.interestRateFrequencyTypeOptions, productData.fundOptions, - productData.transactionProcessingStrategyOptions, productData.accountingMappingOptions, productData.accountingRuleOptions, - accountingMappings, paymentChannelToFundSourceMappings); + final Collection paymentChannelToFundSourceMappings, + final Collection feeToGLAccountMappings, + final Collection penaltyToGLAccountMappings) { + productData.accountingMappings = accountingMappings; + productData.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; + productData.feeToIncomeAccountMappings = feeToGLAccountMappings; + productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; + return productData; } public LoanProductData(final Long id, final String name, final String description, final CurrencyData currency, @@ -210,6 +237,7 @@ public class LoanProductData { final EnumOptionData interestRateFrequencyType, final EnumOptionData amortizationType, final EnumOptionData interestType, final EnumOptionData interestCalculationPeriodType, final Long fundId, final String fundName, final Long transactionProcessingStrategyId, final String transactionProcessingStrategyName, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Collection charges, final EnumOptionData accountingType) { this.id = id; this.name = name; @@ -222,6 +250,9 @@ public class LoanProductData { this.numberOfRepayments = numberOfRepayments; this.minNumberOfRepayments = minNumberOfRepayments; this.maxNumberOfRepayments = maxNumberOfRepayments; + this.graceOnPrincipalPayment = graceOnPrincipalPayment; + this.graceOnInterestPayment = graceOnInterestPayment; + this.graceOnInterestCharged = graceOnInterestCharged; this.repaymentEvery = repaymentEvery; this.interestRatePerPeriod = interestRatePerPeriod; this.minInterestRatePerPeriod = minInterestRatePerPeriod; @@ -240,6 +271,7 @@ public class LoanProductData { this.accountingRule = accountingType; this.chargeOptions = null; + this.penaltyOptions = null; this.paymentTypeOptions = null; this.currencyOptions = null; this.fundOptions = null; @@ -254,16 +286,17 @@ public class LoanProductData { this.accountingRuleOptions = null; this.accountingMappings = null; this.paymentChannelToFundSourceMappings = null; + this.feeToIncomeAccountMappings = null; + this.penaltyToIncomeAccountMappings = null; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, - final Collection paymentTypeOptions, final Collection currencyOptions, - final List amortizationTypeOptions, final List interestTypeOptions, - final List interestCalculationPeriodTypeOptions, final List repaymentFrequencyTypeOptions, - final List interestRateFrequencyTypeOptions, final Collection fundOptions, - final Collection transactionStrategyOptions, - final Map> accountingMappingOptions, final List accountingRuleOptions, - final Map accountingMappings, final Collection paymentChannelToFundSourceMappings) { + final Collection penaltyOptions, final Collection paymentTypeOptions, + final Collection currencyOptions, final List amortizationTypeOptions, + final List interestTypeOptions, final List interestCalculationPeriodTypeOptions, + final List repaymentFrequencyTypeOptions, final List interestRateFrequencyTypeOptions, + final Collection fundOptions, final Collection transactionStrategyOptions, + final Map> accountingMappingOptions, final List accountingRuleOptions) { this.id = productData.id; this.name = productData.name; this.description = productData.description; @@ -290,8 +323,13 @@ public class LoanProductData { this.charges = nullIfEmpty(productData.charges()); this.accountingRule = productData.accountingRule; + this.accountingMappings = productData.accountingMappings; + this.paymentChannelToFundSourceMappings = productData.paymentChannelToFundSourceMappings; + this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings; + this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings; this.chargeOptions = chargeOptions; + this.penaltyOptions = penaltyOptions; this.paymentTypeOptions = paymentTypeOptions; this.currencyOptions = currencyOptions; this.currency = productData.currency; @@ -308,6 +346,10 @@ public class LoanProductData { this.transactionProcessingStrategyName = productData.transactionProcessingStrategyName; } + this.graceOnPrincipalPayment = productData.graceOnPrincipalPayment; + this.graceOnInterestPayment = productData.graceOnInterestPayment; + this.graceOnInterestCharged = productData.graceOnInterestCharged; + this.amortizationTypeOptions = amortizationTypeOptions; this.interestTypeOptions = interestTypeOptions; this.interestCalculationPeriodTypeOptions = interestCalculationPeriodTypeOptions; @@ -317,13 +359,6 @@ public class LoanProductData { this.accountingMappingOptions = accountingMappingOptions; this.accountingRuleOptions = accountingRuleOptions; - if (accountingMappings == null || accountingMappings.isEmpty()) { - this.accountingMappings = null; - } else { - this.accountingMappings = accountingMappings; - } - - this.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; } private Collection nullIfEmpty(final Collection charges) { @@ -418,6 +453,18 @@ public class LoanProductData { return repaymentFrequencyType; } + public Integer getGraceOnPrincipalPayment() { + return this.graceOnPrincipalPayment; + } + + public Integer getGraceOnInterestPayment() { + return this.graceOnInterestPayment; + } + + public Integer getGraceOnInterestCharged() { + return this.graceOnInterestCharged; + } + public EnumOptionData getInterestRateFrequencyType() { return interestRateFrequencyType; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java index 1779a2f47..d9d894631 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProduct.java @@ -104,12 +104,19 @@ public class LoanProduct extends AbstractPersistable { final Integer minNumberOfRepayments = command.integerValueOfParameterNamed("minNumberOfRepayments"); final Integer maxNumberOfRepayments = command.integerValueOfParameterNamed("maxNumberOfRepayments"); final BigDecimal inArrearsTolerance = command.bigDecimalValueOfParameterNamed("inArrearsTolerance"); + + // grace details + final Integer graceOnPrincipalPayment = command.integerValueOfParameterNamed("graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = command.integerValueOfParameterNamed("graceOnInterestPayment"); + final Integer graceOnInterestCharged = command.integerValueOfParameterNamed("graceOnInterestCharged"); + final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(command.integerValueOfParameterNamed("accountingRule")); return new LoanProduct(fund, loanTransactionProcessingStrategy, name, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentFrequencyType, numberOfRepayments, - minNumberOfRepayments, maxNumberOfRepayments, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType); + minNumberOfRepayments, maxNumberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, + amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType); } protected LoanProduct() { @@ -126,6 +133,7 @@ public class LoanProduct extends AbstractPersistable { final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfInstallments, final Integer defaultMinNumberOfInstallments, final Integer defaultMaxNumberOfInstallments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final List charges, final AccountingRuleType accountingRuleType) { this.fund = fund; @@ -143,7 +151,10 @@ public class LoanProduct extends AbstractPersistable { this.loanProductRelatedDetail = new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, - repaymentFrequencyType, defaultNumberOfInstallments, amortizationMethod, inArrearsTolerance); + repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, amortizationMethod, inArrearsTolerance); + + this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, @@ -274,6 +285,10 @@ public class LoanProduct extends AbstractPersistable { return this.loanProductRelatedDetail.getNominalInterestRatePerPeriod(); } + public PeriodFrequencyType getInterestPeriodFrequencyType() { + return this.loanProductRelatedDetail.getInterestPeriodFrequencyType(); + } + public BigDecimal getMinNominalInterestRatePerPeriod() { return this.loanProductMinMaxConstraints().getMinNominalInterestRatePerPeriod(); } @@ -283,7 +298,7 @@ public class LoanProduct extends AbstractPersistable { } public Integer getNumberOfRepayments() { - return this.loanProductRelatedDetail().getNumberOfRepayments(); + return this.loanProductRelatedDetail.getNumberOfRepayments(); } public Integer getMinNumberOfRepayments() { @@ -294,10 +309,6 @@ public class LoanProduct extends AbstractPersistable { return this.loanProductMinMaxConstraints().getMaxNumberOfRepayments(); } - public LoanProductRelatedDetail loanProductRelatedDetail() { - return this.loanProductRelatedDetail; - } - public LoanProductMinMaxConstraints loanProductMinMaxConstraints() { // If all min and max fields are null then loanProductMinMaxConstraints // initialising to null diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 94d4a237e..f114bf3fb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -6,7 +6,9 @@ package org.mifosplatform.portfolio.loanproduct.domain; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.persistence.Column; @@ -16,6 +18,9 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; +import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.Loan; @@ -33,7 +38,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "principal_amount", scale = 6, precision = 19, nullable = false) private BigDecimal principal; - + @Column(name = "nominal_interest_rate_per_period", scale = 6, precision = 19, nullable = false) private BigDecimal nominalInterestRatePerPeriod; @@ -69,7 +74,16 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "number_of_repayments", nullable = false) private Integer numberOfRepayments; - + + @Column(name = "grace_on_principal_periods", nullable = true) + private Integer graceOnPrincipalPayment; + + @Column(name = "grace_on_interest_periods", nullable = true) + private Integer graceOnInterestPayment; + + @Column(name = "grace_interest_free_periods", nullable = true) + private Integer graceOnInterestCharged; + // FIXME - move away form JPA ordinal use for enums using just integer - // requires sql patch for existing users of software. @Enumerated(EnumType.ORDINAL) @@ -84,11 +98,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, repaymentEvery, repaymentPeriodFrequencyType, - numberOfRepayments, amortizationMethod, inArrearsTolerance); + numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, + inArrearsTolerance); } protected LoanProductRelatedDetail() { @@ -96,10 +112,11 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche } public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal, - final BigDecimal defaultNominalInterestRatePerPeriod,final PeriodFrequencyType interestPeriodFrequencyType, + final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod, final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments, + final Integer graceOnPrincipalPayment, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance) { this.currency = currency; this.principal = defaultPrincipal; @@ -111,6 +128,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.repayEvery = repayEvery; this.repaymentPeriodFrequencyType = repaymentFrequencyType; this.numberOfRepayments = defaultNumberOfRepayments; + this.graceOnPrincipalPayment = defaultToNullIfZero(graceOnPrincipalPayment); + this.graceOnInterestPayment = defaultToNullIfZero(graceOnInterestPayment); + this.graceOnInterestCharged = defaultToNullIfZero(graceOnInterestCharged); this.amortizationMethod = amortizationMethod; if (inArrearsTolerance != null && BigDecimal.ZERO.compareTo(inArrearsTolerance) == 0) { this.inArrearsTolerance = null; @@ -119,6 +139,14 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche } } + private Integer defaultToNullIfZero(final Integer value) { + Integer defaultTo = value; + if (value != null && Integer.valueOf(0).equals(value)) { + defaultTo = null; + } + return defaultTo; + } + public MonetaryCurrency getCurrency() { return this.currency.copy(); } @@ -126,7 +154,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public Money getPrincipal() { return Money.of(this.currency, this.principal); } - + public Money getInArrearsTolerance() { return Money.of(this.currency, this.inArrearsTolerance); } @@ -165,7 +193,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public Integer getNumberOfRepayments() { return numberOfRepayments; } - + public AmortizationMethod getAmortizationMethod() { return amortizationMethod; } @@ -231,7 +259,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche actualChanges.put("locale", localeAsInput); this.repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue); } - + final String numberOfRepaymentsParamName = "numberOfRepayments"; if (command.isChangeInIntegerParameterNamed(numberOfRepaymentsParamName, this.numberOfRepayments)) { final Integer newValue = command.integerValueOfParameterNamed(numberOfRepaymentsParamName); @@ -255,7 +283,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche actualChanges.put("locale", localeAsInput); this.inArrearsTolerance = newValue; } - + final String interestRatePerPeriodParamName = "interestRatePerPeriod"; if (command.isChangeInBigDecimalParameterNamed(interestRatePerPeriodParamName, this.nominalInterestRatePerPeriod)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(interestRatePerPeriodParamName); @@ -291,9 +319,65 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.interestCalculationPeriodMethod = InterestCalculationPeriodMethod.fromInt(newValue); } + final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnPrincipalPaymentParamName, this.graceOnPrincipalPayment)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnPrincipalPaymentParamName); + actualChanges.put(graceOnPrincipalPaymentParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnPrincipalPayment = newValue; + } + + final String graceOnInterestPaymentParamName = "graceOnInterestPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestPaymentParamName, this.graceOnInterestPayment)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestPaymentParamName); + actualChanges.put(graceOnInterestPaymentParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnInterestPayment = newValue; + } + + final String graceOnInterestChargedParamName = "graceOnInterestCharged"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestChargedParamName, this.graceOnInterestCharged)) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestChargedParamName); + actualChanges.put(graceOnInterestChargedParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.graceOnInterestCharged = newValue; + } + + validateRepaymentPeriodWithGraceSettings(); + return actualChanges; } + public void validateRepaymentPeriodWithGraceSettings() { + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanproduct"); + + if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnPrincipalPayment)) { + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnInterestPayment)) { + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (this.numberOfRepayments < defaultToZeroIfNull(this.graceOnInterestCharged)) { + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + private Integer defaultToZeroIfNull(final Integer value) { + Integer result = value; + if (value == null) { + result = Integer.valueOf(0); + } + return result; + } + private void updateInterestRateDerivedFields(final AprCalculator aprCalculator) { this.annualNominalInterestRate = aprCalculator.calculateFrom(this.interestPeriodFrequencyType, this.nominalInterestRatePerPeriod); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanTransactionProcessingStrategy.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanTransactionProcessingStrategy.java index 43907793a..91e0d0711 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanTransactionProcessingStrategy.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/domain/LoanTransactionProcessingStrategy.java @@ -9,16 +9,12 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom; import org.mifosplatform.portfolio.loanproduct.data.TransactionProcessingStrategyData; -import org.mifosplatform.useradministration.domain.AppUser; +import org.springframework.data.jpa.domain.AbstractPersistable; -/** - * - */ @Entity @Table(name = "ref_loan_transaction_processing_strategy") -public class LoanTransactionProcessingStrategy extends AbstractAuditableCustom { +public class LoanTransactionProcessingStrategy extends AbstractPersistable { @Column(name = "code", unique = true) private String code; @@ -49,4 +45,12 @@ public class LoanTransactionProcessingStrategy extends AbstractAuditableCustom 0) { + int i = 0; + do { + final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject(); + final Long chargeId = fromApiJsonHelper.extractLongNamed(LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue(), + jsonObject); + final Long incomeAccountId = fromApiJsonHelper.extractLongNamed( + LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue(), jsonObject); + baseDataValidator.reset() + .parameter(parameterName + "[" + i + "]." + LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue()) + .value(chargeId).notNull().integerGreaterThanZero(); + baseDataValidator.reset() + .parameter(parameterName + "[" + i + "]." + LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue()) + .value(incomeAccountId).notNull().integerGreaterThanZero(); + i++; + } while (i < chargeToIncomeAccountMappingArray.size()); + } + } + } + public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator, final LoanProduct loanProduct) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 71dad3c11..f702558f8 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -101,6 +101,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.nominal_interest_rate_per_period as interestRatePerPeriod, lp.min_nominal_interest_rate_per_period as minInterestRatePerPeriod, lp.max_nominal_interest_rate_per_period as maxInterestRatePerPeriod, lp.interest_period_frequency_enum as interestRatePerPeriodFreq, " + "lp.annual_nominal_interest_rate as annualInterestRate, lp.interest_method_enum as interestMethod, lp.interest_calculated_in_period_enum as interestCalculationInPeriodMethod," + "lp.repay_every as repaidEvery, lp.repayment_period_frequency_enum as repaymentPeriodFrequency, lp.number_of_repayments as numberOfRepayments, lp.min_number_of_repayments as minNumberOfRepayments, lp.max_number_of_repayments as maxNumberOfRepayments, " + + "lp.grace_on_principal_periods as graceOnPrincipalPayment, lp.grace_on_interest_periods as graceOnInterestPayment, lp.grace_interest_free_periods as graceOnInterestCharged," + "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, " + "lp.accounting_type as accountingType, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol " @@ -140,6 +141,10 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Integer maxNumberOfRepayments = JdbcSupport.getInteger(rs, "maxNumberOfRepayments"); final Integer repaymentEvery = JdbcSupport.getInteger(rs, "repaidEvery"); + final Integer graceOnPrincipalPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnPrincipalPayment"); + final Integer graceOnInterestPayment = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestPayment"); + final Integer graceOnInterestCharged = JdbcSupport.getIntegerDefaultToNullIfZero(rs, "graceOnInterestCharged"); + final Integer accountingRuleId = JdbcSupport.getInteger(rs, "accountingType"); final EnumOptionData accountingRuleType = AccountingEnumerations.accountingRuleType(accountingRuleId); @@ -168,7 +173,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, fundId, fundName, - transactionStrategyId, transactionStrategyName, this.charges, accountingRuleType); + transactionStrategyId, transactionStrategyName, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, this.charges, accountingRuleType); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 379df3185..75de974fe 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -28,7 +28,7 @@ import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductCommandFromApiJsonDeserializer; +import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,7 +44,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro private final static Logger logger = LoggerFactory.getLogger(LoanProductWritePlatformServiceJpaRepositoryImpl.class); private final PlatformSecurityContext context; - private final LoanProductCommandFromApiJsonDeserializer fromApiJsonDeserializer; + private final LoanProductDataValidator fromApiJsonDeserializer; private final LoanProductRepository loanProductRepository; private final AprCalculator aprCalculator; private final FundRepository fundRepository; @@ -54,7 +54,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro @Autowired public LoanProductWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, - final LoanProductCommandFromApiJsonDeserializer fromApiJsonDeserializer, final LoanProductRepository loanProductRepository, + final LoanProductDataValidator fromApiJsonDeserializer, final LoanProductRepository loanProductRepository, final AprCalculator aprCalculator, final FundRepository fundRepository, final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository, final ChargeRepositoryWrapper chargeRepository, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java index 7bb69f3a4..fc48527f6 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/api/SavingsAccountsApiResource.java @@ -34,7 +34,9 @@ import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.exception.UnrecognizedQueryParamException; import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.mifosplatform.infrastructure.core.service.Page; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; import org.mifosplatform.portfolio.savings.data.SavingsProductData; @@ -93,11 +95,16 @@ public class SavingsAccountsApiResource { @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - public String retrieveAll(@Context final UriInfo uriInfo) { + public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("sqlSearch") final String sqlSearch, + @QueryParam("externalId") final String externalId, @QueryParam("underHierarchy") final String hierarchy, + @QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit, + @QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder) { context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); - final Collection products = this.savingsAccountReadPlatformService.retrieveAll(); + final SearchParameters searchParameters = SearchParameters.forLoans(sqlSearch, externalId, offset, limit, orderBy, sortOrder); + + final Page products = this.savingsAccountReadPlatformService.retrieveAll(searchParameters); final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializer.serialize(settings, products, SavingsApiConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountStatusEnumData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountStatusEnumData.java index 444bc5a05..9379d10d5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountStatusEnumData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountStatusEnumData.java @@ -10,7 +10,6 @@ package org.mifosplatform.portfolio.savings.data; */ public class SavingsAccountStatusEnumData { - @SuppressWarnings("unused") private final Long id; @SuppressWarnings("unused") private final String code; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java index eff523c7a..a21253e49 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/InterestCompoundingPeriodSummary.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.MathContext; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index 6db422eb3..ec8d1cf10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -46,6 +46,7 @@ import org.joda.time.MonthDay; import org.joda.time.format.DateTimeFormatter; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException; import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.security.service.RandomPasswordGenerator; @@ -163,6 +164,7 @@ public class SavingsAccount extends AbstractPersistable { @Column(name = "annual_fee_on_day", nullable = true) private Integer annualFeeOnDay; + @SuppressWarnings("unused") @Temporal(TemporalType.DATE) @Column(name = "annual_fee_next_due_date", nullable = true) private Date annualFeeNextDueDate; @@ -276,8 +278,8 @@ public class SavingsAccount extends AbstractPersistable { return SavingsAccountStatusType.fromInt(this.status).isActive(); } - public void activate(final DateTimeFormatter formatter, final LocalDate activationDate, List existingTransactionIds, - List existingReversedTransactionIds) { + public void activate(final DateTimeFormatter formatter, final LocalDate activationDate, final List existingTransactionIds, + final List existingReversedTransactionIds) { if (isActive()) { final String defaultUserMessage = "Cannot activate account. Account is already active."; @@ -352,8 +354,8 @@ public class SavingsAccount extends AbstractPersistable { return lockedInUntilLocalDate; } - public void postInterest(final LocalDate interestPostingUpToDate, List existingTransactionIds, - List existingReversedTransactionIds) { + public void postInterest(final LocalDate interestPostingUpToDate, final List existingTransactionIds, + final List existingReversedTransactionIds) { final SavingsInterestPostingPeriodType postingPeriodType = SavingsInterestPostingPeriodType.fromInt(this.interestPostingPeriodType); final List postingLocalDates = determineInterestPostingDates(getActivationLocalDate(), interestPostingUpToDate, @@ -756,7 +758,7 @@ public class SavingsAccount extends AbstractPersistable { } public SavingsAccountTransaction deposit(final DateTimeFormatter formatter, final LocalDate transactionDate, - final BigDecimal transactionAmount, List existingTransactionIds, List existingReversedTransactionIds, + final BigDecimal transactionAmount, final List existingTransactionIds, final List existingReversedTransactionIds, final PaymentDetail paymentDetail) { if (isNotActive()) { @@ -819,7 +821,7 @@ public class SavingsAccount extends AbstractPersistable { } public SavingsAccountTransaction withdraw(final DateTimeFormatter formatter, final LocalDate transactionDate, - final BigDecimal transactionAmount, List existingTransactionIds, List existingReversedTransactionIds, + final BigDecimal transactionAmount, final List existingTransactionIds, final List existingReversedTransactionIds, final PaymentDetail paymentDetail) { if (isNotActive()) { @@ -915,7 +917,8 @@ public class SavingsAccount extends AbstractPersistable { return transaction; } - public SavingsAccountTransaction addAnnualFee(final DateTimeFormatter formatter, final LocalDate transactionDate) { + public SavingsAccountTransaction addAnnualFee(final DateTimeFormatter formatter, final LocalDate transactionDate, + final List existingTransactionIds, final List existingReversedTransactionIds) { if (isNotActive()) { @@ -966,6 +969,9 @@ public class SavingsAccount extends AbstractPersistable { throw new PlatformApiDataValidationException(dataValidationErrors); } + existingTransactionIds.addAll(findExistingTransactionIds()); + existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + final Money annualFee = Money.of(this.currency, this.annualFeeAmount); SavingsAccountTransaction annualFeeTransaction = SavingsAccountTransaction.annualFee(this, transactionDate, annualFee); this.transactions.add(annualFeeTransaction); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java index de2241e57..96a6b13ee 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountDailyBalance.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java index d4eff548a..4851d5c8a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountTransaction.java @@ -19,6 +19,7 @@ import javax.persistence.Temporal; import javax.persistence.TemporalType; import org.joda.time.LocalDate; +import org.mifosplatform.infrastructure.core.domain.LocalDateInterval; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java index e6071f8f2..f8883cdc9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/exception/InvalidSavingsAccountStateTransitionException.java @@ -1,11 +1,17 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.mifosplatform.portfolio.savings.exception; import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; public class InvalidSavingsAccountStateTransitionException extends AbstractPlatformDomainRuleException { - public InvalidSavingsAccountStateTransitionException(final String action, String postFix, String defaultUserMessage, Object... defaultUserMessageArgs) { - super("error.msg.saving." + action + "." + postFix, defaultUserMessage, defaultUserMessageArgs); - } + public InvalidSavingsAccountStateTransitionException(final String action, final String postFix, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + super("error.msg.saving." + action + "." + postFix, defaultUserMessage, defaultUserMessageArgs); + } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java index 327c952ee..e8a9a6e14 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformService.java @@ -7,12 +7,14 @@ package org.mifosplatform.portfolio.savings.service; import java.util.Collection; +import org.mifosplatform.infrastructure.core.service.Page; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountTransactionData; public interface SavingsAccountReadPlatformService { - Collection retrieveAll(); + Page retrieveAll(SearchParameters searchParameters); SavingsAccountData retrieveOne(Long savingsId); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index ef142ec0f..af9e35ecb 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -8,14 +8,18 @@ package org.mifosplatform.portfolio.savings.service; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collection; +import org.apache.commons.lang.StringUtils; import org.joda.time.LocalDate; import org.joda.time.MonthDay; import org.mifosplatform.infrastructure.codes.data.CodeValueData; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.DateUtils; +import org.mifosplatform.infrastructure.core.service.Page; +import org.mifosplatform.infrastructure.core.service.PaginationHelper; import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.organisation.monetary.data.CurrencyData; @@ -23,6 +27,7 @@ import org.mifosplatform.portfolio.client.data.ClientData; import org.mifosplatform.portfolio.client.service.ClientReadPlatformService; import org.mifosplatform.portfolio.group.data.GroupGeneralData; import org.mifosplatform.portfolio.group.service.GroupReadPlatformService; +import org.mifosplatform.portfolio.group.service.SearchParameters; import org.mifosplatform.portfolio.paymentdetail.data.PaymentDetailData; import org.mifosplatform.portfolio.savings.data.SavingsAccountData; import org.mifosplatform.portfolio.savings.data.SavingsAccountStatusEnumData; @@ -36,6 +41,7 @@ import org.mifosplatform.portfolio.savings.domain.SavingsInterestCalculationType import org.mifosplatform.portfolio.savings.domain.SavingsInterestPostingPeriodType; import org.mifosplatform.portfolio.savings.domain.SavingsPeriodFrequencyType; import org.mifosplatform.portfolio.savings.exception.SavingsAccountNotFoundException; +import org.mifosplatform.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -53,6 +59,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead private final SavingsDropdownReadPlatformService dropdownReadPlatformService; private final SavingsAccountTransactionTemplateMapper transactionTemplateMapper; private final SavingsAccountTransactionsMapper transactionsMapper; + + private final PaginationHelper paginationHelper = new PaginationHelper(); + private final SavingAccountMapper savingAccountMapper = new SavingAccountMapper(); @Autowired public SavingsAccountReadPlatformServiceImpl(final PlatformSecurityContext context, final TenantAwareRoutingDataSource dataSource, @@ -70,12 +79,54 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead } @Override - public Collection retrieveAll() { + public Page retrieveAll(final SearchParameters searchParameters) { - this.context.authenticatedUser(); - final SavingAccountMapper mapper = new SavingAccountMapper(); - final String sql = "select " + mapper.schema(); - return this.jdbcTemplate.query(sql, mapper, new Object[] {}); + final AppUser currentUser = context.authenticatedUser(); + final String hierarchy = currentUser.getOffice().getHierarchy(); + final String hierarchySearchString = hierarchy + "%"; + + StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder.append("select SQL_CALC_FOUND_ROWS "); + sqlBuilder.append(savingAccountMapper.schema()); + + sqlBuilder.append(" join m_office o on o.id = c.office_id"); + sqlBuilder.append(" where o.hierarchy like ?"); + + final Object[] objectArray = new Object[2]; + objectArray[0] = hierarchySearchString; + int arrayPos = 1; + + String sqlQueryCriteria = searchParameters.getSqlSearch(); + if (StringUtils.isNotBlank(sqlQueryCriteria)) { + sqlQueryCriteria = sqlQueryCriteria.replaceAll("accountNo", "sa.account_no"); + sqlBuilder.append(" and (").append(sqlQueryCriteria).append(")"); + } + + if (StringUtils.isNotBlank(searchParameters.getExternalId())) { + sqlBuilder.append(" and sa.external_id = ?"); + objectArray[arrayPos] = searchParameters.getExternalId(); + arrayPos = arrayPos + 1; + } + + if (searchParameters.isOrderByRequested()) { + sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); + + if (searchParameters.isSortOrderProvided()) { + sqlBuilder.append(' ').append(searchParameters.getSortOrder()); + } + } + + if (searchParameters.isLimited()) { + sqlBuilder.append(" limit ").append(searchParameters.getLimit()); + if (searchParameters.isOffset()) { + sqlBuilder.append(" offset ").append(searchParameters.getOffset()); + } + } + + final Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos); + final String sqlCountRows = "SELECT FOUND_ROWS()"; + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlCountRows, sqlBuilder.toString(), finalObjectArray, + this.savingAccountMapper); } @Override diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java index 51a760d2f..9f09846f2 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -21,6 +21,8 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult deposit(Long savingsId, JsonCommand command); CommandProcessingResult withdrawal(Long savingsId, JsonCommand command); + + CommandProcessingResult applyAnnualFee(Long savingsId); CommandProcessingResult calculateInterest(Long savingsId, JsonCommand command); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 9ccbbfacb..0b9adcf74 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -359,6 +359,35 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .build(); } + @Transactional + @Override + public CommandProcessingResult applyAnnualFee(final Long savingsId) { + + final SavingsAccount account = this.savingAccountRepository.findOneWithNotFoundDetection(savingsId); + + final LocalDate transactionDate = LocalDate.now(); + + final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy"); + + final List existingTransactionIds = new ArrayList(); + final List existingReversedTransactionIds = new ArrayList(); + + final SavingsAccountTransaction annualFee = account.addAnnualFee(fmt, transactionDate, existingTransactionIds, + existingReversedTransactionIds); + final Long transactionId = saveTransactionToGenerateTransactionId(annualFee); + this.savingAccountRepository.save(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(transactionId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + @Override public CommandProcessingResult calculateInterest(final Long savingsId, final JsonCommand command) { this.context.authenticatedUser(); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyAnnualFeeForSavingsScheduledJob.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyAnnualFeeForSavingsScheduledJob.java new file mode 100644 index 000000000..d266bb6fa --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/ApplyAnnualFeeForSavingsScheduledJob.java @@ -0,0 +1,31 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.scheduledjobs; + +import java.util.Date; + +import org.mifosplatform.scheduledjobs.service.ScheduledJobRunnerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApplyAnnualFeeForSavingsScheduledJob { + + private final static Logger logger = LoggerFactory.getLogger(ApplyAnnualFeeForSavingsScheduledJob.class); + + private final ScheduledJobRunnerService scheduledJobRunnerService; + + public ApplyAnnualFeeForSavingsScheduledJob(final ScheduledJobRunnerService scheduledJobRunnerService) { + this.scheduledJobRunnerService = scheduledJobRunnerService; + } + + public void execute() { + logger.info("Running Job ApplyAnnualFeeForSavingsScheduledJob " + new Date()); + + this.scheduledJobRunnerService.applyAnnualFeeForSavings(); + + logger.info("Finishing Job ApplyAnnualFeeForSavingsScheduledJob " + new Date()); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java index 327f48cd9..c335f3913 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerService.java @@ -10,4 +10,6 @@ public interface ScheduledJobRunnerService { void updateLoanSummaryDetails(); void updateLoanArrearsAgeingDetails(); + + void applyAnnualFeeForSavings(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java index 2a4fab657..fd517a8d5 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/scheduledjobs/service/ScheduledJobRunnerServiceImpl.java @@ -5,10 +5,13 @@ */ package org.mifosplatform.scheduledjobs.service; +import java.util.List; + import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant; import org.mifosplatform.infrastructure.core.service.DataSourcePerTenantService; import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil; import org.mifosplatform.infrastructure.security.service.TenantDetailsService; +import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -23,18 +26,20 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService private final TenantDetailsService tenantDetailsService; private final DataSourcePerTenantService dataSourcePerTenantService; + private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; @Autowired public ScheduledJobRunnerServiceImpl(final DataSourcePerTenantService dataSourcePerTenantService, - final TenantDetailsService tenantDetailsService) { + final TenantDetailsService tenantDetailsService, final SavingsAccountWritePlatformService savingsAccountWritePlatformService) { this.dataSourcePerTenantService = dataSourcePerTenantService; this.tenantDetailsService = tenantDetailsService; + this.savingsAccountWritePlatformService = savingsAccountWritePlatformService; } @Transactional @Override public void updateLoanSummaryDetails() { - + final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); ThreadLocalContextUtil.setTenant(tenant); @@ -65,43 +70,57 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService updateSqlBuilder.append("WHERE ml.disbursedon_date is not null "); updateSqlBuilder.append("GROUP BY ml.id "); updateSqlBuilder.append(") x on x.loanId = m_loan.id "); - + updateSqlBuilder.append("SET m_loan.principal_disbursed_derived = x.principal_disbursed_derived,"); updateSqlBuilder.append("m_loan.principal_repaid_derived = x.principal_repaid_derived,"); updateSqlBuilder.append("m_loan.principal_writtenoff_derived = x.principal_writtenoff_derived,"); - updateSqlBuilder.append("m_loan.principal_outstanding_derived = (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)),"); + updateSqlBuilder + .append("m_loan.principal_outstanding_derived = (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)),"); updateSqlBuilder.append("m_loan.interest_charged_derived = x.interest_charged_derived,"); updateSqlBuilder.append("m_loan.interest_repaid_derived = x.interest_repaid_derived,"); updateSqlBuilder.append("m_loan.interest_waived_derived = x.interest_waived_derived,"); updateSqlBuilder.append("m_loan.interest_writtenoff_derived = x.interest_writtenoff_derived,"); - updateSqlBuilder.append("m_loan.interest_outstanding_derived = (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)),"); + updateSqlBuilder + .append("m_loan.interest_outstanding_derived = (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)),"); updateSqlBuilder.append("m_loan.fee_charges_charged_derived = x.fee_charges_charged_derived,"); updateSqlBuilder.append("m_loan.fee_charges_repaid_derived = x.fee_charges_repaid_derived,"); updateSqlBuilder.append("m_loan.fee_charges_waived_derived = x.fee_charges_waived_derived,"); updateSqlBuilder.append("m_loan.fee_charges_writtenoff_derived = x.fee_charges_writtenoff_derived,"); - updateSqlBuilder.append("m_loan.fee_charges_outstanding_derived = (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)),"); + updateSqlBuilder + .append("m_loan.fee_charges_outstanding_derived = (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)),"); updateSqlBuilder.append("m_loan.penalty_charges_charged_derived = x.penalty_charges_charged_derived,"); updateSqlBuilder.append("m_loan.penalty_charges_repaid_derived = x.penalty_charges_repaid_derived,"); updateSqlBuilder.append("m_loan.penalty_charges_waived_derived = x.penalty_charges_waived_derived,"); updateSqlBuilder.append("m_loan.penalty_charges_writtenoff_derived = x.penalty_charges_writtenoff_derived,"); - updateSqlBuilder.append("m_loan.penalty_charges_outstanding_derived = (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived)),"); - updateSqlBuilder.append("m_loan.total_expected_repayment_derived = (x.principal_disbursed_derived + x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); - updateSqlBuilder.append("m_loan.total_repayment_derived = (x.principal_repaid_derived + x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); - updateSqlBuilder.append("m_loan.total_expected_costofloan_derived = (x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); - updateSqlBuilder.append("m_loan.total_costofloan_derived = (x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); - updateSqlBuilder.append("m_loan.total_waived_derived = (x.interest_waived_derived + x.fee_charges_waived_derived + x.penalty_charges_waived_derived),"); - updateSqlBuilder.append("m_loan.total_writtenoff_derived = (x.interest_writtenoff_derived + x.fee_charges_writtenoff_derived + x.penalty_charges_writtenoff_derived),"); + updateSqlBuilder + .append("m_loan.penalty_charges_outstanding_derived = (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived)),"); + updateSqlBuilder + .append("m_loan.total_expected_repayment_derived = (x.principal_disbursed_derived + x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); + updateSqlBuilder + .append("m_loan.total_repayment_derived = (x.principal_repaid_derived + x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); + updateSqlBuilder + .append("m_loan.total_expected_costofloan_derived = (x.interest_charged_derived + x.fee_charges_charged_derived + x.penalty_charges_charged_derived),"); + updateSqlBuilder + .append("m_loan.total_costofloan_derived = (x.interest_repaid_derived + x.fee_charges_repaid_derived + x.penalty_charges_repaid_derived),"); + updateSqlBuilder + .append("m_loan.total_waived_derived = (x.interest_waived_derived + x.fee_charges_waived_derived + x.penalty_charges_waived_derived),"); + updateSqlBuilder + .append("m_loan.total_writtenoff_derived = (x.interest_writtenoff_derived + x.fee_charges_writtenoff_derived + x.penalty_charges_writtenoff_derived),"); updateSqlBuilder.append("m_loan.total_outstanding_derived="); updateSqlBuilder.append(" (x.principal_disbursed_derived - (x.principal_repaid_derived + x.principal_writtenoff_derived)) + "); - updateSqlBuilder.append(" (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)) +"); - updateSqlBuilder.append(" (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)) +"); - updateSqlBuilder.append(" (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived))"); - + updateSqlBuilder + .append(" (x.interest_charged_derived - (x.interest_repaid_derived + x.interest_waived_derived + x.interest_writtenoff_derived)) +"); + updateSqlBuilder + .append(" (x.fee_charges_charged_derived - (x.fee_charges_repaid_derived + x.fee_charges_waived_derived + x.fee_charges_writtenoff_derived)) +"); + updateSqlBuilder + .append(" (x.penalty_charges_charged_derived - (x.penalty_charges_repaid_derived + x.penalty_charges_waived_derived + x.penalty_charges_writtenoff_derived))"); + int result = jdbcTemplate.update(updateSqlBuilder.toString()); logger.info("Results affected by update: " + result); } + @Transactional @Override public void updateLoanArrearsAgeingDetails() { final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); @@ -112,19 +131,24 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService jdbcTemplate.execute("truncate table m_loan_arrears_aging"); - StringBuilder updateSqlBuilder = new StringBuilder(900); - - updateSqlBuilder.append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)"); + + updateSqlBuilder + .append("INSERT INTO m_loan_arrears_aging(`loan_id`,`principal_overdue_derived`,`interest_overdue_derived`,`fee_charges_overdue_derived`,`penalty_charges_overdue_derived`,`total_overdue_derived`,`overdue_since_date_derived`)"); updateSqlBuilder.append("select ml.id as loanId,"); - updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,"); - updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,"); - updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,"); - updateSqlBuilder.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) as principal_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) as interest_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) as fee_charges_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as penalty_charges_overdue_derived,"); updateSqlBuilder.append("SUM((ifnull(mr.principal_amount,0) - ifnull(mr.principal_completed_derived, 0))) +"); updateSqlBuilder.append("SUM((ifnull(mr.interest_amount,0) - ifnull(mr.interest_completed_derived, 0))) +"); updateSqlBuilder.append("SUM((ifnull(mr.fee_charges_amount,0) - ifnull(mr.fee_charges_completed_derived, 0))) +"); - updateSqlBuilder.append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,"); + updateSqlBuilder + .append("SUM((ifnull(mr.penalty_charges_amount,0) - ifnull(mr.penalty_charges_completed_derived, 0))) as total_overdue_derived,"); updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived "); updateSqlBuilder.append(" FROM m_loan ml "); updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id "); @@ -132,9 +156,28 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService updateSqlBuilder.append(" and mr.completed_derived is false "); updateSqlBuilder.append(" and mr.duedate < CURDATE() "); updateSqlBuilder.append(" GROUP BY ml.id"); - + int result = jdbcTemplate.update(updateSqlBuilder.toString()); logger.info("Results affected by update: " + result); } + + @Transactional + @Override + public void applyAnnualFeeForSavings() { + final MifosPlatformTenant tenant = this.tenantDetailsService.loadTenantById("default"); + + ThreadLocalContextUtil.setTenant(tenant); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourcePerTenantService.retrieveTenantAwareDataSource()); + + String sql = "SELECT ms.id as savingsAccountId FROM m_savings_account ms WHERE ms.annual_fee_next_due_date <= NOW() "; + List savingsIds = jdbcTemplate.queryForList(sql, Long.class); + + for (Long savingsId : savingsIds) { + this.savingsAccountWritePlatformService.applyAnnualFee(savingsId); + } + + logger.info("Results affected by update: " + savingsIds.size()); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java index 5644803b5..befd04791 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/useradministration/domain/AppUser.java @@ -25,10 +25,12 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; import org.mifosplatform.infrastructure.core.api.JsonCommand; +import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.security.domain.PlatformUser; import org.mifosplatform.infrastructure.security.exception.NoAuthorizationException; import org.mifosplatform.infrastructure.security.service.PlatformPasswordEncoder; import org.mifosplatform.organisation.office.domain.Office; +import org.mifosplatform.organisation.staff.domain.Staff; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.jpa.domain.AbstractPersistable; @@ -77,9 +79,13 @@ public class AppUser extends AbstractPersistable implements PlatformUser { private boolean deleted; @ManyToOne - @JoinColumn(name = "office_id") + @JoinColumn(name = "office_id", nullable = false) private Office office; + @ManyToOne + @JoinColumn(name = "staff_id", nullable = true) + private Staff staff; + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "m_appuser_role", joinColumns = @JoinColumn(name = "appuser_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; @@ -132,6 +138,14 @@ public class AppUser extends AbstractPersistable implements PlatformUser { this.firstTimeLoginRemaining = true; } + public EnumOptionData organisationalRoleData() { + EnumOptionData organisationalRole = null; + if (this.staff != null) { + organisationalRole = this.staff.organisationalRoleData(); + } + return organisationalRole; + } + public void updatePassword(final String encodePassword) { this.password = encodePassword; this.firstTimeLoginRemaining = false; @@ -451,4 +465,20 @@ public class AppUser extends AbstractPersistable implements PlatformUser { if (hasNotPermissionForDatatable(datatable, "READ")) { throw new NoAuthorizationException("Not authorised to read datatable: " + datatable); } } + + public Long getStaffId() { + Long staffId = null; + if (this.staff != null) { + staffId = this.staff.getId(); + } + return staffId; + } + + public String getStaffDisplayName() { + String staffDisplayName = null; + if (this.staff != null) { + staffDisplayName = this.staff.displayName(); + } + return staffDisplayName; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml index 4adb9767d..03411a951 100644 --- a/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml +++ b/mifosng-provider/src/main/resources/META-INF/spring/scheduler.xml @@ -6,47 +6,78 @@ You can obtain one at http://mozilla.org/MPL/2.0/. --> + + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - - - - - - + + + + + + + + + - - - - - - - - - - - - - + + + + + + - - - - - - - - - + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/AdhikarLoanRepaymentScheduleTransactionProcessorTest.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/AdhikarLoanRepaymentScheduleTransactionProcessorTest.java index c266c8a27..6c2a93abf 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/AdhikarLoanRepaymentScheduleTransactionProcessorTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/AdhikarLoanRepaymentScheduleTransactionProcessorTest.java @@ -12,7 +12,7 @@ import org.junit.runner.RunWith; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.mifosplatform.portfolio.loanaccount.domain.RBILoanRepaymentScheduleTransactionProcessor; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; import org.mockito.runners.MockitoJUnitRunner; @SuppressWarnings("unused") diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/DecliningBalanceMethodLoanScheduleGeneratorTest.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/DecliningBalanceMethodLoanScheduleGeneratorTest.java deleted file mode 100644 index 72820ce8d..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/DecliningBalanceMethodLoanScheduleGeneratorTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.mifosplatform.portfolio.loanaccount; - -//import static org.hamcrest.CoreMatchers.*; -//import static org.junit.Assert.*; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.DecliningBalanceMethodLoanScheduleGenerator; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mockito.runners.MockitoJUnitRunner; - -@SuppressWarnings("unused") -@RunWith(MockitoJUnitRunner.class) -public class DecliningBalanceMethodLoanScheduleGeneratorTest { - - // class under test - private DecliningBalanceMethodLoanScheduleGenerator decliningBalanceGenerator; - - // test doubles - private CurrencyData currencyData = null; - private LoanProductRelatedDetail loanScheduleInfo = null; - - @Before - public void setupForEachTest() { - currencyData = new CurrencyData("USD", "US Dollar", 2, "$", "currency.USD"); - - decliningBalanceGenerator = new DecliningBalanceMethodLoanScheduleGenerator(); - } - - @Test - public void testVoid() { - - } - -// @Test -// public void givenEqualInstallmentAmortisationShouldCalculatePaymentPerInstallmentUsingPmtFunctionAndDerivePrincipalAndInterestComponents() { -// -// // setup -// Integer loanTermFrequency = 12; -// PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; -// loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForEqualInstallmentAmortizationQuarterly(); -// -// LocalDate disbursementDate = new LocalDate(2011, 1, 11); -// LocalDate firstRepaymentDate = new LocalDate(2011, 5, 1); -// LocalDate interestCalculatedFrom = new LocalDate(2011, 2, 1); -// -// // pre assertions -// assertThat(loanTermFrequency, is(loanScheduleInfo.getNumberOfRepayments() * loanScheduleInfo.getRepayEvery())); -// assertThat(loanTermFrequencyType, is(loanScheduleInfo.getRepaymentPeriodFrequencyType())); -// -// // exercise test -// LoanSchedule schedule = decliningBalanceGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, -// disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); -// -// // verification (post assertions) -// given(schedule).assertHasInstallmentSizeOf(4); -// given(schedule).assertTotalPaymentDueOf(1, is("57718.30")).assertInterestDueComponentOf(1, is("12000.00")).assertPrincipalDueComponentOf(1, is("45718.30")); -// given(schedule).assertTotalPaymentDueOf(2, is("57718.30")).assertInterestDueComponentOf(2, is("9256.90")).assertPrincipalDueComponentOf(2, is("48461.40")); -// given(schedule).assertTotalPaymentDueOf(3, is("57718.30")).assertInterestDueComponentOf(3, is("6349.22")).assertPrincipalDueComponentOf(3, is("51369.08")); -// given(schedule).assertTotalPaymentDueOf(4, is("57718.29")).assertInterestDueComponentOf(4, is("3267.07")).assertPrincipalDueComponentOf(4, is("54451.22")); -// } -// -// @Test -// public void givenEqualPrincipalAmortisationShouldCalculateInterestComponentFromOutstandingPrincipalBlanceResultingInDecliningInterestAndTotalRepayments() { -// -// // setup -// Integer loanTermFrequency = 12; -// PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; -// loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForEqualPrincipalAmortizationQuarterly(); -// -// LocalDate disbursementDate = new LocalDate(2011, 1, 11); -// LocalDate firstRepaymentDate = new LocalDate(2011, 5, 1); -// LocalDate interestCalculatedFrom = new LocalDate(2011, 2, 1); -// -// // pre assertions -// assertThat(loanTermFrequency, is(loanScheduleInfo.getNumberOfRepayments() * loanScheduleInfo.getRepayEvery())); -// assertThat(loanTermFrequencyType, is(loanScheduleInfo.getRepaymentPeriodFrequencyType())); -// -// // exercise test -// LoanSchedule schedule = decliningBalanceGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, -// disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); -// -// // verification (post assertions) -// given(schedule).assertHasInstallmentSizeOf(4); -// given(schedule).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("9000.00")).assertTotalPaymentDueOf(2, is("59000.00")); -// given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("6000.00")).assertTotalPaymentDueOf(3, is("56000.00")); -// given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("3000.00")).assertTotalPaymentDueOf(4, is("53000.00")); -// } -// -// private LoanScheduleAssertionBuilder given(final LoanSchedule schedule) { -// return new LoanScheduleAssertionBuilder(schedule); -// } -// -// private class LoanScheduleAssertionBuilder { -// -// private final LoanSchedule schedule; -// -// public LoanScheduleAssertionBuilder(final LoanSchedule schedule) { -// this.schedule = schedule; -// } -// -// public LoanScheduleAssertionBuilder assertPrincipalDueComponentOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected principal due component for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getPrincipalDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertInterestDueComponentOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected interest due component for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getInterestDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertTotalPaymentDueOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected total payment for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getTotalInstallmentDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertHasInstallmentSizeOf(int expectedInstallmentSize) { -// assertThat("Expected number of installments is wrong: ", schedule.getScheduledLoanInstallments().size(), is(expectedInstallmentSize)); -// return this; -// } -// } -} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/FlatMethodLoanScheduleGeneratorTest.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/FlatMethodLoanScheduleGeneratorTest.java deleted file mode 100644 index b217e14d9..000000000 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/FlatMethodLoanScheduleGeneratorTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.mifosplatform.portfolio.loanaccount; - -//import static org.hamcrest.CoreMatchers.*; -//import static org.junit.Assert.*; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mifosplatform.organisation.monetary.data.CurrencyData; -import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.FlatMethodLoanScheduleGenerator; -import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.mockito.runners.MockitoJUnitRunner; - -@SuppressWarnings("unused") -@RunWith(MockitoJUnitRunner.class) -public class FlatMethodLoanScheduleGeneratorTest { - - // class under test - private FlatMethodLoanScheduleGenerator flatLoanScheduleGenerator; - - // test doubles - private CurrencyData currencyData = null; - private LoanProductRelatedDetail loanScheduleInfo = null; - - @Before - public void setupForEachTest() { - currencyData = new CurrencyData("USD", "US Dollar", 2, "$", "currency.USD"); - - flatLoanScheduleGenerator = new FlatMethodLoanScheduleGenerator(); - } - - @Test - public void rewriteAllTests() { - - } - -// @Test -// public void givenFlatMethodShouldCalculateFlatInterestComponentToDerivePrincipalAndTotalRepaymentRegardlessOfEqualInstallmentAmortisation() { -// -// // setup -// Integer loanTermFrequency = 12; -// PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; -// loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForFlatQuarterly(AmortizationMethod.EQUAL_INSTALLMENTS); -// -// LocalDate disbursementDate = new LocalDate(2011, 1, 11); -// LocalDate firstRepaymentDate = new LocalDate(2011, 5, 1); -// LocalDate interestCalculatedFrom = new LocalDate(2011, 2, 1); -// -// // pre assertions -// assertThat(loanTermFrequency, is(loanScheduleInfo.getNumberOfRepayments() * loanScheduleInfo.getRepayEvery())); -// assertThat(loanTermFrequencyType, is(loanScheduleInfo.getRepaymentPeriodFrequencyType())); -// -// // exercise test -// LoanSchedule schedule = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, -// disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); -// -// // verification (post assertions) -// given(schedule).assertHasInstallmentSizeOf(4); -// given(schedule).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("12000.00")).assertTotalPaymentDueOf(2, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("12000.00")).assertTotalPaymentDueOf(3, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("12000.00")).assertTotalPaymentDueOf(4, is("62000.00")); -// } -// -// @Test -// public void givenFlatMethodShouldCalculateFlatInterestComponentToDerivePrincipalAndTotalRepaymentRegardlessOfEqualPrincipalAmortisation() { -// -// // setup -// Integer loanTermFrequency = 12; -// PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; -// loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForFlatQuarterly(AmortizationMethod.EQUAL_PRINCIPAL); -// -// LocalDate disbursementDate = new LocalDate(2011, 1, 11); -// LocalDate firstRepaymentDate = new LocalDate(2011, 5, 1); -// LocalDate interestCalculatedFrom = new LocalDate(2011, 2, 1); -// -// // pre assertions -// assertThat(loanTermFrequency, is(loanScheduleInfo.getNumberOfRepayments() * loanScheduleInfo.getRepayEvery())); -// assertThat(loanTermFrequencyType, is(loanScheduleInfo.getRepaymentPeriodFrequencyType())); -// -// // exercise test -// LoanSchedule schedule = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, -// disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); -// -// // verification (post assertions) -// given(schedule).assertHasInstallmentSizeOf(4); -// given(schedule).assertPrincipalDueComponentOf(1, is("50000.00")).assertInterestDueComponentOf(1, is("12000.00")).assertTotalPaymentDueOf(1, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(2, is("50000.00")).assertInterestDueComponentOf(2, is("12000.00")).assertTotalPaymentDueOf(2, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(3, is("50000.00")).assertInterestDueComponentOf(3, is("12000.00")).assertTotalPaymentDueOf(3, is("62000.00")); -// given(schedule).assertPrincipalDueComponentOf(4, is("50000.00")).assertInterestDueComponentOf(4, is("12000.00")).assertTotalPaymentDueOf(4, is("62000.00")); -// } -// -// @Test -// public void givenFlatMethodAndIrregularFirstRepaymentShouldCalculateFlatInterestComponent() { -// -// // setup -// currencyData = new CurrencyData("KSH", "Keyan Shilling", 0, "KSh", "currency.KSH"); -// Integer loanTermFrequency = 9; -// PeriodFrequencyType loanTermFrequencyType = PeriodFrequencyType.MONTHS; -// loanScheduleInfo = LoanProductRelatedDetailTestHelper.createSettingsForIrregularFlatEveryFourMonths(); -// -// LocalDate disbursementDate = new LocalDate(2012, 3, 27); -// LocalDate firstRepaymentDate = new LocalDate(2011, 7, 27); -// LocalDate interestCalculatedFrom = disbursementDate; -// -// // exercise test -// LoanSchedule schedule = flatLoanScheduleGenerator.generate(loanScheduleInfo, loanTermFrequency, loanTermFrequencyType, -// disbursementDate, firstRepaymentDate, interestCalculatedFrom, currencyData); -// -// // verification (post assertions) -// given(schedule).assertHasInstallmentSizeOf(2); -// given(schedule).assertPrincipalDueComponentOf(1, is("7500")).assertInterestDueComponentOf(1, is("1350")).assertTotalPaymentDueOf(1, is("8850")); -// given(schedule).assertPrincipalDueComponentOf(2, is("7500")).assertInterestDueComponentOf(2, is("1350")).assertTotalPaymentDueOf(2, is("8850")); -// } -// -// private LoanScheduleAssertionBuilder given(final LoanSchedule schedule) { -// return new LoanScheduleAssertionBuilder(schedule); -// } -// -// private class LoanScheduleAssertionBuilder { -// -// private final LoanSchedule schedule; -// -// public LoanScheduleAssertionBuilder(final LoanSchedule schedule) { -// this.schedule = schedule; -// } -// -// public LoanScheduleAssertionBuilder assertPrincipalDueComponentOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected principal due component for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getPrincipalDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertInterestDueComponentOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected interest due component for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getInterestDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertTotalPaymentDueOf(final int installmentNumber, Matcher matchesExpectedAmount) { -// final Integer installmentIndex = installmentNumber -1; -// List repaymentInstallments = schedule.getScheduledLoanInstallments(); -// ScheduledLoanInstallment repaymentInstallment = repaymentInstallments.get(installmentIndex); -// -// assertThat("Expected total payment for installment " + installmentNumber + " is wrong: ", -// repaymentInstallment.getTotalInstallmentDue().toString(), matchesExpectedAmount); -// return this; -// } -// -// public LoanScheduleAssertionBuilder assertHasInstallmentSizeOf(int expectedInstallmentSize) { -// assertThat("Expected number of installments is wrong: ", schedule.getScheduledLoanInstallments().size(), is(expectedInstallmentSize)); -// return this; -// } -// } -} \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java index cbe705c2d..1d2266551 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest.java @@ -11,8 +11,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.Money; -import org.mifosplatform.portfolio.loanaccount.domain.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.mockito.runners.MockitoJUnitRunner; @SuppressWarnings("unused") diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java index e29e3618f..f699fa9db 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java @@ -70,7 +70,7 @@ public class LoanProductRelatedDetailTestHelper { MonetaryCurrency currency = new MonetaryCurrencyBuilder().withCode("USD").withDigitsAfterDecimal(2).build(); BigDecimal defaultPrincipal = BigDecimal.valueOf(Double.valueOf("200000")); - + // 2% per month, 24% per year BigDecimal defaultNominalInterestRatePerPeriod = BigDecimal.valueOf(Double.valueOf("2")); PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.MONTHS; @@ -116,15 +116,20 @@ public class LoanProductRelatedDetailTestHelper { defaultNumberOfRepayments, amortizationMethod, inArrearsTolerance); } - private static LoanProductRelatedDetail createLoanProductRelatedDetail(MonetaryCurrency currency, BigDecimal defaultPrincipal, - BigDecimal defaultNominalInterestRatePerPeriod, PeriodFrequencyType interestPeriodFrequencyType, - BigDecimal defaultAnnualNominalInterestRate, InterestMethod interestMethod, - InterestCalculationPeriodMethod interestCalculationPeriodMethod, Integer repayEvery, - PeriodFrequencyType repaymentFrequencyType, Integer defaultNumberOfRepayments, AmortizationMethod amortizationMethod, - BigDecimal inArrearsTolerance) { + private static LoanProductRelatedDetail createLoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecimal defaultPrincipal, + final BigDecimal defaultNominalInterestRatePerPeriod, final PeriodFrequencyType interestPeriodFrequencyType, + final BigDecimal defaultAnnualNominalInterestRate, final InterestMethod interestMethod, + final InterestCalculationPeriodMethod interestCalculationPeriodMethod, final Integer repayEvery, + final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments, final AmortizationMethod amortizationMethod, + final BigDecimal inArrearsTolerance) { + + final Integer graceOnPrincipalPayment = Integer.valueOf(0); + final Integer graceOnInterestPayment = Integer.valueOf(0); + final Integer graceOnInterestCharged = Integer.valueOf(0); return new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, repayEvery, repaymentFrequencyType, - defaultNumberOfRepayments, amortizationMethod, inArrearsTolerance); + defaultNumberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, + inArrearsTolerance); } } \ No newline at end of file diff --git a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanRepaymentScheduleInstallmentBuilder.java b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanRepaymentScheduleInstallmentBuilder.java index 187ba37da..1b4b0b91c 100644 --- a/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanRepaymentScheduleInstallmentBuilder.java +++ b/mifosng-provider/src/test/java/org/mifosplatform/portfolio/loanaccount/LoanRepaymentScheduleInstallmentBuilder.java @@ -8,54 +8,56 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInsta public class LoanRepaymentScheduleInstallmentBuilder { - private Loan loan = null; - private Integer installmentNumber = Integer.valueOf(1); - private LocalDate fromDate = LocalDate.now(); - private LocalDate dueDate = LocalDate.now(); - private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); - private Money principal = new MoneyBuilder().build(); - private Money interest = new MoneyBuilder().build(); - private Money feeCharges = new MoneyBuilder().build(); - private Money penaltyCharges = new MoneyBuilder().build(); - private boolean completed = false; - - public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) { - this.currencyDetail = currencyDetail; - this.principal = new MoneyBuilder().with(currencyDetail).build(); - this.interest = new MoneyBuilder().with(currencyDetail).build(); - } + private final Loan loan = null; + private Integer installmentNumber = Integer.valueOf(1); + private final LocalDate fromDate = LocalDate.now(); + private LocalDate dueDate = LocalDate.now(); + private final LocalDate latestTransactionDate = LocalDate.now(); + private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build(); + private Money principal = new MoneyBuilder().build(); + private Money interest = new MoneyBuilder().build(); + private final Money feeCharges = new MoneyBuilder().build(); + private final Money penaltyCharges = new MoneyBuilder().build(); + private boolean completed = false; - public LoanRepaymentScheduleInstallment build() { - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount()); - if (completed) { - installment.payPrincipalComponent(principal); - installment.payInterestComponent(interest); - } - return installment; - } + public LoanRepaymentScheduleInstallmentBuilder(final MonetaryCurrency currencyDetail) { + this.currencyDetail = currencyDetail; + this.principal = new MoneyBuilder().with(currencyDetail).build(); + this.interest = new MoneyBuilder().with(currencyDetail).build(); + } - public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) { - this.principal = new MoneyBuilder().with(currencyDetail).with(withPrincipal).build(); - return this; - } - - public LoanRepaymentScheduleInstallmentBuilder withInterest(String withInterest) { - this.interest = new MoneyBuilder().with(currencyDetail).with(withInterest).build(); - return this; - } + public LoanRepaymentScheduleInstallment build() { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, + principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount()); + if (completed) { + installment.payPrincipalComponent(latestTransactionDate, principal); + installment.payInterestComponent(latestTransactionDate, interest); + } + return installment; + } - public LoanRepaymentScheduleInstallmentBuilder withDueDate(LocalDate withDueDate) { - this.dueDate = withDueDate; - return this; - } + public LoanRepaymentScheduleInstallmentBuilder withPrincipal(final String withPrincipal) { + this.principal = new MoneyBuilder().with(currencyDetail).with(withPrincipal).build(); + return this; + } - public LoanRepaymentScheduleInstallmentBuilder completed() { - this.completed = true; - return this; - } + public LoanRepaymentScheduleInstallmentBuilder withInterest(final String withInterest) { + this.interest = new MoneyBuilder().with(currencyDetail).with(withInterest).build(); + return this; + } - public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) { - this.installmentNumber = withInstallmentNumber; - return this; - } -} + public LoanRepaymentScheduleInstallmentBuilder withDueDate(final LocalDate withDueDate) { + this.dueDate = withDueDate; + return this; + } + + public LoanRepaymentScheduleInstallmentBuilder completed() { + this.completed = true; + return this; + } + + public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) { + this.installmentNumber = withInstallmentNumber; + return this; + } +} \ No newline at end of file