FINERACT-1783: Customizeable LoanRepaymentScheduleTransactionProcessor
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
f2756b25bb
commit
fc895fce05
@@ -1,91 +1,5 @@
|
||||
= Modules
|
||||
|
||||
NOTE: Currently modules are a proof of concept feature in Fineract.
|
||||
We are currently working towards a fully modular codebase and will publish more here when we are ready.
|
||||
|
||||
== Custom modules
|
||||
|
||||
Creating customizations for Fineract services is easy. The method described here will work both with our future module guidelines (aka "clean room" modules) and with the intermediary solution we will put in place to avoid major refactorings.
|
||||
|
||||
The folder structure for modules is based on a convention that ensures that your extensions don't clash with Fineract's internals. This is to make sure that your downstream forks of Fineract are easy to sync. In the past we had all kinds of strategies to add custom code - including editing existing sources in `fineract-provider`. This is not recommended.
|
||||
|
||||
IMPORTANT: At the moment the only service(s) we prepared to be overridden/replaced are `org.apache.fineract.portfolio.note.service.NoteReadPlatformService` and `org.apache.fineract.portfolio.note.service.NoteWritePlatformService`. Please reach out on the developer mailing list if you need other services.
|
||||
|
||||
The recommended folder structure is very simple. If you follow this recommendation you'll get some additional benefits, e. g. you don't even have to edit `settings.gradle` to include your new custom modules. Your modules will also be automatically included in a custom Fineract Docker image build that you can use for your production deployments.
|
||||
|
||||
Let's assume your company/org is called "ACME Inc." and you are trying to (fully/partially) replace an existing Fineract service, let's say those in `org.apache.fineract.portfolio.note`. The recommended folder structure would then look something like this:
|
||||
|
||||
[plantuml, format=svg]
|
||||
----
|
||||
include::{diagramsdir}/custom-folder-structure.puml[]
|
||||
----
|
||||
|
||||
As soon as we can publish Fineract module JARs to Maven Central you'll have more freedom to setup your projects (including to setup separate Git repos). But for now please follow these instructions:
|
||||
|
||||
1. Create a folder under `custom` and name it according to your company/organisation (e. g. `acme` if your company is `ACME Inc.`); this way your custom modules can't clash even with other companies' modules
|
||||
2. Under your company folder create a folder for the `category` or `domain` your module is targeting; e. g. "loan", "client", "account" etc.
|
||||
3. Finally, setup `library` folders for the actual modules you want to create; usually that will be to replace/extend some existing service, so there could be a `service` folder, maybe even a `core` folder, e. g. if you want to add additional DTOs etc.; we have also an example for COB business steps
|
||||
4. Per `category`/`domain` you should have a `starter` library; means: a Spring Boot auto-configuration setup that makes including your module in Fineract easier ("hands-free"); the necessary parts for a auto-configuration library are a Spring Java configuration class (annotated with `@Configuration`) and a text file at `META-INF/spring.factories` in your starter resource folder:
|
||||
+
|
||||
[source,properties]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/starter/src/main/resources/META-INF/spring.factories[]
|
||||
----
|
||||
+
|
||||
Please make sure that your module libraries have proper `build.gradle` files:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/service/build.gradle[lines=19..]
|
||||
----
|
||||
+
|
||||
NOTE: You don't need to edit `settings.gradle` to add your modules/libraries. If you follow above convention they'll get included automatically.
|
||||
+
|
||||
5. The dependency.gradle file could look something like this:
|
||||
[source,groovy]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/service/dependencies.gradle[lines=19..]
|
||||
----
|
||||
|
||||
NOTE: We've included by default some basic and useful dependencies for all custom modules, like Slf4j, Lombok, the usual testing frameworks (JUnit, Cucumber, Mockito etc.)
|
||||
|
||||
IMPORTANT: Do not include your custom module in `fineract-provider`'s dependency.gradle file. This creates a circular dependency and will fail your build.
|
||||
|
||||
=== Database Migrations
|
||||
|
||||
If database migrations are needed as part of your customizations then you can add your own migration scripts. This is again based on conventions:
|
||||
|
||||
1. Create folders `db/custom-changelog` in one of your `resources` folders; we recommend using the resources folder in your starter library, but actually any of your custom libs will do.
|
||||
2. Under `db/custom-changelog` create an XML changelog file, e. g. `changelog-acme-note.xml`; you are free to choose a name for this file, but we recommend being consistent to avoid classpath conflicts.
|
||||
3. Under `db/custom-changelog` create a folder `parts` for your specific changelogs
|
||||
|
||||
[plantuml, format=svg]
|
||||
----
|
||||
include::{diagramsdir}/custom-db-migration-folder-structure.puml[]
|
||||
----
|
||||
|
||||
And here an example migration script:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/starter/src/main/resources/db/custom-changelog/0001_acme_note_initial.xml[lines=22..]
|
||||
----
|
||||
|
||||
IMPORTANT: By default custom database migration changelogs are executed in context `tenant_db`. That makes sure your changes will be applied to the tenant database (read: main database and not the tenant store database). In theory you could also target the tenant configuration database, but it's not recommended to do that.
|
||||
|
||||
=== Deployment
|
||||
|
||||
Custom modules (better: the JAR files) only need to be dropped in Fineract's `libs` folder if you run Fineract from the Spring Boot JAR file. Dynamic loading of external JARs is provided since Fineract version 1.5.0. For your convenience we've created a separate Docker image module that automatically includes your custom modules (see `custom/docker`). You can build this Docker image with
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew :custom:docker:jibDockerBuild
|
||||
----
|
||||
|
||||
The Docker image with included custom modules is called `fineract-custom`.
|
||||
|
||||
NOTE: We'll provide soon a way to customize the Docker image parameters (image name, JVM implementation, JVM args, ports etc.).
|
||||
|
||||
|
||||
=== Outlook
|
||||
|
||||
If this *proof of concept* is accepted we could prepare Fineract's services to be replaceable. This approach works already very well even if we don't have proper JAR libraries published on Maven Central. It's an important goal to separate customized code from Fineract's internals to have soon real modules.
|
||||
NOTE: Even if we are not quite there yet with full modularity you can already create your own custom modules to extend Fineract. Please see chapter <<_custom_modules>>.
|
||||
@@ -0,0 +1,36 @@
|
||||
= Custom Business Steps
|
||||
|
||||
It very easy to add your own business steps to Fineract's default steps:
|
||||
|
||||
1. Create a custom module (e. g. `custom/acme/steps`, follow the instructions on how to create a custom module)
|
||||
2. Create a class that implements interace `org.apache.fineract.cob.COBBusinessStep`
|
||||
3. Provide the custom database migration to add the necessary information about your business step in table `m_batch_business_steps`
|
||||
|
||||
.Business Step Interface
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStep.java[lines=19..]
|
||||
----
|
||||
|
||||
== Business Step Implementation
|
||||
|
||||
.Custom Business Step Implementation Example
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/custom/acme/loan/cob/src/main/java/com/acme/fineract/loan/cob/AcmeNoopBusinessStep.java[lines=19..]
|
||||
----
|
||||
|
||||
As you can see this implementation is very simple and doesn't do much. There are some simple conventions though that you should follow implementing your own business steps:
|
||||
|
||||
1. Make sure the value returned by method `getEnumStyledName()` is unique; it's a good idea to choose a prefix that reflects the name of your organization (in this example `ACME_`)
|
||||
2. You have more freedom for the value returned by `getHumanReadableName()`, but it's a good idea to keep this value as unique as possible
|
||||
|
||||
== Business Step Database Migration
|
||||
|
||||
.Business Step Database Migration Example
|
||||
[source,xml]
|
||||
----
|
||||
include::{rootdir}/custom/acme/loan/cob/src/main/resources/db/custom-changelog/0001_acme_loan_cob.xml[lines=22..]
|
||||
----
|
||||
|
||||
NOTE: See also chapter about batch jobs in this documentation.
|
||||
@@ -0,0 +1,21 @@
|
||||
= Custom Database Migration
|
||||
|
||||
If database migrations are needed as part of your customizations then you can add your own migration scripts. This is again based on conventions:
|
||||
|
||||
1. Create folders `db/custom-changelog` in one of your `resources` folders; we recommend using the resources folder in your starter library, but actually any of your custom libs will do.
|
||||
2. Under `db/custom-changelog` create an XML changelog file, e. g. `changelog-acme-note.xml`; you are free to choose a name for this file, but we recommend being consistent to avoid classpath conflicts.
|
||||
3. Under `db/custom-changelog` create a folder `parts` for your specific changelogs
|
||||
|
||||
[plantuml, format=svg]
|
||||
----
|
||||
include::{diagramsdir}/custom-db-migration-folder-structure.puml[]
|
||||
----
|
||||
|
||||
And here an example migration script:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/starter/src/main/resources/db/custom-changelog/0001_acme_note_initial.xml[lines=22..]
|
||||
----
|
||||
|
||||
IMPORTANT: By default, custom database migration changelogs are executed in context `tenant_db`. That makes sure your changes will be applied to the tenant database (read: main database and not the tenant store database). In theory you could also target the tenant configuration database, but it's not recommended to do that.
|
||||
@@ -0,0 +1,12 @@
|
||||
= Deploying Custom Modules
|
||||
|
||||
Custom modules (better: the JAR files) only need to be dropped in Fineract's `libs` folder if you run Fineract from the Spring Boot JAR file. Dynamic loading of external JARs is provided since Fineract version 1.5.0. For your convenience we've created a separate Docker image module that automatically includes your custom modules (see `custom/docker`). You can build this Docker image with
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew :custom:docker:jibDockerBuild
|
||||
----
|
||||
|
||||
The Docker image with included custom modules is called `fineract-custom`.
|
||||
|
||||
NOTE: We'll provide soon a way to customize the Docker image parameters (image name, JVM implementation, JVM args, ports etc.).
|
||||
@@ -0,0 +1,17 @@
|
||||
= Custom Modules
|
||||
|
||||
NOTE: Currently modules are a proof of concept feature in Fineract.
|
||||
|
||||
include::intro.adoc[leveloffset=+1]
|
||||
|
||||
include::service.adoc[leveloffset=+1]
|
||||
|
||||
include::business-step.adoc[leveloffset=+1]
|
||||
|
||||
include::loan-transaction-processor.adoc[leveloffset=+1]
|
||||
|
||||
include::database-migration.adoc[leveloffset=+1]
|
||||
|
||||
include::deployment.adoc[leveloffset=+1]
|
||||
|
||||
include::outlook.adoc[leveloffset=+1]
|
||||
@@ -0,0 +1,47 @@
|
||||
= Introduction
|
||||
|
||||
Creating customizations for Fineract services is easy. The method described here will work both with our future module guidelines (aka "clean room" modules) and with the intermediary solution we will put in place to avoid major refactorings.
|
||||
|
||||
The folder structure for modules is based on a convention that ensures that your extensions don't clash with Fineract's internals. This is to make sure that your downstream forks of Fineract are easy to sync. In the past we had all kinds of strategies to add custom code - including editing existing sources in `fineract-provider`. This is not recommended.
|
||||
|
||||
IMPORTANT: At the moment the only service(s) we prepared to be overridden/replaced are `org.apache.fineract.portfolio.note.service.NoteReadPlatformService` and `org.apache.fineract.portfolio.note.service.NoteWritePlatformService`. Please reach out on the developer mailing list if you need other services.
|
||||
|
||||
The recommended folder structure is very simple. If you follow this recommendation you'll get some additional benefits, e. g. you don't even have to edit `settings.gradle` to include your new custom modules. Your modules will also be automatically included in a custom Fineract Docker image build that you can use for your production deployments.
|
||||
|
||||
Let's assume your company/org is called "ACME Inc." and you are trying to (fully/partially) replace an existing Fineract service, let's say those in `org.apache.fineract.portfolio.note`. The recommended folder structure would then look something like this:
|
||||
|
||||
[plantuml, format=svg]
|
||||
----
|
||||
include::{diagramsdir}/custom-folder-structure.puml[]
|
||||
----
|
||||
|
||||
As soon as we can publish Fineract module JARs to Maven Central you'll have more freedom to setup your projects (including to setup separate Git repos). But for now please follow these instructions:
|
||||
|
||||
1. Create a folder under `custom` and name it according to your company/organisation (e. g. `acme` if your company is `ACME Inc.`); this way your custom modules can't clash even with other companies' modules
|
||||
2. Under your company folder create a folder for the `category` or `domain` your module is targeting; e. g. "loan", "client", "account" etc.
|
||||
3. Finally, setup `library` folders for the actual modules you want to create; usually that will be to replace/extend some existing service, so there could be a `service` folder, maybe even a `core` folder, e. g. if you want to add additional DTOs etc.; we have also an example for COB business steps
|
||||
4. Per `category`/`domain` you should have a `starter` library; means: a Spring Boot auto-configuration setup that makes including your module in Fineract easier ("hands-free"); the necessary parts for a auto-configuration library are a Spring Java configuration class (annotated with `@Configuration`) and a text file at `META-INF/spring.factories` in your starter resource folder:
|
||||
+
|
||||
[source,properties]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/starter/src/main/resources/META-INF/spring.factories[]
|
||||
----
|
||||
+
|
||||
Please make sure that your module libraries have proper `build.gradle` files:
|
||||
+
|
||||
[source,groovy]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/service/build.gradle[lines=19..]
|
||||
----
|
||||
+
|
||||
NOTE: You don't need to edit `settings.gradle` to add your modules/libraries. If you follow above convention they'll get included automatically.
|
||||
+
|
||||
5. The dependency.gradle file could look something like this:
|
||||
[source,groovy]
|
||||
----
|
||||
include::{rootdir}/custom/acme/note/service/dependencies.gradle[lines=19..]
|
||||
----
|
||||
|
||||
NOTE: We've included by default some basic and useful dependencies for all custom modules, like Slf4j, Lombok, the usual testing frameworks (JUnit, Cucumber, Mockito etc.)
|
||||
|
||||
IMPORTANT: Do not include your custom module in `fineract-provider`'s dependency.gradle file. This creates a circular dependency and will fail your build.
|
||||
@@ -0,0 +1,114 @@
|
||||
= Custom Loan Transaction Processors
|
||||
|
||||
Fineract has 7 built-in loan transaction processors:
|
||||
|
||||
1. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor`
|
||||
2. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor`
|
||||
3. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor`
|
||||
4. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor`
|
||||
5. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor`
|
||||
6. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor`
|
||||
7. `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor`
|
||||
|
||||
.Default Loan Transaction Processor configuration
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java[lines=38..80]
|
||||
----
|
||||
|
||||
All default processor implementations are enabled by default, but can also be prevented from being loaded into memory by a simple configuration in `application.properties`. Use the environment variables you see below in your Kubernetes and Docker Compose deployments to override the default behavior.
|
||||
|
||||
.Default Loan Transaction Processor Application Properties
|
||||
[source,properties]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=64..70]
|
||||
----
|
||||
|
||||
== Implement Processors
|
||||
|
||||
.Loan Transaction Processor Interface
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java[lines=19..]
|
||||
----
|
||||
|
||||
.Custom Loan Transaction Processor Example
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java[lines=19..]
|
||||
----
|
||||
|
||||
The example implementation doesn't do much. We are just overriding one of the default processor implementations `org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor` and give the custom processor it's own lookup code and name (descriptive text for display in UIs, e. g. when configuring a loan product). As usual it is a good idea to follow some simple conventions:
|
||||
|
||||
1. Make sure the value returned by `getCode()` is unique. Prefixing it with characters that reflect your organization name (here `acme-`) is a good idea.
|
||||
2. You have more freedom for the descriptive test returned by `getName()`, but it is still a good idea to keep the value unique to avoid confusion.
|
||||
|
||||
=== Method `getCode()`
|
||||
|
||||
Lookup value that is used to pick a loan transaction processor (see processor factory).
|
||||
|
||||
=== Method `getName()`
|
||||
|
||||
Descriptive text about the loan transaction processor that is mostly used in user interfaces.
|
||||
|
||||
=== Method `handleTransaction()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `handleWriteOff()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `handleRepaymentSchedule()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `isInterestFirstRepaymentScheduleTransactionProcessor()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `handleRefund()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `handleChargeback()`
|
||||
|
||||
TBD
|
||||
|
||||
=== Method `processTransactionsFromDerivedFields()`
|
||||
|
||||
TBD
|
||||
|
||||
== Override Processor Factory
|
||||
|
||||
The processor factory has no reference to any specific implementation of the loan transaction processor interface. All avaible implementations will be injected here (internal default and custom implementations). Processor instances can be looked up via method `determineProcessor()`. You can pass either the code of the processor or the processor's name to look it up. If a matching processor can't be found then the factory function will either return the default instance or fails with an exception depending on the configuration in `application.properties`.
|
||||
|
||||
IMPORTANT: It is preferable to use the processor code to lookup processor instances. Lookups via processor names are only done in the import service via Excel sheets (should be fixed).
|
||||
|
||||
.Loan Transaction Processor Factory Implementation
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleTransactionProcessorFactory.java[lines=19..]
|
||||
----
|
||||
|
||||
This is the default factory auto-configuration.
|
||||
|
||||
.Loan Transaction Processor Factory Auto-Configuration
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java[lines=81..87]
|
||||
----
|
||||
|
||||
If you need then you can override this, e.g. because you want to set a different default processor then you can do so in your custom module's auto-configuration.
|
||||
|
||||
.Custom Loan Transaction Processor Factory Auto-Configuration Example
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/custom/acme/loan/starter/src/main/java/com/acme/fineract/loan/starter/AcmeLoanAutoConfiguration.java[lines=36..41]
|
||||
----
|
||||
|
||||
.Processor Lookup Failure Configuration Property
|
||||
[source,properties]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=71..71]
|
||||
----
|
||||
@@ -0,0 +1,3 @@
|
||||
= Outlook
|
||||
|
||||
If this *proof of concept* is accepted we could prepare more of Fineract's internal services to be replaceable. This approach works already very well even if we don't have proper JAR libraries published on Maven Central. It's an important goal to separate customized code from Fineract's internals to have soon real modules.
|
||||
@@ -0,0 +1,29 @@
|
||||
= Note Service
|
||||
|
||||
The Note service is responsible for ... TBD
|
||||
|
||||
NOTE: We chose the note service because it's interface is very simple and has not many cross dependencies.
|
||||
|
||||
== Interfaces
|
||||
|
||||
.Note Read Service Interface
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteReadPlatformService.java[lines=19..]
|
||||
----
|
||||
|
||||
.Note Write Service Interface
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteReadPlatformService.java[lines=19..]
|
||||
----
|
||||
|
||||
== Auto Start Configuration
|
||||
|
||||
The rules to replace the Note services are very simple. If you provide an alternative implementation of the services then the default implementations will *not* be loaded.
|
||||
|
||||
.Note Auto Starter Configuration
|
||||
[source,java]
|
||||
----
|
||||
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/starter/NoteAutoConfiguration.java[lines=19..]
|
||||
----
|
||||
@@ -0,0 +1,5 @@
|
||||
= Custom Services
|
||||
|
||||
NOTE: We are still trying to figure out which internal services make most sense to be pluggable. Please join the discussion and let us know if you have a specific requirement.
|
||||
|
||||
include::service-note.adoc[leveloffset=+1]
|
||||
@@ -19,6 +19,7 @@
|
||||
| Michael Vorburger | mailto:mike@vorburger.ch[]
|
||||
| Aleksandar Vidakovic | mailto:aleks@apache.org[]
|
||||
| Arnold Galovics | mailto:arnold@apache.org[]
|
||||
| Adam Saghy | mailto:adamsaghy@gmail.com[]
|
||||
|===
|
||||
|
||||
//ifdef::backend-pdf[]
|
||||
@@ -34,7 +35,10 @@
|
||||
| Arnold Galovics | Database architecture section | 2022-03-11 | 1.7.0
|
||||
| Aleksandar Vidakovic | Module section | 2022-03-23 | 1.7.0
|
||||
| Aleksandar Vidakovic | Release section, file re-organisation | 2022-04-18 | 1.7.0
|
||||
| Arnold Galovics | Spring Batch | 2022-03-11 | 1.7.0
|
||||
| Aleksandar Vidakovic | AsciiDoc cheatsheet | 2022-05-09 | 1.7.0
|
||||
| Adam Saghy | Business data | 2022-06-13 | 1.7.0
|
||||
| Aleksandar Vidakovic | Custom modules and extensions | 2022-10-29 | 1.8.0
|
||||
|===
|
||||
|
||||
toc::[]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -37,6 +37,8 @@ include::chapters/architecture/index.adoc[leveloffset=+1]
|
||||
|
||||
include::chapters/development/index.adoc[leveloffset=+1]
|
||||
|
||||
include::chapters/custom/index.adoc[leveloffset=+1]
|
||||
|
||||
include::chapters/security/index.adoc[leveloffset=+1]
|
||||
|
||||
include::chapters/testing/index.adoc[leveloffset=+1]
|
||||
|
||||
Reference in New Issue
Block a user