Merge pull request #43 from myrle-krantz/develop

Charges necessary for the functioning of loans are now readonly.
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java
new file mode 100644
index 0000000..e374850
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.client;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ChargeDefinitionIsReadOnly extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index 1b2f61e..b80fa76 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -208,6 +208,7 @@
           produces = MediaType.ALL_VALUE,
           consumes = MediaType.APPLICATION_JSON_VALUE
   )
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ChargeDefinitionIsReadOnly.class)
   void changeChargeDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
           @PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier,
@@ -219,6 +220,7 @@
           produces = MediaType.ALL_VALUE,
           consumes = MediaType.APPLICATION_JSON_VALUE
   )
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = ChargeDefinitionIsReadOnly.class)
   void deleteChargeDefinition(
           @PathVariable("productidentifier") final String productIdentifier,
           @PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier);
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
index 8cd4fc0..2268871 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
@@ -79,6 +79,8 @@
   @ValidPaymentCycleUnit
   private ChronoUnit forCycleSizeUnit;
 
+  private boolean readOnly;
+
   public ChargeDefinition() {
   }
 
@@ -179,45 +181,55 @@
     this.forCycleSizeUnit = forCycleSizeUnit;
   }
 
+  public boolean isReadOnly() {
+    return readOnly;
+  }
+
+  public void setReadOnly(boolean readOnly) {
+    this.readOnly = readOnly;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     ChargeDefinition that = (ChargeDefinition) o;
-    return Objects.equals(identifier, that.identifier) &&
-            Objects.equals(name, that.name) &&
-            Objects.equals(description, that.description) &&
-            Objects.equals(accrueAction, that.accrueAction) &&
-            Objects.equals(chargeAction, that.chargeAction) &&
-            Objects.equals(amount, that.amount) &&
-            chargeMethod == that.chargeMethod &&
-            Objects.equals(proportionalTo, that.proportionalTo) &&
-            Objects.equals(fromAccountDesignator, that.fromAccountDesignator) &&
-            Objects.equals(accrualAccountDesignator, that.accrualAccountDesignator) &&
-            Objects.equals(toAccountDesignator, that.toAccountDesignator) &&
-            forCycleSizeUnit == that.forCycleSizeUnit;
+    return readOnly == that.readOnly &&
+        Objects.equals(identifier, that.identifier) &&
+        Objects.equals(name, that.name) &&
+        Objects.equals(description, that.description) &&
+        Objects.equals(accrueAction, that.accrueAction) &&
+        Objects.equals(chargeAction, that.chargeAction) &&
+        Objects.equals(amount, that.amount) &&
+        chargeMethod == that.chargeMethod &&
+        Objects.equals(proportionalTo, that.proportionalTo) &&
+        Objects.equals(fromAccountDesignator, that.fromAccountDesignator) &&
+        Objects.equals(accrualAccountDesignator, that.accrualAccountDesignator) &&
+        Objects.equals(toAccountDesignator, that.toAccountDesignator) &&
+        forCycleSizeUnit == that.forCycleSizeUnit;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, proportionalTo, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit);
+    return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, proportionalTo, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit, readOnly);
   }
 
   @Override
   public String toString() {
     return "ChargeDefinition{" +
-            "identifier='" + identifier + '\'' +
-            ", name='" + name + '\'' +
-            ", description='" + description + '\'' +
-            ", accrueAction='" + accrueAction + '\'' +
-            ", chargeAction='" + chargeAction + '\'' +
-            ", amount=" + amount +
-            ", chargeMethod=" + chargeMethod +
-            ", proportionalTo='" + proportionalTo + '\'' +
-            ", fromAccountDesignator='" + fromAccountDesignator + '\'' +
-            ", accrualAccountDesignator='" + accrualAccountDesignator + '\'' +
-            ", toAccountDesignator='" + toAccountDesignator + '\'' +
-            ", forCycleSizeUnit=" + forCycleSizeUnit +
-            '}';
+        "identifier='" + identifier + '\'' +
+        ", name='" + name + '\'' +
+        ", description='" + description + '\'' +
+        ", accrueAction='" + accrueAction + '\'' +
+        ", chargeAction='" + chargeAction + '\'' +
+        ", amount=" + amount +
+        ", chargeMethod=" + chargeMethod +
+        ", proportionalTo='" + proportionalTo + '\'' +
+        ", fromAccountDesignator='" + fromAccountDesignator + '\'' +
+        ", accrualAccountDesignator='" + accrualAccountDesignator + '\'' +
+        ", toAccountDesignator='" + toAccountDesignator + '\'' +
+        ", forCycleSizeUnit=" + forCycleSizeUnit +
+        ", readOnly=" + readOnly +
+        '}';
   }
 }
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
index 59f3c27..f65e8a6 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -16,8 +16,11 @@
 package io.mifos.portfolio;
 
 import io.mifos.core.api.util.NotFoundException;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
 import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.client.ChargeDefinitionIsReadOnly;
 import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
 import io.mifos.portfolio.api.v1.domain.Product;
 import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
