diff --git a/mifosng-db/patches/patch-0003-drop-create-deposit-account-and-product-tables.sql b/mifosng-db/patches/patch-0003-drop-create-deposit-account-and-product-tables.sql new file mode 100644 index 000000000..38fb05e8c --- /dev/null +++ b/mifosng-db/patches/patch-0003-drop-create-deposit-account-and-product-tables.sql @@ -0,0 +1,74 @@ +DROP TABLE if exists `portfolio_deposit_account`; +DROP TABLE if exists `portfolio_product_deposit`; + +CREATE TABLE `portfolio_product_deposit` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `is_deleted` tinyint(1) NOT NULL DEFAULT '0', + `external_id` varchar(100) DEFAULT NULL, + `description` varchar(500) DEFAULT NULL, + `currency_code` varchar(3) NOT NULL, + `currency_digits` smallint(5) NOT NULL, + `minimum_balance` decimal(19,6) DEFAULT NULL, + `maximum_balance` decimal(19,6) DEFAULT NULL, + `tenure_months` int(11) NOT NULL, + `interest_compounded_every` smallint(5) NOT NULL DEFAULT '1', + `interest_compounded_every_period_enum` smallint(5) NOT NULL DEFAULT '2', + `maturity_default_interest_rate` decimal(19,6) NOT NULL, + `maturity_min_interest_rate` decimal(19,6) NOT NULL, + `maturity_max_interest_rate` decimal(19,6) NOT NULL, + `is_renewal_allowed` tinyint(1) NOT NULL DEFAULT '0', + `is_preclosure_allowed` tinyint(1) NOT NULL DEFAULT '0', + `pre_closure_interest_rate` decimal(19,6) NOT NULL, + `createdby_id` bigint(20) DEFAULT NULL, + `created_date` datetime DEFAULT NULL, + `lastmodified_date` datetime DEFAULT NULL, + `lastmodifiedby_id` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name_deposit_product` (`name`), + UNIQUE KEY `externalid_deposit_product` (`external_id`), + KEY `FKJPW0000000000003` (`createdby_id`), + KEY `FKJPW0000000000004` (`lastmodifiedby_id`), + CONSTRAINT `FKJPX0000000000003` FOREIGN KEY (`createdby_id`) REFERENCES `admin_appuser` (`id`), + CONSTRAINT `FKJPX0000000000004` FOREIGN KEY (`lastmodifiedby_id`) REFERENCES `admin_appuser` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +CREATE TABLE `portfolio_deposit_account` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `is_deleted` tinyint(1) NOT NULL DEFAULT '0', + `external_id` varchar(100) DEFAULT NULL, + `client_id` bigint(20) NOT NULL, + `product_id` bigint(20) NOT NULL, + `currency_code` varchar(3) NOT NULL, + `currency_digits` smallint(5) NOT NULL, + `deposit_amount` decimal(19,6) DEFAULT NULL, + `maturity_nominal_interest_rate` decimal(19,6) NOT NULL, + `tenure_months` int(11) NOT NULL, + `interest_compounded_every` smallint(5) NOT NULL DEFAULT '1', + `interest_compounded_every_period_enum` smallint(5) NOT NULL DEFAULT '2', + `projected_commencement_date` DATE NOT NULL, + `actual_commencement_date` DATE DEFAULT NULL, + `projected_maturity_date` DATE NOT NULL, + `actual_maturity_date` DATE DEFAULT NULL, + `projected_interest_accrued_on_maturity` decimal(19,6) NOT NULL, + `actual_interest_accrued` decimal(19,6) DEFAULT NULL, + `projected_total_maturity_amount` decimal(19,6) NOT NULL, + `actual_total_amount` decimal(19,6) DEFAULT NULL, + `is_renewal_allowed` tinyint(1) NOT NULL DEFAULT '0', + `renewed_account_id` bigint(20) DEFAULT NULL, + `is_preclosure_allowed` tinyint(1) NOT NULL DEFAULT '0', + `pre_closure_interest_rate` decimal(19,6) NOT NULL, + `createdby_id` bigint(20) DEFAULT NULL, + `created_date` datetime DEFAULT NULL, + `lastmodified_date` datetime DEFAULT NULL, + `lastmodifiedby_id` bigint(20) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `deposit_acc_external_id` (`external_id`), + KEY `FKKW0000000000001` (`client_id`), + KEY `FKKW0000000000002` (`product_id`), + KEY `FKKW0000000000003` (`renewed_account_id`), + CONSTRAINT `FKKW0000000000001` FOREIGN KEY (`client_id`) REFERENCES `portfolio_client` (`id`), + CONSTRAINT `FKKW0000000000002` FOREIGN KEY (`product_id`) REFERENCES `portfolio_product_deposit` (`id`), + CONSTRAINT `FKKW0000000000003` FOREIGN KEY (`renewed_account_id`) REFERENCES `portfolio_deposit_account` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositProductsApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositProductsApiResource.java index 749a479ef..62698d5b0 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositProductsApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/DepositProductsApiResource.java @@ -53,7 +53,6 @@ public class DepositProductsApiResource { EntityIdentifier entityIdentifier=this.depositProductWritePlatformService.createDepositProduct(command); return Response.ok().entity(entityIdentifier).build(); - } @PUT @@ -84,9 +83,11 @@ public class DepositProductsApiResource { public String retrieveAllDepositProducts(@Context final UriInfo uriInfo) { Set typicalResponseParameters = new HashSet( - Arrays.asList("id", "name", "description", "createdOn", "lastModifedOn", - "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths","maturityDefaultInterestRate","maturityMinInterestRate", - "maturityMaxInterestRate","renewalAllowed","preClosureAllowed","preClosureInterestRate")); + Arrays.asList("id", "externalId", "name", "description", "createdOn", "lastModifedOn", + "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths", + "maturityDefaultInterestRate","maturityMinInterestRate","maturityMaxInterestRate", + "interestCompoundedEvery", "interestCompoundedEveryPeriodType", + "renewalAllowed","preClosureAllowed","preClosureInterestRate")); Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { @@ -105,9 +106,11 @@ public class DepositProductsApiResource { public String retrieveDepositProductDetails(@PathParam("productId") final Long productId, @Context final UriInfo uriInfo) { Set typicalResponseParameters = new HashSet( - Arrays.asList("id", "name", "description", "createdOn", "lastModifedOn", - "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths","maturityDefaultInterestRate","maturityMinInterestRate", - "maturityMaxInterestRate","renewalAllowed","preClosureAllowed","preClosureInterestRate")); + Arrays.asList("id", "externalId", "name", "description", "createdOn", "lastModifedOn", + "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths", + "maturityDefaultInterestRate","maturityMinInterestRate","maturityMaxInterestRate", + "interestCompoundedEvery", "interestCompoundedEveryPeriodType", + "renewalAllowed","preClosureAllowed","preClosureInterestRate")); Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { @@ -116,9 +119,11 @@ public class DepositProductsApiResource { boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()); boolean template = ApiParameterHelper.template(uriInfo.getQueryParameters()); if (template) { - responseParameters.addAll(Arrays.asList("currencyOptions")); + responseParameters.addAll(Arrays.asList("currencyOptions", "interestCompoundedEveryPeriodTypeOptions")); } + DepositProductData productData = this.depositProductReadPlatformService.retrieveDepositProductData(productId); + return this.apiDataConversionService.convertDepositProductDataToJson(prettyPrint, responseParameters, productData); } @@ -129,9 +134,12 @@ public class DepositProductsApiResource { public String retrieveNewDepositProductDetails(@Context final UriInfo uriInfo) { Set typicalResponseParameters = new HashSet( - Arrays.asList("currencyOptions", "id", "name", "description", "createdOn", "lastModifedOn", - "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths","maturityDefaultInterestRate","maturityMinInterestRate", - "maturityMaxInterestRate","renewalAllowed","preClosureAllowed","preClosureInterestRate")); + Arrays.asList("currencyOptions", "interestCompoundedEveryPeriodTypeOptions", + "id", "externalId", "name", "description", "createdOn", "lastModifedOn", + "currencyCode","digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths", + "maturityDefaultInterestRate","maturityMinInterestRate","maturityMaxInterestRate", + "interestCompoundedEvery", "interestCompoundedEveryPeriodType", + "renewalAllowed","preClosureAllowed","preClosureInterestRate")); Set responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters()); if (responseParameters.isEmpty()) { @@ -143,5 +151,4 @@ public class DepositProductsApiResource { return this.apiDataConversionService.convertDepositProductDataToJson(prettyPrint, responseParameters, depositProduct); } - -} +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositProductCommand.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositProductCommand.java index 54c7829af..e64104a2d 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositProductCommand.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/DepositProductCommand.java @@ -6,6 +6,7 @@ import java.util.Set; public class DepositProductCommand { private final Long id; + private final String externalId; private final String name; private final String description; @@ -19,6 +20,9 @@ public class DepositProductCommand { private final BigDecimal maturityDefaultInterestRate; private final BigDecimal maturityMinInterestRate; private final BigDecimal maturityMaxInterestRate; + private final Integer interestCompoundedEvery; + private final Integer interestCompoundedEveryPeriodType; + private final boolean renewalAllowed; private final boolean preClosureAllowed; private final BigDecimal preClosureInterestRate; @@ -26,13 +30,16 @@ public class DepositProductCommand { private final Set modifiedParameters; public DepositProductCommand(final Set modifiedParameters, - final Long id, final String name, final String description, + final Long id, final String externalId, final String name, final String description, final String currencyCode, final Integer digitsAfterDecimal, final BigDecimal minimumBalance, final BigDecimal maximumBalance, final Integer tenureInMonths, final BigDecimal maturityDefaultInterestRate, final BigDecimal maturityMinInterestRate, final BigDecimal maturityMaxInterestRate, + final Integer interestCompoundedEvery, + final Integer interestCompoundedEveryPeriodType, final boolean renewalAllowed, final boolean preClosureAllowed, final BigDecimal preClosureInterestRate ) { this.id = id; + this.externalId = externalId; this.name = name; this.description = description; @@ -46,6 +53,8 @@ public class DepositProductCommand { this.maturityDefaultInterestRate = maturityDefaultInterestRate; this.maturityMinInterestRate = maturityMinInterestRate; this.maturityMaxInterestRate = maturityMaxInterestRate; + this.interestCompoundedEvery = interestCompoundedEvery; + this.interestCompoundedEveryPeriodType = interestCompoundedEveryPeriodType; this.preClosureInterestRate = preClosureInterestRate; this.renewalAllowed = renewalAllowed; @@ -58,6 +67,10 @@ public class DepositProductCommand { public Long getId() { return id; } + + public String getExternalId() { + return externalId; + } public String getName() { return name; @@ -99,6 +112,14 @@ public class DepositProductCommand { return maturityMaxInterestRate; } + public Integer getInterestCompoundedEvery() { + return interestCompoundedEvery; + } + + public Integer getInterestCompoundedEveryPeriodType() { + return interestCompoundedEveryPeriodType; + } + public boolean isRenewalAllowed() { return renewalAllowed; } @@ -110,6 +131,10 @@ public class DepositProductCommand { public BigDecimal getPreClosureInterestRate() { return preClosureInterestRate; } + + public boolean isExternalIdChanged() { + return this.modifiedParameters.contains("externalId"); + } public boolean isNameChanged() { return this.modifiedParameters.contains("name"); @@ -155,6 +180,14 @@ public class DepositProductCommand { return this.modifiedParameters.contains("maturityMaxInterestRate"); } + public boolean isInterestCompoundedEveryChanged() { + return this.modifiedParameters.contains("interestCompoundedEvery"); + } + + public boolean isInterestCompoundedEveryPeriodTypeChanged() { + return this.modifiedParameters.contains("interestCompoundedEveryPeriodType"); + } + public boolean isRenewalAllowedChanged() { return this.modifiedParameters.contains("renewalAllowed"); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DepositProductData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DepositProductData.java index a72c8ef6d..b4b19a81f 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DepositProductData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/DepositProductData.java @@ -12,6 +12,7 @@ import org.joda.time.DateTime; public class DepositProductData { private final Long id; + private final String externalId; private final String name; private final String description; private final String currencyCode; @@ -22,6 +23,9 @@ public class DepositProductData { private final BigDecimal maturityDefaultInterestRate; private final BigDecimal maturityMinInterestRate; private final BigDecimal maturityMaxInterestRate; + private final Integer interestCompoundedEvery; + private final EnumOptionData interestCompoundedEveryPeriodType; + private final boolean renewalAllowed; private final boolean preClosureAllowed; private final BigDecimal preClosureInterestRate; @@ -30,11 +34,13 @@ public class DepositProductData { private final DateTime lastModifedOn; private final List currencyOptions; + private final List interestCompoundedEveryPeriodTypeOptions; public DepositProductData( final DateTime createdOn, final DateTime lastModifedOn, final Long id, + final String externalId, final String name, final String description, final String currencyCode, @@ -45,6 +51,8 @@ public class DepositProductData { final BigDecimal maturityDefaultInterestRate, final BigDecimal maturityMinInterestRate, final BigDecimal maturityMaxInterestRate, + final Integer interestCompoundedEvery, + final EnumOptionData interestCompoundedEveryPeriodType, final boolean renewalAllowed, final boolean preClosureAllowed, final BigDecimal preClosureInterestRate) { @@ -52,6 +60,7 @@ public class DepositProductData { this.createdOn=createdOn; this.lastModifedOn=lastModifedOn; this.id=id; + this.externalId = externalId; this.name=name; this.description=description; this.currencyCode=currencyCode; @@ -63,17 +72,24 @@ public class DepositProductData { this.maturityDefaultInterestRate = maturityDefaultInterestRate; this.maturityMinInterestRate=maturityMinInterestRate; this.maturityMaxInterestRate=maturityMaxInterestRate; + this.interestCompoundedEvery = interestCompoundedEvery; + this.interestCompoundedEveryPeriodType = interestCompoundedEveryPeriodType; this.renewalAllowed=renewalAllowed; this.preClosureAllowed=preClosureAllowed; this.preClosureInterestRate=preClosureInterestRate; this.currencyOptions = new ArrayList(); + this.interestCompoundedEveryPeriodTypeOptions = new ArrayList(); } - public DepositProductData(final List currencyOptions) { + public DepositProductData( + final List currencyOptions, + final EnumOptionData defaultInterestCompoundedEveryPeriodType, + final List interestCompoundedEveryPeriodTypeOptions) { this.createdOn=null; this.lastModifedOn=null; this.id=null; + this.externalId = null; this.name=null; this.description=null; this.currencyCode=null; @@ -85,17 +101,25 @@ public class DepositProductData { this.maturityDefaultInterestRate = null; this.maturityMinInterestRate=BigDecimal.ZERO; this.maturityMaxInterestRate=BigDecimal.ZERO; + this.interestCompoundedEvery = Integer.valueOf(1); + this.interestCompoundedEveryPeriodType = defaultInterestCompoundedEveryPeriodType; + this.renewalAllowed=true; this.preClosureAllowed=true; this.preClosureInterestRate=BigDecimal.ZERO; this.currencyOptions = currencyOptions; + this.interestCompoundedEveryPeriodTypeOptions = interestCompoundedEveryPeriodTypeOptions; } - public DepositProductData(final DepositProductData product, final List currencyOptions) { + public DepositProductData( + final DepositProductData product, + final List currencyOptions, + final List interestCompoundedEveryPeriodTypeOptions) { this.createdOn=product.getCreatedOn(); this.lastModifedOn=product.getLastModifedOn(); this.id=product.getId(); + this.externalId = product.getExternalId(); this.name=product.getName(); this.description=product.getDescription(); this.currencyCode=product.getCurrencyCode(); @@ -107,16 +131,24 @@ public class DepositProductData { this.maturityDefaultInterestRate = product.getMaturityDefaultInterestRate(); this.maturityMinInterestRate=product.getMaturityMinInterestRate(); this.maturityMaxInterestRate=product.getMaturityMaxInterestRate(); + this.interestCompoundedEvery = product.getInterestCompoundedEvery(); + this.interestCompoundedEveryPeriodType = product.getInterestCompoundedEveryPeriodType(); + this.renewalAllowed=product.isRenewalAllowed(); this.preClosureAllowed=product.isPreClosureAllowed(); this.preClosureInterestRate=product.getPreClosureInterestRate(); this.currencyOptions = currencyOptions; + this.interestCompoundedEveryPeriodTypeOptions = interestCompoundedEveryPeriodTypeOptions; } public Long getId() { return id; } + + public String getExternalId() { + return externalId; + } public String getName() { return name; @@ -157,6 +189,14 @@ public class DepositProductData { public BigDecimal getMaturityMaxInterestRate() { return maturityMaxInterestRate; } + + public Integer getInterestCompoundedEvery() { + return interestCompoundedEvery; + } + + public EnumOptionData getInterestCompoundedEveryPeriodType() { + return interestCompoundedEveryPeriodType; + } public boolean isRenewalAllowed() { return renewalAllowed; @@ -181,4 +221,8 @@ public class DepositProductData { public List getCurrencyOptions() { return currencyOptions; } + + public List getInterestCompoundedEveryPeriodTypeOptions() { + return interestCompoundedEveryPeriodTypeOptions; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java index 539bbbc61..3dbecbc54 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/infrastructure/ApiDataConversionServiceImpl.java @@ -1572,12 +1572,16 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { Map requestMap = gsonConverter.fromJson(json, typeOfMap); Set supportedParams = new HashSet( - Arrays.asList("name","description","currencyCode", "digitsAfterDecimal","locale","minimumBalance","maximumBalance","tenureInMonths", - "maturityDefaultInterestRate","maturityMinInterestRate","maturityMaxInterestRate","renewalAllowed","preClosureAllowed","preClosureInterestRate") + Arrays.asList("locale", "name", "externalId", "description","currencyCode", "digitsAfterDecimal","minimumBalance","maximumBalance","tenureInMonths", + "maturityDefaultInterestRate","maturityMinInterestRate","maturityMaxInterestRate", + "interestCompoundedEvery", "interestCompoundedEveryPeriodType", + "renewalAllowed","preClosureAllowed","preClosureInterestRate") ); checkForUnsupportedParameters(requestMap, supportedParams); Set modifiedParameters = new HashSet(); String name = extractStringParameter("name", requestMap,modifiedParameters); + String externalId = extractStringParameter("externalId", requestMap,modifiedParameters); + String description = extractStringParameter("description", requestMap,modifiedParameters); String currencyCode=extractStringParameter("currencyCode", requestMap,modifiedParameters); Integer digitsAfterDecimalValue = extractIntegerParameter("digitsAfterDecimal", requestMap, modifiedParameters); @@ -1588,12 +1592,19 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { BigDecimal maturityDefaultInterestRate = extractBigDecimalParameter("maturityDefaultInterestRate", requestMap, modifiedParameters); BigDecimal maturityMinInterestRate = extractBigDecimalParameter("maturityMinInterestRate", requestMap, modifiedParameters); BigDecimal maturityMaxInterestRate = extractBigDecimalParameter("maturityMaxInterestRate", requestMap, modifiedParameters); + + Integer interestCompoundedEvery = extractIntegerParameter("interestCompoundedEvery", requestMap, modifiedParameters); + Integer interestCompoundedEveryPeriodType = extractIntegerParameter("interestCompoundedEveryPeriodType", requestMap, modifiedParameters); + boolean canRenew = extractBooleanParameter("renewalAllowed", requestMap, modifiedParameters); boolean canPreClose = extractBooleanParameter("preClosureAllowed", requestMap, modifiedParameters); BigDecimal preClosureInterestRate = extractBigDecimalParameter("preClosureInterestRate", requestMap, modifiedParameters); - return new DepositProductCommand(modifiedParameters, resourceIdentifier, name, description, currencyCode, digitsAfterDecimalValue, minimumBalance,maximumBalance, - tenureMonths, maturityDefaultInterestRate, maturityMinInterestRate, maturityMaxInterestRate, canRenew, canPreClose, preClosureInterestRate); + return new DepositProductCommand(modifiedParameters, resourceIdentifier, externalId, + name, description, currencyCode, digitsAfterDecimalValue, minimumBalance,maximumBalance, + tenureMonths, maturityDefaultInterestRate, maturityMinInterestRate, maturityMaxInterestRate, + interestCompoundedEvery, interestCompoundedEveryPeriodType, + canRenew, canPreClose, preClosureInterestRate); } @Override @@ -1601,9 +1612,11 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService { Set responseParameters, DepositProductData... products) { Set supportedParameters = new HashSet( - Arrays.asList("id", "name", "description","createdOn", "lastModifedOn","currencyCode","digitsAfterDecimal", - "currencyOptions", "minimumBalance","maximumBalance","tenureInMonths","maturityDefaultInterestRate","maturityMinInterestRate", - "maturityMaxInterestRate","renewalAllowed","preClosureAllowed","preClosureInterestRate")); + Arrays.asList("currencyOptions", "interestCompoundedEveryPeriodTypeOptions", + "id", "externalId", "name", "description","createdOn", "lastModifedOn","currencyCode","digitsAfterDecimal", + "minimumBalance","maximumBalance","tenureInMonths","maturityDefaultInterestRate","maturityMinInterestRate", + "maturityMaxInterestRate", "interestCompoundedEvery", "interestCompoundedEveryPeriodType", + "renewalAllowed","preClosureAllowed","preClosureInterestRate")); final Set parameterNamesToSkip = new HashSet(); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/DepositProduct.java b/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/DepositProduct.java index 80a986a2d..6cbd84422 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/DepositProduct.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/DepositProduct.java @@ -6,21 +6,28 @@ import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Table; +import javax.persistence.UniqueConstraint; import org.apache.commons.lang.StringUtils; import org.mifosng.platform.api.commands.DepositProductCommand; import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.exceptions.ValueOutsideRangeException; import org.mifosng.platform.infrastructure.AbstractAuditableCustom; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.user.domain.AppUser; @Entity -@Table(name = "portfolio_product_deposit") +@Table(name = "portfolio_product_deposit", uniqueConstraints={ + @UniqueConstraint(columnNames = {"name"}, name="name_deposit_product"), + @UniqueConstraint(columnNames = {"external_id"}, name="externalid_deposit_product") +}) public class DepositProduct extends AbstractAuditableCustom { - @SuppressWarnings("unused") @Column(name = "name", nullable = false) private String name; + + @Column(name = "external_id", length=100) + private String externalId; @SuppressWarnings("unused") @Column(name = "description") @@ -39,8 +46,12 @@ public class DepositProduct extends AbstractAuditableCustom { private BigDecimal maximumBalance; @SuppressWarnings("unused") - @Column(name = "tenure_months", nullable=false) - private Integer tenureInMonths; + @Column(name = "interest_compounded_every", nullable=false) + private Integer interestCompoundedEvery; + + @SuppressWarnings("unused") + @Column(name = "interest_compounded_every_period_enum", nullable=false) + private PeriodFrequencyType interestCompoundedEveryPeriodType; @SuppressWarnings("unused") @Column(name = "maturity_default_interest_rate", scale = 6, precision = 19, nullable = false) @@ -52,10 +63,14 @@ public class DepositProduct extends AbstractAuditableCustom { @Column(name = "maturity_max_interest_rate", scale = 6, precision = 19, nullable = false) private BigDecimal maturityMaxInterestRate; - @Column(name = "can_renew", nullable=false) + @SuppressWarnings("unused") + @Column(name = "tenure_months", nullable=false) + private Integer tenureInMonths; + + @Column(name = "is_renewal_allowed", nullable=false) private boolean renewalAllowed = false; - @Column(name = "can_pre_close", nullable=false) + @Column(name = "is_preclosure_allowed", nullable=false) private boolean preClosureAllowed = false; @SuppressWarnings("unused") @@ -66,19 +81,29 @@ public class DepositProduct extends AbstractAuditableCustom { // } - public DepositProduct(final String name, final String description, + public DepositProduct(final String name, final String externalId, + final String description, final MonetaryCurrency currency, final BigDecimal minimumBalance, final BigDecimal maximumBalance, final Integer tenureMonths, final BigDecimal maturityDefaultInterestRate, final BigDecimal maturityMinInterestRate, - BigDecimal maturityMaxInterestRate, boolean canRenew, - boolean canPreClose, BigDecimal preClosureInterestRate) { + final BigDecimal maturityMaxInterestRate, + final Integer interestCompoundedEvery, + final PeriodFrequencyType interestCompoundedEveryPeriodType, + final boolean canRenew, + final boolean canPreClose, + final BigDecimal preClosureInterestRate) { this.name = name.trim(); if (StringUtils.isNotBlank(description)) { this.description = description.trim(); } else { this.description = null; } + if (StringUtils.isNotBlank(externalId)) { + this.externalId = externalId.trim(); + } else { + this.externalId = null; + } this.currency = currency; this.minimumBalance = minimumBalance; @@ -87,6 +112,8 @@ public class DepositProduct extends AbstractAuditableCustom { this.maturityDefaultInterestRate = maturityDefaultInterestRate; this.maturityMinInterestRate = maturityMinInterestRate; this.maturityMaxInterestRate = maturityMaxInterestRate; + this.interestCompoundedEvery = interestCompoundedEvery; + this.interestCompoundedEveryPeriodType = interestCompoundedEveryPeriodType; this.renewalAllowed = canRenew; this.preClosureAllowed = canPreClose; this.preClosureInterestRate = preClosureInterestRate; @@ -108,11 +135,22 @@ public class DepositProduct extends AbstractAuditableCustom { return deleted; } + /** + * Delete is a soft delete. Updates flag on product so it wont appear in query/report results. + * + * Any fields with unique constraints and prepended with id of record. + */ public void delete() { this.deleted = true; + this.name = this.getId() + "_DELETED_" + this.name; + this.externalId = this.getId() + "_DELETED_" + this.externalId; } - public void update(final DepositProductCommand command){ + public void update(final DepositProductCommand command, final PeriodFrequencyType interestCompoundingFrequency){ + + if (command.isExternalIdChanged()) { + this.externalId = command.getExternalId(); + } if (command.isNameChanged()) { this.name = command.getName(); @@ -160,6 +198,14 @@ public class DepositProduct extends AbstractAuditableCustom { this.maturityMinInterestRate=command.getMaturityMinInterestRate(); } + if (command.isInterestCompoundedEveryChanged()) { + this.interestCompoundedEvery = command.getInterestCompoundedEvery(); + } + + if (command.isInterestCompoundedEveryPeriodTypeChanged()) { + this.interestCompoundedEveryPeriodType = interestCompoundingFrequency; + } + if (command.isRenewalAllowedChanged()) { this.renewalAllowed=command.isRenewalAllowed(); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/SavingsDepositEnumerations.java b/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/SavingsDepositEnumerations.java new file mode 100644 index 000000000..de4c67ca7 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosng/platform/deposit/domain/SavingsDepositEnumerations.java @@ -0,0 +1,31 @@ +package org.mifosng.platform.deposit.domain; + +import org.mifosng.platform.api.data.EnumOptionData; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; + +public class SavingsDepositEnumerations { + + public static EnumOptionData interestCompoundingPeriodType(final int id) { + return interestCompoundingPeriodType(PeriodFrequencyType.fromInt(id)); + } + + public static EnumOptionData interestCompoundingPeriodType(final PeriodFrequencyType type) { + final String codePrefix = "deposit.interest.compounding.period."; + EnumOptionData optionData = null; + switch (type) { + case DAYS: + optionData = new EnumOptionData(PeriodFrequencyType.DAYS.getValue().longValue(), codePrefix + PeriodFrequencyType.DAYS.getCode(), "Days"); + break; + case WEEKS: + optionData = new EnumOptionData(PeriodFrequencyType.WEEKS.getValue().longValue(), codePrefix + PeriodFrequencyType.WEEKS.getCode(), "Weeks"); + break; + case MONTHS: + optionData = new EnumOptionData(PeriodFrequencyType.MONTHS.getValue().longValue(), codePrefix + PeriodFrequencyType.MONTHS.getCode(), "Months"); + break; + default: + optionData = new EnumOptionData(PeriodFrequencyType.INVALID.getValue().longValue(), PeriodFrequencyType.INVALID.getCode(), "Invalid"); + break; + } + return optionData; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductCommandValidator.java b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductCommandValidator.java index a7228f76e..707a257c6 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductCommandValidator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductCommandValidator.java @@ -21,6 +21,8 @@ public class DepositProductCommandValidator { List dataValidationErrors = new ArrayList(); DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("deposit.product"); + + baseDataValidator.reset().parameter("externalId").value(command.getExternalId()).ignoreIfNull().notExceedingLengthOf(100); baseDataValidator.reset().parameter("name").value(command.getName()).notBlank(); baseDataValidator.reset().parameter("description").value(command.getDescription()).notExceedingLengthOf(500); baseDataValidator.reset().parameter("currencyCode").value(command.getCurrencyCode()).notBlank(); @@ -37,6 +39,9 @@ public class DepositProductCommandValidator { baseDataValidator.reset().parameter("maturityDefaultInterestRate").comapareMinAndMaxOfTwoBigDecmimalNos(command.getMaturityDefaultInterestRate(), command.getMaturityMaxInterestRate()); baseDataValidator.reset().parameter("maturityMinInterestRate").comapareMinAndMaxOfTwoBigDecmimalNos(command.getMaturityMinInterestRate(), command.getMaturityDefaultInterestRate()); + baseDataValidator.reset().parameter("interestCompoundedEvery").value(command.getInterestCompoundedEvery()).notNull().zeroOrPositiveAmount(); + baseDataValidator.reset().parameter("interestCompoundedEveryPeriodType").value(command.getInterestCompoundedEveryPeriodType()).notNull().inMinMaxRange(1, 3); + baseDataValidator.reset().parameter("renewalAllowed").trueOrFalseRequired(command.isRenewalAllowedChanged()); baseDataValidator.reset().parameter("preClosureAllowed").trueOrFalseRequired(command.isPreClosureAllowedChanged()); @@ -54,6 +59,7 @@ public class DepositProductCommandValidator { DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("deposit.product"); baseDataValidator.reset().parameter("id").value(command.getId()).notNull(); + baseDataValidator.reset().parameter("externalId").value(command.getExternalId()).ignoreIfNull().notExceedingLengthOf(100); baseDataValidator.reset().parameter("name").value(command.getName()).ignoreIfNull().notBlank(); baseDataValidator.reset().parameter("description").value(command.getDescription()).ignoreIfNull().notExceedingLengthOf(500); @@ -71,6 +77,9 @@ public class DepositProductCommandValidator { baseDataValidator.reset().comapareMinAndMaxOfTwoBigDecmimalNos(command.getMaturityDefaultInterestRate(), command.getMaturityMaxInterestRate()); baseDataValidator.reset().comapareMinAndMaxOfTwoBigDecmimalNos(command.getMaturityMinInterestRate(), command.getMaturityDefaultInterestRate()); + baseDataValidator.reset().parameter("interestCompoundedEvery").value(command.getInterestCompoundedEvery()).ignoreIfNull().zeroOrPositiveAmount(); + baseDataValidator.reset().parameter("interestCompoundedEveryPeriodType").value(command.getInterestCompoundedEveryPeriodType()).ignoreIfNull().inMinMaxRange(1, 3); + baseDataValidator.reset().parameter("preClosureInterestRate").value(command.getPreClosureInterestRate()).ignoreIfNull().zeroOrPositiveAmount(); baseDataValidator.reset().comapareMinAndMaxOfTwoBigDecmimalNos(command.getPreClosureInterestRate(), command.getMaturityMinInterestRate()); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductReadPlatformServiceImpl.java index 6168ce6e4..e16ec5c30 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductReadPlatformServiceImpl.java @@ -3,6 +3,7 @@ package org.mifosng.platform.depositproduct.service; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -10,9 +11,12 @@ import org.joda.time.DateTime; import org.mifosng.platform.api.data.CurrencyData; import org.mifosng.platform.api.data.DepositProductData; import org.mifosng.platform.api.data.DepositProductLookup; +import org.mifosng.platform.api.data.EnumOptionData; import org.mifosng.platform.currency.service.CurrencyReadPlatformService; +import org.mifosng.platform.deposit.domain.SavingsDepositEnumerations; import org.mifosng.platform.infrastructure.JdbcSupport; import org.mifosng.platform.infrastructure.TenantAwareRoutingDataSource; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.security.PlatformSecurityContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; @@ -58,22 +62,30 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductData productData = this.jdbcTemplate.queryForObject(sql, depositProductMapper, new Object[]{productId}); List currencyOptions = currencyReadPlatformService.retrieveAllowedCurrencies(); - return new DepositProductData(productData, currencyOptions); + EnumOptionData monthly = SavingsDepositEnumerations.interestCompoundingPeriodType(PeriodFrequencyType.MONTHS); + List interestCompoundedEveryPeriodTypeOptions = Arrays.asList(monthly); + + return new DepositProductData(productData, currencyOptions, interestCompoundedEveryPeriodTypeOptions); } @Override public DepositProductData retrieveNewDepositProductDetails() { List currencyOptions = currencyReadPlatformService.retrieveAllowedCurrencies(); - return new DepositProductData(currencyOptions); + EnumOptionData monthly = SavingsDepositEnumerations.interestCompoundingPeriodType(PeriodFrequencyType.MONTHS); + List interestCompoundedEveryPeriodTypeOptions = Arrays.asList(monthly); + + return new DepositProductData(currencyOptions, monthly, interestCompoundedEveryPeriodTypeOptions); } private static final class DepositProductMapper implements RowMapper{ public String depositProductSchema(){ - return "dp.id as id,dp.name as name, dp.description as description,dp.currency_code as currencyCode, dp.currency_digits as currencyDigits,dp.minimum_balance as minimumBalance,dp.maximum_balance as maximumBalance," + - "dp.created_date as createdon, dp.lastmodified_date as modifiedon,dp.tenure_months as tenureMonths, dp.maturity_default_interest_rate as maturityDefaultInterestRate, " + - "dp.maturity_min_interest_rate as maturityMinInterestRate, dp.maturity_max_interest_rate as maturityMaxInterestRate, dp.can_renew as canRenew, dp.can_pre_close as canPreClose, dp.pre_closure_interest_rate as preClosureInterestRate, " + + return " dp.id as id, dp.external_id as exernalId, dp.name as name, dp.description as description,dp.currency_code as currencyCode, dp.currency_digits as currencyDigits,dp.minimum_balance as minimumBalance,dp.maximum_balance as maximumBalance," + + "dp.created_date as createdon, dp.lastmodified_date as modifiedon,dp.tenure_months as tenureMonths, " + + "dp.interest_compounded_every as interestCompoundedEvery, dp.interest_compounded_every_period_enum as interestCompoundedEveryPeriodType, " + + "dp.maturity_default_interest_rate as maturityDefaultInterestRate, " + + "dp.maturity_min_interest_rate as maturityMinInterestRate, dp.maturity_max_interest_rate as maturityMaxInterestRate, dp.is_renewal_allowed as canRenew, dp.is_preclosure_allowed as canPreClose, dp.pre_closure_interest_rate as preClosureInterestRate, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol " + "from portfolio_product_deposit dp join ref_currency curr on curr.code = dp.currency_code "; } @@ -83,6 +95,7 @@ public class DepositProductReadPlatformServiceImpl implements throws SQLException { Long id = rs.getLong("id"); + String exernalId = rs.getString("exernalId"); String name = rs.getString("name"); String description = rs.getString("description"); @@ -96,6 +109,10 @@ public class DepositProductReadPlatformServiceImpl implements BigDecimal maximumBalance=rs.getBigDecimal("maximumBalance"); Integer tenureMonths = JdbcSupport.getInteger(rs, "tenureMonths"); + Integer interestCompoundedEvery = JdbcSupport.getInteger(rs, "interestCompoundedEvery"); + Integer interestCompoundedEveryPeriodTypeValue = JdbcSupport.getInteger(rs, "interestCompoundedEveryPeriodType"); + EnumOptionData interestCompoundedEveryPeriodType = SavingsDepositEnumerations.interestCompoundingPeriodType(interestCompoundedEveryPeriodTypeValue); + BigDecimal maturityDefaultInterestRate=rs.getBigDecimal("maturityDefaultInterestRate"); BigDecimal maturityMinInterestRate= rs.getBigDecimal("maturityMinInterestRate"); BigDecimal maturityMaxInterestRate = rs.getBigDecimal("maturityMaxInterestRate"); @@ -105,8 +122,10 @@ public class DepositProductReadPlatformServiceImpl implements BigDecimal preClosureInterestRate=rs.getBigDecimal("preClosureInterestRate"); - return new DepositProductData(createdOn, lastModifedOn, id, name, description, currencyCode, currencyDigits, minimumBalance, maximumBalance, - tenureMonths, maturityDefaultInterestRate,maturityMinInterestRate,maturityMaxInterestRate,canRenew,canPreClose,preClosureInterestRate); + return new DepositProductData(createdOn, lastModifedOn, id, exernalId, name, description, currencyCode, currencyDigits, minimumBalance, maximumBalance, + tenureMonths, maturityDefaultInterestRate,maturityMinInterestRate,maturityMaxInterestRate, + interestCompoundedEvery, interestCompoundedEveryPeriodType, + canRenew,canPreClose,preClosureInterestRate); } } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductWritePlatformServiceJpaRepositoryImpl.java index 808766d27..bd4d28172 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/depositproduct/service/DepositProductWritePlatformServiceJpaRepositoryImpl.java @@ -6,65 +6,103 @@ import org.mifosng.platform.currency.domain.MonetaryCurrency; import org.mifosng.platform.deposit.domain.DepositProduct; import org.mifosng.platform.deposit.domain.DepositProductRepository; import org.mifosng.platform.exceptions.DepositProductNotFoundException; +import org.mifosng.platform.exceptions.PlatformDataIntegrityException; +import org.mifosng.platform.loan.domain.PeriodFrequencyType; import org.mifosng.platform.security.PlatformSecurityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -public class DepositProductWritePlatformServiceJpaRepositoryImpl implements - DepositProductWritePlatformService { +public class DepositProductWritePlatformServiceJpaRepositoryImpl implements DepositProductWritePlatformService { + + private final static Logger logger = LoggerFactory.getLogger(DepositProductWritePlatformServiceJpaRepositoryImpl.class); private final PlatformSecurityContext context; private final DepositProductRepository depositProductRepository; - @Autowired public DepositProductWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final DepositProductRepository depositProductRepository){ this.context=context; this.depositProductRepository=depositProductRepository; } - - @Transactional - @Override - public EntityIdentifier createDepositProduct(DepositProductCommand command) { + + /* + * Guaranteed to throw an exception no matter what the data integrity issue is. + */ + private void handleDataIntegrityIssues(final DepositProductCommand command, final DataIntegrityViolationException dve) { - this.context.authenticatedUser(); - DepositProductCommandValidator validator=new DepositProductCommandValidator(command); - validator.validateForCreate(); - - MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); - DepositProduct product = new DepositProduct(command.getName(),command.getDescription(),currency,command.getMinimumBalance(),command.getMaximumBalance(), - command.getTenureInMonths(),command.getMaturityDefaultInterestRate(),command.getMaturityMinInterestRate(),command.getMaturityMaxInterestRate(), - command.isRenewalAllowed(), command.isPreClosureAllowed(), - command.getPreClosureInterestRate()); - this.depositProductRepository.save(product); - - return new EntityIdentifier(product.getId()); - - } - - @Transactional - @Override - public EntityIdentifier updateDepositProduct(DepositProductCommand command) { - - this.context.authenticatedUser(); - DepositProductCommandValidator validator=new DepositProductCommandValidator(command); - validator.validateForUpdate(); - - DepositProduct product=this.depositProductRepository.findOne(command.getId()); - if(product==null){ - throw new DepositProductNotFoundException(command.getId()); + Throwable realCause = dve.getMostSpecificCause(); + if (realCause.getMessage().contains("name_deposit_product")) { + throw new PlatformDataIntegrityException("error.msg.desposit.product.duplicate.name", "Deposit product with name: " + command.getName() + " already exists", "name", command.getName()); + } + if (realCause.getMessage().contains("externalid_deposit_product")) { + throw new PlatformDataIntegrityException("error.msg.desposit.product.duplicate.externalId", "Deposit product with externalId " + command.getExternalId() + " already exists", "externalId", command.getExternalId()); } - product.update(command); - this.depositProductRepository.save(product); - return new EntityIdentifier(Long.valueOf(product.getId())); + logger.error(dve.getMessage(), dve); + throw new PlatformDataIntegrityException("error.msg.deposit.product.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); } @Transactional @Override - public EntityIdentifier deleteDepositProduct(Long productId) { + public EntityIdentifier createDepositProduct(final DepositProductCommand command) { + + try { + this.context.authenticatedUser(); + DepositProductCommandValidator validator=new DepositProductCommandValidator(command); + validator.validateForCreate(); + + PeriodFrequencyType interestCompoundingPeriodType = PeriodFrequencyType.fromInt(command.getInterestCompoundedEveryPeriodType()); + MonetaryCurrency currency = new MonetaryCurrency(command.getCurrencyCode(), command.getDigitsAfterDecimal()); + DepositProduct product = new DepositProduct(command.getName(), command.getExternalId(), command.getDescription(),currency,command.getMinimumBalance(),command.getMaximumBalance(), + command.getTenureInMonths(),command.getMaturityDefaultInterestRate(),command.getMaturityMinInterestRate(),command.getMaturityMaxInterestRate(), + command.getInterestCompoundedEvery(), interestCompoundingPeriodType, + command.isRenewalAllowed(), command.isPreClosureAllowed(), + command.getPreClosureInterestRate()); + this.depositProductRepository.save(product); + + return new EntityIdentifier(product.getId()); + } catch (DataIntegrityViolationException dve) { + handleDataIntegrityIssues(command, dve); + return new EntityIdentifier(Long.valueOf(-1)); + } + } + + @Transactional + @Override + public EntityIdentifier updateDepositProduct(final DepositProductCommand command) { + + try { + this.context.authenticatedUser(); + DepositProductCommandValidator validator=new DepositProductCommandValidator(command); + validator.validateForUpdate(); + + DepositProduct product=this.depositProductRepository.findOne(command.getId()); + if(product==null){ + throw new DepositProductNotFoundException(command.getId()); + } + + PeriodFrequencyType interestCompoundingFrequency = null; + if (command.isInterestCompoundedEveryPeriodTypeChanged()) { + interestCompoundingFrequency = PeriodFrequencyType.fromInt(command.getInterestCompoundedEveryPeriodType()); + } + + product.update(command, interestCompoundingFrequency); + this.depositProductRepository.save(product); + return new EntityIdentifier(Long.valueOf(product.getId())); + } catch (DataIntegrityViolationException dve) { + handleDataIntegrityIssues(command, dve); + return new EntityIdentifier(Long.valueOf(-1)); + } + } + + @Transactional + @Override + public EntityIdentifier deleteDepositProduct(final Long productId) { this.context.authenticatedUser(); DepositProduct product=this.depositProductRepository.findOne(productId);