@@ -27,6 +30,7 @@
 
 import java.math.BigDecimal;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -40,34 +44,58 @@
     final Product product = createProduct();
 
     final List<ChargeDefinition> charges = portfolioManager.getAllChargeDefinitionsForProduct(product.getIdentifier());
-    final Set<String> chargeDefinitionIdentifiers = charges.stream().map(ChargeDefinition::getIdentifier).collect(Collectors.toSet());
-    final Set<String> expectedChargeDefinitionIdentifiers = Stream.of(
+    final Map<Boolean, List<ChargeDefinition>> chargeIdentifersPartitionedByReadOnly
+        = charges.stream().collect(Collectors.partitioningBy(ChargeDefinition::isReadOnly, Collectors.toList()));
+    final Set<String> readOnlyChargeDefinitionIdentifiers
+        = chargeIdentifersPartitionedByReadOnly.get(true).stream()
+        .map(ChargeDefinition::getIdentifier)
+        .collect(Collectors.toSet());
+    final Set<String> changeableChargeDefinitionIdentifiers
+        = chargeIdentifersPartitionedByReadOnly.get(false).stream()
+        .map(ChargeDefinition::getIdentifier)
+        .collect(Collectors.toSet());
+
+    final Set<String> expectedReadOnlyChargeDefinitionIdentifiers = Stream.of(
         ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
-        ChargeIdentifiers.DISBURSEMENT_FEE_ID,
-        ChargeIdentifiers.INTEREST_ID,
-        ChargeIdentifiers.LATE_FEE_ID,
         ChargeIdentifiers.LOAN_FUNDS_ALLOCATION_ID,
-        ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID,
-        ChargeIdentifiers.PROCESSING_FEE_ID,
         ChargeIdentifiers.RETURN_DISBURSEMENT_ID,
         ChargeIdentifiers.DISBURSE_PAYMENT_ID,
         ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID,
         ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID,
-        ChargeIdentifiers.REPAYMENT_ID
-        )
+        ChargeIdentifiers.REPAYMENT_ID)
         .collect(Collectors.toSet());
-    Assert.assertEquals(expectedChargeDefinitionIdentifiers, chargeDefinitionIdentifiers);
+    final Set<String> expectedChangeableChargeDefinitionIdentifiers = Stream.of(
+        ChargeIdentifiers.DISBURSEMENT_FEE_ID,
+        ChargeIdentifiers.INTEREST_ID,
+        ChargeIdentifiers.LATE_FEE_ID,
+        ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID,
+        ChargeIdentifiers.PROCESSING_FEE_ID)
+        .collect(Collectors.toSet());
+
+    Assert.assertEquals(expectedReadOnlyChargeDefinitionIdentifiers, readOnlyChargeDefinitionIdentifiers);
+    Assert.assertEquals(expectedChangeableChargeDefinitionIdentifiers, changeableChargeDefinitionIdentifiers);
   }
 
   @Test
   public void shouldDeleteChargeDefinition() throws InterruptedException {
     final Product product = createProduct();
 
-    final List<ChargeDefinition> charges = portfolioManager.getAllChargeDefinitionsForProduct(product.getIdentifier());
-    final ChargeDefinition chargeDefinitionToDelete = charges.get(0);
+    final ChargeDefinition chargeDefinitionToDelete = new ChargeDefinition();
+    chargeDefinitionToDelete.setAmount(BigDecimal.TEN);
+    chargeDefinitionToDelete.setIdentifier("blah");
+    chargeDefinitionToDelete.setName("blah blah");
+    chargeDefinitionToDelete.setDescription("blah blah blah");
+    chargeDefinitionToDelete.setChargeAction(Action.APPROVE.name());
+    chargeDefinitionToDelete.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+    chargeDefinitionToDelete.setToAccountDesignator(AccountDesignators.ARREARS_ALLOWANCE);
+    chargeDefinitionToDelete.setFromAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
+    portfolioManager.createChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete);
+    Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_CHARGE_DEFINITION,
+        new ChargeDefinitionEvent(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier())));
+
     portfolioManager.deleteChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier());
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.DELETE_PRODUCT_CHARGE_DEFINITION,
-            new ChargeDefinitionEvent(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier())));
+        new ChargeDefinitionEvent(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier())));
 
     try {
       portfolioManager.getChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete.getIdentifier());
@@ -77,6 +105,13 @@
     catch (final NotFoundException ignored) { }
   }
 
+  @Test(expected = ChargeDefinitionIsReadOnly.class)
+  public void shouldNotDeleteReadOnlyChargeDefinition() throws InterruptedException {
+    final Product product = createProduct();
+
+    portfolioManager.deleteChargeDefinition(product.getIdentifier(), ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID);
+  }
+
   @Test
   public void shouldCreateChargeDefinition() throws InterruptedException {
     final Product product = createProduct();
@@ -120,4 +155,31 @@
 
     Assert.assertEquals(interestChargeDefinition, chargeDefinitionAsChanged);
   }
+
+  @Test
+  public void shouldNotChangeDisbursalChargeDefinition() throws InterruptedException {
+    final Product product = createProduct();
+
+    final ChargeDefinition originalDisbursalChargeDefinition
+        = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSE_PAYMENT_ID);
+
+    final ChargeDefinition disbursalChargeDefinition
+        = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSE_PAYMENT_ID);
+    disbursalChargeDefinition.setProportionalTo(ChargeProportionalDesignator.NOT_PROPORTIONAL.getValue());
+    disbursalChargeDefinition.setReadOnly(false);
+
+    try {
+      portfolioManager.changeChargeDefinition(
+          product.getIdentifier(),
+          disbursalChargeDefinition.getIdentifier(),
+          disbursalChargeDefinition);
+      Assert.fail("Changing a readonly charge definition should fail.");
+    }
+    catch (final ChargeDefinitionIsReadOnly ignore) { }
+
+    final ChargeDefinition chargeDefinitionAsChanged
+        = portfolioManager.getChargeDefinition(product.getIdentifier(), disbursalChargeDefinition.getIdentifier());
+
+    Assert.assertEquals(originalDisbursalChargeDefinition, chargeDefinitionAsChanged);
+  }
 }
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index ed4c8a0..3de3a47 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -106,6 +106,7 @@
             BigDecimal.valueOf(0.01),
             ENTRY,
             PROCESSING_FEE_INCOME);
+    processingFee.setReadOnly(false);
 
     final ChargeDefinition loanOriginationFee = charge(
             LOAN_ORIGINATION_FEE_NAME,
@@ -113,6 +114,7 @@
             BigDecimal.valueOf(0.01),
             ENTRY,
             ORIGINATION_FEE_INCOME);
+    loanOriginationFee.setReadOnly(false);
 
     final ChargeDefinition loanFundsAllocation = charge(
             LOAN_FUNDS_ALLOCATION_ID,
@@ -120,6 +122,7 @@
             BigDecimal.valueOf(1.00),
             LOAN_FUNDS_SOURCE,
             PENDING_DISBURSAL);
+    loanFundsAllocation.setReadOnly(true);
 
     final ChargeDefinition disbursementFee = charge(
             DISBURSEMENT_FEE_NAME,
@@ -127,6 +130,7 @@
             BigDecimal.valueOf(0.001),
             ENTRY,
             DISBURSEMENT_FEE_INCOME);
+    disbursementFee.setReadOnly(false);
 
     final ChargeDefinition disbursePayment = new ChargeDefinition();
     disbursePayment.setChargeAction(Action.DISBURSE.name());
@@ -138,6 +142,7 @@
     disbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
     disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     disbursePayment.setAmount(BigDecimal.ONE);
+    disbursePayment.setReadOnly(true);
 
     final ChargeDefinition trackPrincipalDisbursePayment = new ChargeDefinition();
     trackPrincipalDisbursePayment.setChargeAction(Action.DISBURSE.name());
@@ -149,6 +154,7 @@
     trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
     trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     trackPrincipalDisbursePayment.setAmount(BigDecimal.ONE);
+    trackPrincipalDisbursePayment.setReadOnly(true);
 
     //TODO: Make payable at time of ACCEPT_PAYMENT but accrued at MARK_LATE
     final ChargeDefinition lateFee = charge(
@@ -160,6 +166,7 @@
     lateFee.setAccrueAction(Action.MARK_LATE.name());
     lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
     lateFee.setProportionalTo(ChargeIdentifiers.REPAYMENT_ID);
+    lateFee.setReadOnly(false);
 
     //TODO: Make multiple write off allowance charges.
     final ChargeDefinition writeOffAllowanceCharge = charge(
@@ -169,6 +176,7 @@
             PENDING_DISBURSAL,
             ARREARS_ALLOWANCE);
     writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
+    writeOffAllowanceCharge.setReadOnly(true);
 
     final ChargeDefinition interestCharge = charge(
         INTEREST_NAME,
@@ -180,6 +188,7 @@
     interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
     interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
     interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
+    interestCharge.setReadOnly(false);
 
     final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
     customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
@@ -191,6 +200,7 @@
     customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
     customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     customerRepaymentCharge.setAmount(BigDecimal.ONE);
+    customerRepaymentCharge.setReadOnly(true);
 
     final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
     trackReturnPrincipalCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
@@ -202,6 +212,7 @@
     trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
     trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
     trackReturnPrincipalCharge.setAmount(BigDecimal.ONE);
+    trackReturnPrincipalCharge.setReadOnly(true);
 
     final ChargeDefinition disbursementReturnCharge = charge(
             RETURN_DISBURSEMENT_NAME,
@@ -209,7 +220,8 @@
             BigDecimal.valueOf(1.0),
             PENDING_DISBURSAL,
             LOAN_FUNDS_SOURCE);
-    interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
+    disbursementReturnCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
+    disbursementReturnCharge.setReadOnly(true);
 
     ret.add(processingFee);
     ret.add(loanOriginationFee);
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index 4718e1b..91958f5 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
@@ -20,6 +20,8 @@
 import io.mifos.portfolio.service.internal.repository.ChargeDefinitionEntity;
 import io.mifos.portfolio.service.internal.repository.ProductEntity;
 
+import java.util.Optional;
+
 import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
 
 /**
@@ -43,6 +45,7 @@
     ret.setFromAccountDesignator(chargeDefinition.getFromAccountDesignator());
     ret.setAccrualAccountDesignator(chargeDefinition.getAccrualAccountDesignator());
     ret.setToAccountDesignator(chargeDefinition.getToAccountDesignator());
+    ret.setReadOnly(chargeDefinition.isReadOnly());
 
     return ret;
   }
@@ -62,10 +65,42 @@
     ret.setFromAccountDesignator(from.getFromAccountDesignator());
     ret.setAccrualAccountDesignator(from.getAccrualAccountDesignator());
     ret.setToAccountDesignator(from.getToAccountDesignator());
+    ret.setReadOnly(Optional.ofNullable(from.getReadOnly()).orElseGet(() -> readOnlyLegacyMapper(from.getIdentifier())));
 
     return ret;
   }
 
+  private static Boolean readOnlyLegacyMapper(final String identifier) {
+    switch (identifier) {
+      case LOAN_FUNDS_ALLOCATION_ID:
+        return true;
+      case RETURN_DISBURSEMENT_ID:
+        return true;
+      case INTEREST_ID:
+        return false;
+      case ALLOW_FOR_WRITE_OFF_ID:
+        return false;
+      case LATE_FEE_ID:
+        return true;
+      case DISBURSEMENT_FEE_ID:
+        return false;
+      case DISBURSE_PAYMENT_ID:
+        return false;
+      case TRACK_DISBURSAL_PAYMENT_ID:
+        return false;
+      case LOAN_ORIGINATION_FEE_ID:
+        return true;
+      case PROCESSING_FEE_ID:
+        return true;
+      case REPAYMENT_ID:
+        return false;
+      case TRACK_RETURN_PRINCIPAL_ID:
+        return false;
+      default:
+        return false;
+    }
+  }
+
   private static String proportionalToLegacyMapper(final ChargeDefinitionEntity from,
                                                    final ChargeDefinition.ChargeMethod chargeMethod,
                                                    final String identifier) {
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
index 3f0eab0..423c871 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
@@ -76,6 +76,9 @@
   @Column(name = "for_cycle_size_unit")
   private ChronoUnit forCycleSizeUnit;
 
+  @Column(name = "read_only")
+  private Boolean readOnly;
+
   public ChargeDefinitionEntity() {
   }
 
@@ -191,6 +194,14 @@
     this.forCycleSizeUnit = forCycleSizeUnit;
   }
 
+  public Boolean getReadOnly() {
+    return readOnly;
+  }
+
+  public void setReadOnly(Boolean readOnly) {
+    this.readOnly = readOnly;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
index d460ea7..9e74ff2 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
@@ -84,8 +84,11 @@
   {
     checkProductExists(productIdentifier);
 
+    if (instance.isReadOnly())
+      throw ServiceException.badRequest("Created charges cannot be read only.");
+
     chargeDefinitionService.findByIdentifier(productIdentifier, instance.getIdentifier())
-            .ifPresent(taskDefinition -> {throw ServiceException.conflict("Duplicate identifier: " + taskDefinition.getIdentifier());});
+        .ifPresent(taskDefinition -> {throw ServiceException.conflict("Duplicate identifier: " + taskDefinition.getIdentifier());});
 
     this.commandGateway.process(new CreateChargeDefinitionCommand(productIdentifier, instance));
     return new ResponseEntity<>(HttpStatus.ACCEPTED);
@@ -105,7 +108,7 @@
     checkProductExists(productIdentifier);
 
     return chargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier).orElseThrow(
-            () -> ServiceException.notFound("No charge definition with the identifier '" + chargeDefinitionIdentifier  + "' found."));
+        () -> ServiceException.notFound("No charge definition with the identifier '" + chargeDefinitionIdentifier  + "' found."));
   }
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@@ -120,7 +123,7 @@
           @PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier,
           @RequestBody @Valid final ChargeDefinition instance)
   {
-    checkChargeExistsInProduct(productIdentifier, chargeDefinitionIdentifier);
+    checkChargeExistsInProductAndIsNotReadOnly(productIdentifier, chargeDefinitionIdentifier);
 
     if (!chargeDefinitionIdentifier.equals(instance.getIdentifier()))
       throw ServiceException.badRequest("Instance identifiers may not be changed.");
@@ -141,17 +144,22 @@
           @PathVariable("productidentifier") final String productIdentifier,
           @PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier)
   {
-    checkChargeExistsInProduct(productIdentifier, chargeDefinitionIdentifier);
+    checkChargeExistsInProductAndIsNotReadOnly(productIdentifier, chargeDefinitionIdentifier);
 
     commandGateway.process(new DeleteProductChargeDefinitionCommand(productIdentifier, chargeDefinitionIdentifier));
 
     return ResponseEntity.accepted().build();
   }
 
-  private void checkChargeExistsInProduct(final String productIdentifier,
-                                          final String chargeDefinitionIdentifier) {
-    chargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier).orElseThrow(
-            () -> ServiceException.notFound("No charge definition in the product " + productIdentifier + "with the identifier '" + chargeDefinitionIdentifier  + "' found."));
+  private void checkChargeExistsInProductAndIsNotReadOnly(final String productIdentifier,
+                                                          final String chargeDefinitionIdentifier) {
+    final boolean readOnly = chargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier)
+        .orElseThrow(() -> ServiceException.notFound("No charge definition ''{0}.{1}'' found.",
+            productIdentifier, chargeDefinitionIdentifier))
+        .isReadOnly();
+
+    if (readOnly)
+      throw ServiceException.conflict("Charge definition is read only ''{0}''", chargeDefinitionIdentifier);
   }
 
   private void checkProductExists(final String productIdentifier) {
diff --git a/service/src/main/resources/db/migrations/mariadb/V5__readonly_charges.sql b/service/src/main/resources/db/migrations/mariadb/V5__readonly_charges.sql
new file mode 100644
index 0000000..7e8856d
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V5__readonly_charges.sql
@@ -0,0 +1,17 @@
+--
+-- Copyright 2017 The Mifos Initiative.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--    http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN read_only BOOLEAN NULL DEFAULT NULL;
\ No newline at end of file