diff --git a/front/.bowerrc b/front/.bowerrc new file mode 100755 index 0000000..69fad35 --- /dev/null +++ b/front/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "bower_components" +} diff --git a/front/.editorconfig b/front/.editorconfig new file mode 100755 index 0000000..e717f5e --- /dev/null +++ b/front/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/front/.gitignore b/front/.gitignore new file mode 100755 index 0000000..1f309e5 --- /dev/null +++ b/front/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +bower_components/ +.sass-cache/ +.idea/ +.tmp/ +dist/ diff --git a/front/.jshintrc b/front/.jshintrc new file mode 100755 index 0000000..f230768 --- /dev/null +++ b/front/.jshintrc @@ -0,0 +1,17 @@ +{ + "strict": true, + "bitwise": true, + "curly": true, + "eqeqeq": true, + "latedef": false, + "noarg": true, + "undef": true, + "unused": true, + "validthis": true, + "jasmine": true, + "globals": { + "angular": false, + "inject": false, + "module": false + } +} diff --git a/front/.yo-rc.json b/front/.yo-rc.json new file mode 100755 index 0000000..117f89c --- /dev/null +++ b/front/.yo-rc.json @@ -0,0 +1,70 @@ +{ + "generator-gulp-angular": { + "version": "0.12.1", + "props": { + "angularVersion": "~1.4.0", + "angularModules": [ + { + "key": "animate", + "module": "ngAnimate" + }, + { + "key": "cookies", + "module": "ngCookies" + }, + { + "key": "touch", + "module": "ngTouch" + }, + { + "key": "sanitize", + "module": "ngSanitize" + } + ], + "jQuery": { + "key": "jquery2" + }, + "resource": { + "key": "angular-resource", + "module": "ngResource" + }, + "router": { + "key": "angular-route", + "module": "ngRoute" + }, + "ui": { + "key": "bootstrap", + "module": null + }, + "bootstrapComponents": { + "key": "none", + "module": null + }, + "cssPreprocessor": { + "key": "less", + "extension": "less" + }, + "jsPreprocessor": { + "key": "none", + "extension": "js", + "srcExtension": "js" + }, + "htmlPreprocessor": { + "key": "none", + "extension": "html" + }, + "foundationComponents": { + "name": null, + "version": null, + "key": null, + "module": null + }, + "paths": { + "src": "src", + "dist": "dist", + "e2e": "e2e", + "tmp": ".tmp" + } + } + } +} \ No newline at end of file diff --git a/front/bower.json b/front/bower.json new file mode 100755 index 0000000..d097a09 --- /dev/null +++ b/front/bower.json @@ -0,0 +1,30 @@ +{ + "name": "loanWizard", + "version": "0.0.0", + "dependencies": { + "angular-animate": "~1.4.0", + "angular-cookies": "~1.4.0", + "angular-touch": "~1.4.0", + "angular-sanitize": "~1.4.0", + "jquery": "~2.1.4", + "angular-resource": "~1.4.0", + "angular-route": "~1.4.0", + "bootstrap": "~3.3.4", + "malarkey": "yuanqing/malarkey#~1.3.0", + "toastr": "~2.1.1", + "moment": "~2.10.3", + "animate.css": "~3.3.0", + "angular": "~1.4.0", + "Chart.js": "~1.0.2", + "eonasdan-bootstrap-datetimepicker": "~4.14.30", + "loaders.css": "0.1.0", + "underscore": "~1.8.3" + }, + "devDependencies": { + "angular-mocks": "~1.4.0" + }, + "resolutions": { + "jquery": "~2.1.4", + "angular": "~1.4.0" + } +} diff --git a/front/e2e/.jshintrc b/front/e2e/.jshintrc new file mode 100755 index 0000000..5540069 --- /dev/null +++ b/front/e2e/.jshintrc @@ -0,0 +1,10 @@ +{ + "extends": "../.jshintrc", + "globals": { + "browser": false, + "element": false, + "by": false, + "$": false, + "$$": false + } +} diff --git a/front/e2e/main.po.js b/front/e2e/main.po.js new file mode 100755 index 0000000..0f0428c --- /dev/null +++ b/front/e2e/main.po.js @@ -0,0 +1,15 @@ +/** + * This file uses the Page Object pattern to define the main page for tests + * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ + */ + +'use strict'; + +var MainPage = function() { + this.jumbEl = element(by.css('.jumbotron')); + this.h1El = this.jumbEl.element(by.css('h1')); + this.imgEl = this.jumbEl.element(by.css('img')); + this.thumbnailEls = element(by.css('body')).all(by.repeater('awesomeThing in main.awesomeThings')); +}; + +module.exports = new MainPage(); diff --git a/front/e2e/main.spec.js b/front/e2e/main.spec.js new file mode 100755 index 0000000..ef2e5c1 --- /dev/null +++ b/front/e2e/main.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('The main view', function () { + var page; + + beforeEach(function () { + browser.get('/index.html'); + page = require('./main.po'); + }); + + it('should include jumbotron with correct data', function() { + expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); + expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); + expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); + }); + + it('should list more than 5 awesome things', function () { + expect(page.thumbnailEls.count()).toBeGreaterThan(5); + }); + +}); diff --git a/front/gulp/.jshintrc b/front/gulp/.jshintrc new file mode 100755 index 0000000..072135c --- /dev/null +++ b/front/gulp/.jshintrc @@ -0,0 +1,4 @@ +{ + "extends": "../.jshintrc", + "node": true +} diff --git a/front/gulp/build.js b/front/gulp/build.js new file mode 100755 index 0000000..0e75010 --- /dev/null +++ b/front/gulp/build.js @@ -0,0 +1,94 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var $ = require('gulp-load-plugins')({ + pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] +}); + +gulp.task('partials', function () { + return gulp.src([ + path.join(conf.paths.src, '/app/**/*.html'), + path.join(conf.paths.tmp, '/serve/app/**/*.html') + ]) + .pipe($.minifyHtml({ + empty: true, + spare: true, + quotes: true + })) + .pipe($.angularTemplatecache('templateCacheHtml.js', { + module: 'loanWizard', + root: 'app' + })) + .pipe(gulp.dest(conf.paths.tmp + '/partials/')); +}); + +gulp.task('html', ['inject', 'partials'], function () { + var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false }); + var partialsInjectOptions = { + starttag: '', + ignorePath: path.join(conf.paths.tmp, '/partials'), + addRootSlash: false + }; + + var htmlFilter = $.filter('*.html'); + var jsFilter = $.filter('**/*.js'); + var cssFilter = $.filter('**/*.css'); + var assets; + + return gulp.src(path.join(conf.paths.tmp, '/serve/*.html')) + .pipe($.inject(partialsInjectFile, partialsInjectOptions)) + .pipe(assets = $.useref.assets()) + .pipe($.rev()) + .pipe(jsFilter) + .pipe($.ngAnnotate()) + .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) + .pipe(jsFilter.restore()) + .pipe(cssFilter) + .pipe($.replace('../../bower_components/bootstrap/fonts/', '../fonts/')) + .pipe($.csso()) + .pipe(cssFilter.restore()) + .pipe(assets.restore()) + .pipe($.useref()) + .pipe($.revReplace()) + .pipe(htmlFilter) + .pipe($.minifyHtml({ + empty: true, + spare: true, + quotes: true, + conditionals: true + })) + .pipe(htmlFilter.restore()) + .pipe(gulp.dest(path.join(conf.paths.dist, '/'))) + .pipe($.size({ title: path.join(conf.paths.dist, '/'), showFiles: true })); +}); + +// Only applies for fonts from bower dependencies +// Custom fonts are handled by the "other" task +gulp.task('fonts', function () { + return gulp.src($.mainBowerFiles()) + .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) + .pipe($.flatten()) + .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); +}); + +gulp.task('other', function () { + var fileFilter = $.filter(function (file) { + return file.stat.isFile(); + }); + + return gulp.src([ + path.join(conf.paths.src, '/**/*'), + path.join('!' + conf.paths.src, '/**/*.{html,css,js,less}') + ]) + .pipe(fileFilter) + .pipe(gulp.dest(path.join(conf.paths.dist, '/'))); +}); + +gulp.task('clean', function (done) { + $.del([path.join(conf.paths.dist, '/'), path.join(conf.paths.tmp, '/')], done); +}); + +gulp.task('build', ['html', 'fonts', 'other']); diff --git a/front/gulp/conf.js b/front/gulp/conf.js new file mode 100755 index 0000000..8f08ac6 --- /dev/null +++ b/front/gulp/conf.js @@ -0,0 +1,41 @@ +/** + * This file contains the variables used in other gulp files + * which defines tasks + * By design, we only put there very generic config values + * which are used in several places to keep good readability + * of the tasks + */ + +var gutil = require('gulp-util'); + +/** + * The main paths of your project handle these with care + */ +exports.paths = { + src: 'src', + dist: 'dist', + tmp: '.tmp', + e2e: 'e2e' +}; + +/** + * Wiredep is the lib which inject bower dependencies in your project + * Mainly used to inject script tags in the index.html but also used + * to inject css preprocessor deps and js files in karma + */ +exports.wiredep = { + exclude: [/bootstrap.js$/, /bootstrap\.css/], + directory: 'bower_components' +}; + +/** + * Common implementation for an error handler of a Gulp plugin + */ +exports.errorHandler = function(title) { + 'use strict'; + + return function(err) { + gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); + this.emit('end'); + }; +}; diff --git a/front/gulp/e2e-tests.js b/front/gulp/e2e-tests.js new file mode 100755 index 0000000..3a66702 --- /dev/null +++ b/front/gulp/e2e-tests.js @@ -0,0 +1,38 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var browserSync = require('browser-sync'); + +var $ = require('gulp-load-plugins')(); + +// Downloads the selenium webdriver +gulp.task('webdriver-update', $.protractor.webdriver_update); + +gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); + +function runProtractor (done) { + var params = process.argv; + var args = params.length > 3 ? [params[3], params[4]] : []; + + gulp.src(path.join(conf.paths.e2e, '/**/*.js')) + .pipe($.protractor.protractor({ + configFile: 'protractor.conf.js', + args: args + })) + .on('error', function (err) { + // Make sure failed tests cause gulp to exit non-zero + throw err; + }) + .on('end', function () { + // Close browser sync server + browserSync.exit(); + done(); + }); +} + +gulp.task('protractor', ['protractor:src']); +gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); +gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); diff --git a/front/gulp/inject.js b/front/gulp/inject.js new file mode 100755 index 0000000..f1189f9 --- /dev/null +++ b/front/gulp/inject.js @@ -0,0 +1,36 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var $ = require('gulp-load-plugins')(); + +var wiredep = require('wiredep').stream; +var _ = require('lodash'); + +gulp.task('inject', ['scripts', 'styles'], function () { + var injectStyles = gulp.src([ + path.join(conf.paths.tmp, '/serve/app/**/*.css'), + path.join('!' + conf.paths.tmp, '/serve/app/vendor.css') + ], { read: false }); + + var injectScripts = gulp.src([ + path.join(conf.paths.src, '/app/**/*.module.js'), + path.join(conf.paths.src, '/app/**/*.js'), + path.join('!' + conf.paths.src, '/app/**/*.spec.js'), + path.join('!' + conf.paths.src, '/app/**/*.mock.js') + ]) + .pipe($.angularFilesort()).on('error', conf.errorHandler('AngularFilesort')); + + var injectOptions = { + ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], + addRootSlash: false + }; + + return gulp.src(path.join(conf.paths.src, '/*.html')) + .pipe($.inject(injectStyles, injectOptions)) + .pipe($.inject(injectScripts, injectOptions)) + .pipe(wiredep(_.extend({}, conf.wiredep))) + .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve'))); +}); diff --git a/front/gulp/scripts.js b/front/gulp/scripts.js new file mode 100755 index 0000000..7a3e536 --- /dev/null +++ b/front/gulp/scripts.js @@ -0,0 +1,17 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var browserSync = require('browser-sync'); + +var $ = require('gulp-load-plugins')(); + +gulp.task('scripts', function () { + return gulp.src(path.join(conf.paths.src, '/app/**/*.js')) + .pipe($.jshint()) + .pipe($.jshint.reporter('jshint-stylish')) + .pipe(browserSync.reload({ stream: true })) + .pipe($.size()) +}); diff --git a/front/gulp/server.js b/front/gulp/server.js new file mode 100755 index 0000000..7c03f18 --- /dev/null +++ b/front/gulp/server.js @@ -0,0 +1,63 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var browserSync = require('browser-sync'); +var browserSyncSpa = require('browser-sync-spa'); + +var util = require('util'); + +var proxyMiddleware = require('http-proxy-middleware'); + +function browserSyncInit(baseDir, browser) { + browser = browser === undefined ? 'default' : browser; + + var routes = null; + if(baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) { + routes = { + '/bower_components': 'bower_components' + }; + } + + var server = { + baseDir: baseDir, + routes: routes + }; + + /* + * You can add a proxy to your backend by uncommenting the line bellow. + * You just have to configure a context which will we redirected and the target url. + * Example: $http.get('/users') requests will be automatically proxified. + * + * For more details and option, https://github.com/chimurai/http-proxy-middleware/blob/v0.0.5/README.md + */ + // server.middleware = proxyMiddleware('/users', {target: 'http://jsonplaceholder.typicode.com', proxyHost: 'jsonplaceholder.typicode.com'}); + + browserSync.instance = browserSync.init({ + startPath: '/', + server: server, + browser: browser + }); +} + +browserSync.use(browserSyncSpa({ + selector: '[ng-app]'// Only needed for angular apps +})); + +gulp.task('serve', ['watch'], function () { + browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); +}); + +gulp.task('serve:dist', ['build'], function () { + browserSyncInit(conf.paths.dist); +}); + +gulp.task('serve:e2e', ['inject'], function () { + browserSyncInit([conf.paths.tmp + '/serve', conf.paths.src], []); +}); + +gulp.task('serve:e2e-dist', ['build'], function () { + browserSyncInit(conf.paths.dist, []); +}); diff --git a/front/gulp/styles.js b/front/gulp/styles.js new file mode 100755 index 0000000..8a5d20c --- /dev/null +++ b/front/gulp/styles.js @@ -0,0 +1,49 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var browserSync = require('browser-sync'); + +var $ = require('gulp-load-plugins')(); + +var wiredep = require('wiredep').stream; +var _ = require('lodash'); + +gulp.task('styles', function () { + var lessOptions = { + options: [ + 'bower_components', + path.join(conf.paths.src, '/app') + ] + }; + + var injectFiles = gulp.src([ + path.join(conf.paths.src, '/app/**/*.less'), + path.join('!' + conf.paths.src, '/app/index.less') + ], { read: false }); + + var injectOptions = { + transform: function(filePath) { + filePath = filePath.replace(conf.paths.src + '/app/', ''); + return '@import "' + filePath + '";'; + }, + starttag: '// injector', + endtag: '// endinjector', + addRootSlash: false + }; + + + return gulp.src([ + path.join(conf.paths.src, '/app/index.less') + ]) + .pipe($.inject(injectFiles, injectOptions)) + .pipe(wiredep(_.extend({}, conf.wiredep))) + .pipe($.sourcemaps.init()) + .pipe($.less(lessOptions)).on('error', conf.errorHandler('Less')) + .pipe($.autoprefixer()).on('error', conf.errorHandler('Autoprefixer')) + .pipe($.sourcemaps.write()) + .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve/app/'))) + .pipe(browserSync.reload({ stream: true })); +}); diff --git a/front/gulp/unit-tests.js b/front/gulp/unit-tests.js new file mode 100755 index 0000000..5e6aa75 --- /dev/null +++ b/front/gulp/unit-tests.js @@ -0,0 +1,25 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var karma = require('karma'); + +function runTests (singleRun, done) { + karma.server.start({ + configFile: path.join(__dirname, '/../karma.conf.js'), + singleRun: singleRun, + autoWatch: !singleRun + }, function() { + done(); + }); +} + +gulp.task('test', ['scripts'], function(done) { + runTests(true, done); +}); + +gulp.task('test:auto', ['watch'], function(done) { + runTests(false, done); +}); diff --git a/front/gulp/watch.js b/front/gulp/watch.js new file mode 100755 index 0000000..8bc37f1 --- /dev/null +++ b/front/gulp/watch.js @@ -0,0 +1,39 @@ +'use strict'; + +var path = require('path'); +var gulp = require('gulp'); +var conf = require('./conf'); + +var browserSync = require('browser-sync'); + +function isOnlyChange(event) { + return event.type === 'changed'; +} + +gulp.task('watch', ['inject'], function () { + + gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject']); + + gulp.watch([ + path.join(conf.paths.src, '/app/**/*.css'), + path.join(conf.paths.src, '/app/**/*.less') + ], function(event) { + if(isOnlyChange(event)) { + gulp.start('styles'); + } else { + gulp.start('inject'); + } + }); + + gulp.watch(path.join(conf.paths.src, '/app/**/*.js'), function(event) { + if(isOnlyChange(event)) { + gulp.start('scripts'); + } else { + gulp.start('inject'); + } + }); + + gulp.watch(path.join(conf.paths.src, '/app/**/*.html'), function(event) { + browserSync.reload(event.path); + }); +}); diff --git a/front/gulpfile.js b/front/gulpfile.js new file mode 100755 index 0000000..5669b5d --- /dev/null +++ b/front/gulpfile.js @@ -0,0 +1,29 @@ +/** + * Welcome to your gulpfile! + * The gulp tasks are splitted in several files in the gulp directory + * because putting all here was really too long + */ + +'use strict'; + +var gulp = require('gulp'); +var wrench = require('wrench'); + +/** + * This will load all js or coffee files in the gulp directory + * in order to load all gulp tasks + */ +wrench.readdirSyncRecursive('./gulp').filter(function(file) { + return (/\.(js|coffee)$/i).test(file); +}).map(function(file) { + require('./gulp/' + file); +}); + + +/** + * Default task clean temporaries directories and launch the + * main optimization build task + */ +gulp.task('default', ['clean'], function () { + gulp.start('build'); +}); diff --git a/front/karma.conf.js b/front/karma.conf.js new file mode 100755 index 0000000..cad0589 --- /dev/null +++ b/front/karma.conf.js @@ -0,0 +1,74 @@ +'use strict'; + +var path = require('path'); +var conf = require('./gulp/conf'); + +var _ = require('lodash'); +var wiredep = require('wiredep'); + +function listFiles() { + var wiredepOptions = _.extend({}, conf.wiredep, { + dependencies: true, + devDependencies: true + }); + + return wiredep(wiredepOptions).js + .concat([ + path.join(conf.paths.src, '/app/**/*.module.js'), + path.join(conf.paths.src, '/app/**/*.js'), + path.join(conf.paths.src, '/**/*.spec.js'), + path.join(conf.paths.src, '/**/*.mock.js'), + path.join(conf.paths.src, '/**/*.html') + ]); +} + +module.exports = function(config) { + + var configuration = { + files: listFiles(), + + singleRun: true, + + autoWatch: false, + + frameworks: ['jasmine', 'angular-filesort'], + + angularFilesort: { + whitelist: [path.join(conf.paths.src, '/**/!(*.html|*.spec|*.mock).js')] + }, + + ngHtml2JsPreprocessor: { + stripPrefix: 'src/', + moduleName: 'loanWizard' + }, + + browsers : ['PhantomJS'], + + plugins : [ + 'karma-phantomjs-launcher', + 'karma-angular-filesort', + 'karma-jasmine', + 'karma-ng-html2js-preprocessor' + ], + + preprocessors: { + 'src/**/*.html': ['ng-html2js'] + } + }; + + // This block is needed to execute Chrome on Travis + // If you ever plan to use Chrome and Travis, you can keep it + // If not, you can safely remove it + // https://github.com/karma-runner/karma/issues/1144#issuecomment-53633076 + if(configuration.browsers[0] === 'Chrome' && process.env.TRAVIS) { + configuration.customLaunchers = { + 'chrome-travis-ci': { + base: 'Chrome', + flags: ['--no-sandbox'] + } + }; + configuration.browsers = ['chrome-travis-ci']; + } + + config.set(configuration); +}; diff --git a/front/package.json b/front/package.json new file mode 100755 index 0000000..cfcd4e6 --- /dev/null +++ b/front/package.json @@ -0,0 +1,55 @@ +{ + "name": "loanWizard", + "version": "0.0.0", + "dependencies": {}, + "scripts": { + "test": "gulp test" + }, + "devDependencies": { + "gulp": "~3.9.0", + "gulp-autoprefixer": "~2.3.1", + "gulp-angular-templatecache": "~1.6.0", + "del": "~1.2.0", + "lodash": "~3.9.3", + "gulp-csso": "~1.0.0", + "gulp-filter": "~2.0.2", + "gulp-flatten": "~0.0.4", + "gulp-jshint": "~1.11.0", + "gulp-load-plugins": "~0.10.0", + "gulp-size": "~1.2.1", + "gulp-uglify": "~1.2.0", + "gulp-useref": "~1.2.0", + "gulp-util": "~3.0.5", + "gulp-ng-annotate": "~1.0.0", + "gulp-replace": "~0.5.3", + "gulp-rename": "~1.2.2", + "gulp-rev": "~5.0.0", + "gulp-rev-replace": "~0.4.2", + "gulp-minify-html": "~1.0.3", + "gulp-inject": "~1.3.1", + "gulp-protractor": "~1.0.0", + "gulp-sourcemaps": "~1.5.2", + "gulp-less": "~3.0.3", + "gulp-angular-filesort": "~1.1.1", + "main-bower-files": "~2.8.0", + "merge-stream": "~0.1.7", + "jshint-stylish": "~2.0.0", + "wiredep": "~2.2.2", + "karma": "~0.12.36", + "karma-jasmine": "~0.3.5", + "karma-phantomjs-launcher": "~0.2.0", + "karma-angular-filesort": "~0.1.0", + "karma-ng-html2js-preprocessor": "~0.1.2", + "concat-stream": "~1.5.0", + "require-dir": "~0.3.0", + "browser-sync": "~2.7.12", + "browser-sync-spa": "~1.0.2", + "http-proxy-middleware": "~0.0.5", + "chalk": "~1.0.0", + "uglify-save-license": "~0.4.1", + "wrench": "~1.5.8" + }, + "engines": { + "node": ">=0.10.0" + } +} diff --git a/front/protractor.conf.js b/front/protractor.conf.js new file mode 100755 index 0000000..f2db101 --- /dev/null +++ b/front/protractor.conf.js @@ -0,0 +1,27 @@ +'use strict'; + +var paths = require('./.yo-rc.json')['generator-gulp-angular'].props.paths; + +// An example configuration file. +exports.config = { + // The address of a running selenium server. + //seleniumAddress: 'http://localhost:4444/wd/hub', + //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json + + // Capabilities to be passed to the webdriver instance. + capabilities: { + 'browserName': 'chrome' + }, + + baseUrl: 'http://localhost:3000', + + // Spec patterns are relative to the current working directly when + // protractor is called. + specs: [paths.e2e + '/**/*.js'], + + // Options to be passed to Jasmine-node. + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000 + } +}; diff --git a/front/src/app/common.less b/front/src/app/common.less new file mode 100644 index 0000000..9f6253b --- /dev/null +++ b/front/src/app/common.less @@ -0,0 +1,26 @@ +.row-no-padding { + [class*="col-"] { + padding-left: 0 !important; + padding-right: 0 !important; + } +} + +.text-large { + font-size: @text-size-large; +} + +.btn-default { + color: white; + + background-image: -webkit-linear-gradient(top,@color-blue 0, darken(@color-blue, 10%), 100%); + background-image: -o-linear-gradient(top,@color-blue 0, darken(@color-blue, 10%) 100%); + background-image: -webkit-gradient(linear,left top,left bottom,from(@color-blue),to(darken(@color-blue, 10%))); + background-image: linear-gradient(to bottom,@color-blue 0, darken(@color-blue, 10%) 100%); + + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + + background-repeat: repeat-x; + border-color: darken(@color-blue, 5%); + +} diff --git a/front/src/app/components/datepicker/datepicker.directive.js b/front/src/app/components/datepicker/datepicker.directive.js new file mode 100755 index 0000000..9649be5 --- /dev/null +++ b/front/src/app/components/datepicker/datepicker.directive.js @@ -0,0 +1,35 @@ +(function() { + 'use strict'; + + angular.module("loanWizard") + .directive('datepicker', datepickerDirective); + + /*** @ngInject */ + function datepickerDirective() { + + var directive = { + restrict: 'A', + require: '?ngModel', + scope: { + select: "&" + }, + link: link + }; + + return directive; + + /*** @ngInject */ + function link(scope, element, attrs, ngModelCtrl) { + + var today = new Date(); + element.datetimepicker({ format: 'DD/MM/YYYY' }); + + element.on("dp.change", function(e) { + scope.$apply(function(){ + ngModelCtrl.$setViewValue(e.date.format("DD/MM/YYYY")); + }); + }); + + } + } +})(); diff --git a/front/src/app/components/loan/loan.directive.js b/front/src/app/components/loan/loan.directive.js new file mode 100755 index 0000000..b83aaaf --- /dev/null +++ b/front/src/app/components/loan/loan.directive.js @@ -0,0 +1,61 @@ + +(function() { + 'use strict'; + + angular + .module('loanWizard') + .directive('loanPlan', loanDirective); + + /*** @ngInject */ + function loanDirective(dataservice, moment) { + + var directive = { + restrict: 'EA', + templateUrl: 'app/components/loan/loan.htm', + controller: loanController, + controllerAs: 'ctrl', + scope: { + loan: '=', + id: '=' + }, + bindToController: true + }; + + /*** @ngInject */ + function loanController() { + + var ctrl = this; + ctrl.showForm = true; + ctrl.loading = false; + + ctrl.startingAt = moment().format('DD/MM/YYYY'); + // (54537.35, payments, 180, 3.6, RatePeriod.ANNUAL, 31.82 + + ctrl.updateStartingAt = function (value) { + ctrl.startingAt = value.date; + }; + + /*-------------------------*/ + ctrl.getLoan = function() { + ctrl.loading = true; + + var callback = function(data) { + if (data.error === undefined) { + ctrl.loan = data; + ctrl.showForm = false; + ctrl.loading = false; + } else { + ctrl.showForm = true; + ctrl.loading = false; + } + }; + + dataservice.getLoan(ctrl.loan, callback); + }; + + } + + return directive; + } + +})(); diff --git a/front/src/app/components/loan/loan.htm b/front/src/app/components/loan/loan.htm new file mode 100755 index 0000000..fb5da62 --- /dev/null +++ b/front/src/app/components/loan/loan.htm @@ -0,0 +1,179 @@ +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Début{{ ctrl.startingAt }}
Emprunt + {{ ctrl.loan.borrowed }} € +
Période + {{ ctrl.loan.period }} mois +
+ {{ ctrl.loan.period / 12 | number: 1}} % années +
+
Taux + {{ ctrl.loan.rate }} % +
+ {{ ctrl.loan.rate * 12 | number: 2}} % annuel +
+
Assurance + {{ ctrl.loan.insurance }} € +
Décalé + {{ ctrl.loan.shift }} mois +
+ +
+ +
+
+
+ + + + + + + + + + + +
Payement +
+
+ {{ ctrl.loan.payment + ctrl.loan.insurance | number : 2 }} € +
+
+
+ [{{ ctrl.loan.payment }} + {{ ctrl.loan.insurance }}] +
+
+
+
Coût total + {{ ctrl.loan.totalCost | number : 2 }} € +
+ {{ ctrl.loan.totalCost - ctrl.loan.borrowed | number : 2 }} € / + + {{ ((ctrl.loan.totalCost / ctrl.loan.borrowed) -1) * 100 | number : 2 }} % + +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/front/src/app/components/loan/loan.less b/front/src/app/components/loan/loan.less new file mode 100755 index 0000000..3714ba7 --- /dev/null +++ b/front/src/app/components/loan/loan.less @@ -0,0 +1,124 @@ +.loanPlan { + + .form-value { + font-size: 1.5em; + } + + .loan__form { + + width: 320px; + padding: 10px; + border-radius: 5px; + border: 1px solid lighten(@color-default, 20%); + box-shadow: 1px 1px 1px #fff; + margin: 0 auto; + + div.form-group { + display: inline-block; + } + + [datepicker], input[ng-model="ctrl.loan.rate"] { + width: 100px; + } + + input[ng-model="ctrl.loan.borrowed"], input[ng-model="ctrl.loan.insurance"] { + width: 110px; + } + + input[ng-model="ctrl.loan.shift"], input[ng-model="ctrl.loan.period"] { + width: 70px; + } + + label { + font-weight: normal; + font-size: 0.9em; + } + } + + .loan-form { + padding: 2px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.7); + box-shadow: 0px 0px 10px; + + button { + margin-top: 25px; + } + } + + .monthRate { + line-height: 1em; + margin-top: 12px; + } + + .info { + font-size: 0.8em; + text-transform: uppercase; + } + + .loan__payments { + margin-top: 20px; + } + + .loan__data { + + margin-bottom: 20px; + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + padding: 20px 0; + + &::after { + content: ''; + display: block; + clear: both; + } + + & > div[class*="col-"] { + + & > table { + width: 100%; + & tr { + td { + width: 50%; + border-bottom: 1px solid rgb(221, 221, 221); + & + td { + text-align: right; + } + } + } + } + } + + & button.edit { + margin-top: -10px; + margin-right: 10px; + margin-bottom: 10px; + } + } +} + +.subloan__widget { + margin-top: 10px; +} + +.ball-clip-rotate-multiple { + & > div { + border: 2px solid @color-blue; + + &:last-child { + border-color: @color-blue transparent @color-blue transparent; + } + } +} + +.loan__form-submit { + label { + display: block; + } +} + +button.btn + button.btn { + margin-left: 10px; +} \ No newline at end of file diff --git a/front/src/app/components/loanChart/loanchart.directive.js b/front/src/app/components/loanChart/loanchart.directive.js new file mode 100755 index 0000000..c19265a --- /dev/null +++ b/front/src/app/components/loanChart/loanchart.directive.js @@ -0,0 +1,140 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .directive('ebizLoanCharts', loanCharts); + + /*** @ngInject */ + function loanCharts() { + + var directive = { + template: '', + link: link, + restrict: 'EA', + scope: { + height : "=", + width : "=", + payments: "=" + }, + controller: LoanChartsController, + controllerAs: 'ctrl', + bindToController: true // because the scope is isolated + }; + return directive; + + //LoanChartsController.$inject = ['$scope']; + + /*** @ngInject */ + function link(scope, element, attributes) { + var canvas = element.find('canvas'); + } + + /*** @ngInject */ + function LoanChartsController($scope, $element) { + + var ctrl = this; + var ctx = $element.find('canvas').get(0).getContext("2d"); + ctrl.chart; + + ctx.canvas.width = ctrl.width; + ctx.canvas.height = ctrl.height; + + $scope.$watch('ctrl.payments', function () { + createChart(ctrl.payments); + }); + + function createChart (payments) { + + // Get context with jQuery - using jQuery's .get() method. + + var fillColor1 = "rgba(229, 104, 0, 0.2)"; + var strokeColor1 = "rgba(229, 104, 0, 1)"; + + var fillColor2 = "rgba(151,187,205,0.2)"; + var strokeColor2 = "rgba(151,187,205,1)"; + + var fillColor3 = "rgba(114, 63, 189, 0.2)"; + var strokeColor3 = "rgba(114, 63, 189, 1)"; + + var fillColor4 = "rgba(84, 98, 153, 0.2)"; + var strokeColor4 = "rgba(84, 98, 153, 1)"; + + + var labels = []; + var basePayments = []; + var fullPayments = []; + var interest = []; + var capital = []; + + _.each(ctrl.payments, function(p){ + labels.push(p.period); + basePayments.push(p.basePayment); + fullPayments.push(p.fullPayment); + interest.push(p.interest); + capital.push(p.capital); + }); + + var fullPayementDataset = { + label: "Payements", + fillColor: fillColor1, + strokeColor: strokeColor1, + pointColor: strokeColor1, + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: strokeColor1, + data: fullPayments + }; + + var basePayementDataset = { + label: "Base", + fillColor: fillColor2, + strokeColor: strokeColor2, + pointColor: strokeColor2, + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: strokeColor2, + data: basePayments + }; + + var capitalDataset = { + label: "Intêret", + fillColor: fillColor3, + strokeColor: strokeColor3, + pointColor: strokeColor3, + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: strokeColor3, + data: capital + }; + + var interestDataset = { + label: "Intêret", + fillColor: fillColor4, + strokeColor: strokeColor4, + pointColor: strokeColor4, + pointStrokeColor: "#fff", + pointHighlightFill: "#fff", + pointHighlightStroke: strokeColor4, + data: interest + }; + + + var data = { + labels: labels, + datasets: [ + fullPayementDataset, + basePayementDataset, + capitalDataset, + interestDataset + ] + }; + + ctrl.chart = new Chart(ctx).Line(data, {pointDot : false}); + } + } + + } + +})(); + diff --git a/front/src/app/components/loanChart/loanchart.less b/front/src/app/components/loanChart/loanchart.less new file mode 100644 index 0000000..66c2afa --- /dev/null +++ b/front/src/app/components/loanChart/loanchart.less @@ -0,0 +1,4 @@ +.loanChart { + + +} diff --git a/front/src/app/components/loanProject/loanProject.directive.js b/front/src/app/components/loanProject/loanProject.directive.js new file mode 100755 index 0000000..511dd73 --- /dev/null +++ b/front/src/app/components/loanProject/loanProject.directive.js @@ -0,0 +1,54 @@ +(function() { + 'use strict'; + + angular.module('loanWizard').directive('loanProject', loanProjectDirective); + + /** @ngInject */ + function loanProjectDirective() { + + var directive = { + restrict: 'EA', + templateUrl: 'app/components/loanProject/loanProject.htm', + controller: loanProjectController, + controllerAs: 'ctrl', + bindToController: true + }; + + return directive; + + /** @ngInject */ + function loanProjectController($scope, $compile) { + + var ctrl = this; + + ctrl.loans = []; + + ctrl.addLoanPlan = function () { + var loan = { + "rate" : 2.15, + "borrowed": 10000, + "totalCost": "", + "insurance": 31.82, + "period": 180, + "payment": "", + "payments": [] + }; + + ctrl.loans.push(loan); + ctrl.addDirective(ctrl.loans.length-1); + }; + + ctrl.addDirective = function(index) { + angular.element( + document.getElementById('loanProject') + ).append( + $compile('')($scope) + ); + }; + + ctrl.addLoanPlan(); + } + } + + +})(); diff --git a/front/src/app/components/loanProject/loanProject.htm b/front/src/app/components/loanProject/loanProject.htm new file mode 100755 index 0000000..38f2823 --- /dev/null +++ b/front/src/app/components/loanProject/loanProject.htm @@ -0,0 +1,8 @@ +
+

+ Dossier de prêt + +

+
diff --git a/front/src/app/components/payments/payments.directive.js b/front/src/app/components/payments/payments.directive.js new file mode 100755 index 0000000..0519177 --- /dev/null +++ b/front/src/app/components/payments/payments.directive.js @@ -0,0 +1,37 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .directive('ebizPayments', paymentsDirective); + + /*** @ngInject */ + function paymentsDirective(moment) { + + var directive = { + restrict: 'EA', + templateUrl: 'app/components/payments/payments.htm', + controller: paymentsController, + controllerAs: 'ctrl', + bindToController: true, + scope: { + payments: "=", + startingAt: "=" + } + }; + + return directive; + + /*** @ngInject */ + function paymentsController() { + var ctrl = this; + + ctrl.dateAt = function (period) { + var start = moment(ctrl.startingAt, "DD/MM/YYYY"); + return start.add(period-1, "M").format("MMM YYYY"); + }; + } + + } + +})(); diff --git a/front/src/app/components/payments/payments.htm b/front/src/app/components/payments/payments.htm new file mode 100755 index 0000000..84e8eda --- /dev/null +++ b/front/src/app/components/payments/payments.htm @@ -0,0 +1,29 @@ +
+
+
[{{payment.period}}] {{ ctrl.dateAt(payment.period) }}
+
+
Emprunt
+
{{ payment.borrowed | number : 2}}
+
+
+
Payement
+
{{ payment.basePayment | number : 2}}
+
+
+
Capital
+
{{ payment.capital | number : 2 }}
+
+
+
Intérêt
+
{{ payment.interest | number : 2}}
+
+
+
Assurance
+
{{ payment.insurance | number : 2}}
+
+
+
Total
+
{{ payment.fullPayment | number : 2}}
+
+
+
diff --git a/front/src/app/components/payments/payments.less b/front/src/app/components/payments/payments.less new file mode 100755 index 0000000..88b8502 --- /dev/null +++ b/front/src/app/components/payments/payments.less @@ -0,0 +1,52 @@ +@import "../../variables"; + +.payments { + + overflow: scroll; + overflow-y: hidden; + -ms-overflow-y: hidden; + + white-space: nowrap; + height: 140px; + + .payments__line-above::before { + border-top: 1px solid #c5c5c5; + position: relative; + top: 0; + left: 10px; + width: 90%; + } + + .payments__period { + + text-align: center; + + } + .payments__onePayment { + font-size: 0.8em; + position: relative; + display: inline-block; + border: 1px solid @border-light-grey; + padding: 5px 10px; + width: 130px; + margin-left: 5px; + + } + +} + +.color-total { + color: @color-total; +} + +.color-capital { + color: @color-capital; +} + +.color-interest { + color: @color-interest; +} + +.color-payment { + color: @color-payment; +} diff --git a/front/src/app/components/subLoan/subloan.directive.js b/front/src/app/components/subLoan/subloan.directive.js new file mode 100644 index 0000000..e2a4c2e --- /dev/null +++ b/front/src/app/components/subLoan/subloan.directive.js @@ -0,0 +1,34 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .directive('subLoan', subLoanDirective); + + /*** @ngInject */ + function subLoanDirective() { + + var directive = { + restrict: 'EA', + templateUrl: 'app/components/subLoan/subloan.htm', + controller: subLoanController, + controllerAs: 'ctrl', + scope: { + subloan: '=data', + }, + bindToController: true + }; + + /*** @ngInject */ + function subLoanController() { + + var ctrl = this; + + ctrl.validated = false; + + } + + return directive; + } + +})(); \ No newline at end of file diff --git a/front/src/app/components/subLoan/subloan.htm b/front/src/app/components/subLoan/subloan.htm new file mode 100644 index 0000000..53d0a41 --- /dev/null +++ b/front/src/app/components/subLoan/subloan.htm @@ -0,0 +1,37 @@ +
+
+
+ + +
+
+
+ + +
{{ ctrl.subloan.rate }} %
+
+
+ + +
{{ ctrl.subloan.period }} m.
+
+
+ + +
+
+
+ + +
{{ctrl.subloan.insurance}} €
+
+
+ +
+
+
\ No newline at end of file diff --git a/front/src/app/components/subLoan/subloan.less b/front/src/app/components/subLoan/subloan.less new file mode 100644 index 0000000..69759f0 --- /dev/null +++ b/front/src/app/components/subLoan/subloan.less @@ -0,0 +1,45 @@ +.subloan__form { + & > div { + display: inline-block; + text-align: center; + } + + .form-control { + padding: 6px; + background-color: rgb(247, 247, 247); + border: none; + border-bottom: 1px solid rgb(204, 204, 204); + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + + label { + font-weight: normal; + text-transform: capitalize; + font-size: 0.8em; + width: 100%; + } +} + +.subloan__directive { + width: 100%; +} + +.subloan__form-amount { + width: 30%; +} + +.subloan__form-rate { + width: 16%; +} + +.subloan__form-start, +.subloan__form-period { + width: 12%; +} + +.subloan__form-insurance{ + width: 16%; +} \ No newline at end of file diff --git a/front/src/app/components/twelveCalculator/twelveCalculator.directive.js b/front/src/app/components/twelveCalculator/twelveCalculator.directive.js new file mode 100755 index 0000000..664465d --- /dev/null +++ b/front/src/app/components/twelveCalculator/twelveCalculator.directive.js @@ -0,0 +1,49 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .directive('ebizTwelveCalculator', twelveCalculator); + + function twelveCalculator() { + + var directive = { + restrict: 'EA', + templateUrl: 'app/components/twelveCalculator/twelveCalculator.htm', + scope: { + label : "@", + type : "@", + }, + controller: twelveCalculatorController, + controllerAs: 'ctrl', + bindToController: true // because the scope is isolated + }; + + return directive; + + //TwelveDividerController.$inject = ['$scope']; + + /*** @ngInject */ + function twelveCalculatorController() { + + var ctrl = this; + + ctrl.yearly = 1; + ctrl.monthly = ctrl.type === "divide" ? Math.round((ctrl.yearly / 12) * 10000) / 10000 : ctrl.yearly * 12; + + ctrl.updateMonthtly = function () { + if (ctrl.type === "divide") { + ctrl.monthly = Math.round((ctrl.yearly / 12) * 10000) / 10000; + } else { + ctrl.monthly = ctrl.yearly * 12; + } + }; + + } + + } + +})(); + + + diff --git a/front/src/app/components/twelveCalculator/twelveCalculator.htm b/front/src/app/components/twelveCalculator/twelveCalculator.htm new file mode 100755 index 0000000..b51c279 --- /dev/null +++ b/front/src/app/components/twelveCalculator/twelveCalculator.htm @@ -0,0 +1,32 @@ +
+
+

{{ ctrl.label }}

+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
diff --git a/front/src/app/components/twelveCalculator/twelveCalculator.less b/front/src/app/components/twelveCalculator/twelveCalculator.less new file mode 100644 index 0000000..6c4360f --- /dev/null +++ b/front/src/app/components/twelveCalculator/twelveCalculator.less @@ -0,0 +1,57 @@ +#twelve__directive { + + margin-bottom: 20px; + + border: 1px solid lighten(@color-default, 20%); + box-shadow: 1px 1px 1px #fff; + + h3 { + margin: 0 0 15px 0; + + font-family: 'Roboto', sans-serif; + font-size: 1em; + text-transform: uppercase; + background-color: lighten(@color-default, 20%); + color: white; + padding: 5px; + } + + label { + color: @color-title; + } + + .twelve__input { + width: 70%; + margin: 0 auto; + } + + .twelve__result, input { + display: block; + width: 100%; + height: 34px; + text-align: center; + padding: 6px 12px; + font-size: 18px; + line-height: 1.42857143; + color: #555; + margin-bottom: 20px; + } + + input { + + font-size: 16px; + color: #555; + background: none; + border: 0; + border-bottom: 1px solid #CCC; + + //box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + } + + i.font { + margin-top: 20%; + color: @color-default; + } +} diff --git a/front/src/app/data/data.json b/front/src/app/data/data.json new file mode 100755 index 0000000..472707f --- /dev/null +++ b/front/src/app/data/data.json @@ -0,0 +1,442 @@ +{ + "rate":0.2042, + "borrowed":10000.00, + "totalCost":10508.16, + "insurance":0.0000, + "period":48, + "payment":218.92, + "payments":[ + { + "borrowed":9801.50, + "capital":198.50, + "interest":20.42, + "period":1, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":9602.59, + "capital":198.91, + "interest":20.01, + "period":2, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":9403.28, + "capital":199.31, + "interest":19.61, + "period":3, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":9203.56, + "capital":199.72, + "interest":19.20, + "period":4, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":9003.43, + "capital":200.13, + "interest":18.79, + "period":5, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":8802.90, + "capital":200.53, + "interest":18.39, + "period":6, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":8601.96, + "capital":200.94, + "interest":17.98, + "period":7, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":8400.61, + "capital":201.35, + "interest":17.57, + "period":8, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":8198.84, + "capital":201.77, + "interest":17.15, + "period":9, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":7996.66, + "capital":202.18, + "interest":16.74, + "period":10, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":7794.07, + "capital":202.59, + "interest":16.33, + "period":11, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":7591.07, + "capital":203.00, + "interest":15.92, + "period":12, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":7387.65, + "capital":203.42, + "interest":15.50, + "period":13, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":7183.82, + "capital":203.83, + "interest":15.09, + "period":14, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":6979.57, + "capital":204.25, + "interest":14.67, + "period":15, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":6774.90, + "capital":204.67, + "interest":14.25, + "period":16, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":6569.81, + "capital":205.09, + "interest":13.83, + "period":17, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":6364.31, + "capital":205.50, + "interest":13.42, + "period":18, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":6158.39, + "capital":205.92, + "interest":13.00, + "period":19, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":5952.05, + "capital":206.34, + "interest":12.58, + "period":20, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":5745.28, + "capital":206.77, + "interest":12.15, + "period":21, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":5538.09, + "capital":207.19, + "interest":11.73, + "period":22, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":5330.48, + "capital":207.61, + "interest":11.31, + "period":23, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":5122.44, + "capital":208.04, + "interest":10.88, + "period":24, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":4913.98, + "capital":208.46, + "interest":10.46, + "period":25, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":4705.09, + "capital":208.89, + "interest":10.03, + "period":26, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":4495.78, + "capital":209.31, + "interest":9.61, + "period":27, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":4286.04, + "capital":209.74, + "interest":9.18, + "period":28, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":4075.87, + "capital":210.17, + "interest":8.75, + "period":29, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":3865.27, + "capital":210.60, + "interest":8.32, + "period":30, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":3654.24, + "capital":211.03, + "interest":7.89, + "period":31, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":3442.78, + "capital":211.46, + "interest":7.46, + "period":32, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":3230.89, + "capital":211.89, + "interest":7.03, + "period":33, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":3018.57, + "capital":212.32, + "interest":6.60, + "period":34, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":2805.81, + "capital":212.76, + "interest":6.16, + "period":35, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":2592.62, + "capital":213.19, + "interest":5.73, + "period":36, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":2378.99, + "capital":213.63, + "interest":5.29, + "period":37, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":2164.93, + "capital":214.06, + "interest":4.86, + "period":38, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":1950.43, + "capital":214.50, + "interest":4.42, + "period":39, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":1735.49, + "capital":214.94, + "interest":3.98, + "period":40, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":1520.11, + "capital":215.38, + "interest":3.54, + "period":41, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":1304.29, + "capital":215.82, + "interest":3.10, + "period":42, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":1088.03, + "capital":216.26, + "interest":2.66, + "period":43, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":871.33, + "capital":216.70, + "interest":2.22, + "period":44, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":654.19, + "capital":217.14, + "interest":1.78, + "period":45, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":436.61, + "capital":217.58, + "interest":1.34, + "period":46, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":218.58, + "capital":218.03, + "interest":0.89, + "period":47, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + }, + { + "borrowed":0.11, + "capital":218.47, + "interest":0.45, + "period":48, + "insurance":0.0000, + "fullPayment":218.92, + "basePayment":218.92 + } + ] +} diff --git a/front/src/app/home.less b/front/src/app/home.less new file mode 100755 index 0000000..3464ed8 --- /dev/null +++ b/front/src/app/home.less @@ -0,0 +1,34 @@ + +.title { + font-family: 'Dancing Script', cursive; + font-weight: bold; + color: @color-title; + text-shadow: 1px 1px @color-shadow-title; +} + +.text-blue { + color: @color-blue; +} + +body{ + margin: 0; + padding: 0; + color: @color-default; + background:#efefef url('../assets/images/gray_jean.png'); + text-shadow: 0 0 1px transparent; /* google font pixelation fix */ +} + +nav { + margin: 0 !important; + background-color: @bckg-nav; + min-height: 100% !important; + position: absolute !important; +} + +#main__tools { + + .title { + border-bottom: 1px solid @border-light-grey; + } + +} diff --git a/front/src/app/index.config.js b/front/src/app/index.config.js new file mode 100755 index 0000000..6f1f87c --- /dev/null +++ b/front/src/app/index.config.js @@ -0,0 +1,23 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .config(config); + + /** @ngInject */ + function config($logProvider, toastr, $httpProvider) { + // Enable log + $logProvider.debugEnabled(true); + + // Set options third-party lib + toastr.options.timeOut = 3000; + toastr.options.positionClass = 'toast-top-right'; + toastr.options.preventDuplicates = true; + toastr.options.progressBar = true; + + $httpProvider.defaults.useXDomain = true; + delete $httpProvider.defaults.headers.common['X-Requested-With']; + } +'' +})(); diff --git a/front/src/app/index.constants.js b/front/src/app/index.constants.js new file mode 100755 index 0000000..1346927 --- /dev/null +++ b/front/src/app/index.constants.js @@ -0,0 +1,10 @@ +/* global toastr:false, moment:false */ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .constant('toastr', toastr) + .constant('moment', moment); + +})(); diff --git a/front/src/app/index.less b/front/src/app/index.less new file mode 100755 index 0000000..15791d1 --- /dev/null +++ b/front/src/app/index.less @@ -0,0 +1,25 @@ +/** + * Do not remove this comments bellow. It's the markers used by wiredep to inject + * less dependencies when defined in the bower.json of your dependencies + */ +// bower:less +// endbower + +/** + * If you want to override some bootstrap variables, you have to change values here. + * The list of variables are listed here bower_components/bootstrap/less/variables.less + */ +@icon-font-path: '../../bower_components/bootstrap/fonts/'; +@import url(http://fonts.googleapis.com/css?family=Dancing+Script); +@import url(http://fonts.googleapis.com/css?family=Roboto:400,100); + +nav { + padding-top: 50px; +} + +/** + * Do not remove this comments bellow. It's the markers used by gulp-inject to inject + * all your less files automatically + */ +// injector +// endinjector diff --git a/front/src/app/index.module.js b/front/src/app/index.module.js new file mode 100755 index 0000000..77b8567 --- /dev/null +++ b/front/src/app/index.module.js @@ -0,0 +1,15 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard', + [ + 'ngAnimate', + 'ngCookies', + 'ngTouch', + 'ngSanitize', + 'ngResource', + 'ngRoute' + ]); + +})(); diff --git a/front/src/app/index.route.js b/front/src/app/index.route.js new file mode 100755 index 0000000..cab8395 --- /dev/null +++ b/front/src/app/index.route.js @@ -0,0 +1,20 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .config(routeConfig); + + function routeConfig($routeProvider) { + $routeProvider + .when('/', { + templateUrl: 'app/main/main.html', + controller: 'MainController', + controllerAs: 'ctrl' + }) + .otherwise({ + redirectTo: '/' + }); + } + +})(); diff --git a/front/src/app/index.run.js b/front/src/app/index.run.js new file mode 100755 index 0000000..905eb88 --- /dev/null +++ b/front/src/app/index.run.js @@ -0,0 +1,14 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .run(runBlock); + + /** @ngInject */ + function runBlock($log) { + + $log.debug('runBlock end'); + } + +})(); diff --git a/front/src/app/main/main.controller.js b/front/src/app/main/main.controller.js new file mode 100755 index 0000000..8a0b610 --- /dev/null +++ b/front/src/app/main/main.controller.js @@ -0,0 +1,22 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .controller('MainController', MainController); + + /** @ngInject */ + function MainController($timeout, toastr) { + var vm = this; + + vm.awesomeThings = []; + vm.classAnimation = ''; + vm.showToastr = showToastr; + + function showToastr() { + toastr.info('Fork generator-gulp-angular'); + vm.classAnimation = ''; + } + + } +})(); diff --git a/front/src/app/main/main.controller.spec.js b/front/src/app/main/main.controller.spec.js new file mode 100755 index 0000000..1c1d669 --- /dev/null +++ b/front/src/app/main/main.controller.spec.js @@ -0,0 +1,15 @@ +(function() { + 'use strict'; + + describe('controllers', function(){ + + beforeEach(module('loanWizard')); + + it('should define more than 5 awesome things', inject(function($controller) { + var vm = $controller('MainController'); + + expect(angular.isArray(vm.awesomeThings)).toBeTruthy(); + expect(vm.awesomeThings.length > 5).toBeTruthy(); + })); + }); +})(); diff --git a/front/src/app/main/main.html b/front/src/app/main/main.html new file mode 100755 index 0000000..e6740fa --- /dev/null +++ b/front/src/app/main/main.html @@ -0,0 +1,17 @@ +
+
+

Thibault de Lambilly

+
+
+ +
+
+
+

Outils

+
+ + + + +
+
diff --git a/front/src/app/services/data.service.js b/front/src/app/services/data.service.js new file mode 100755 index 0000000..dcf8b85 --- /dev/null +++ b/front/src/app/services/data.service.js @@ -0,0 +1,51 @@ +(function() { + 'use strict'; + + angular + .module('loanWizard') + .factory('dataservice', dataservice); + + /*** @ngInject */ + function dataservice($http) { + + return { + getLoan: getLoan + }; + + function getLoan(loan, callback) { + //* + var url = 'http://localhost:5001/loan/get' + + "/" + loan.borrowed + + "/" + loan.period + + "/" + loan.rate + + "/" + loan.insurance; + + return $http.get(url) + .success(getData) + .error(getDataFailed); + //*/ + /* + // {capital}/{period}/{rate}/{insurance} + return $http.post('/loan/get', loan) + .then(getData) + .catch(getDataFailed); + //*/ + + /* + return $http.get('app/data/data.json') + .then(getData) + .catch(getDataFailed); + //*/ + + function getData(data) { + callback(data); + } + + function getDataFailed(data, status, headers, config) { + callback({error: "Exception"}); + } + } + } + + +})(); diff --git a/front/src/app/variables.less b/front/src/app/variables.less new file mode 100755 index 0000000..de28c12 --- /dev/null +++ b/front/src/app/variables.less @@ -0,0 +1,16 @@ +@color-default: #8E8E8E; +@color-title: #8E8E8E; +@color-shadow-title: #fff; +@color-blue: #51A0C3; + +@color-total: rgb(229, 104, 0); +@color-capital: rgb(114, 63, 189); +@color-interest: rgb(84, 98, 153); +@color-payment: rgb(151,187,205); + +@border-light-grey: #C5C5C5; + +@bckg-nav: #222; + +@text-size-default: 1em; +@text-size-large: 2.3em; diff --git a/front/src/assets/images/gray_jean.png b/front/src/assets/images/gray_jean.png new file mode 100755 index 0000000..355fba2 Binary files /dev/null and b/front/src/assets/images/gray_jean.png differ diff --git a/front/src/favicon.ico b/front/src/favicon.ico new file mode 100755 index 0000000..6527905 Binary files /dev/null and b/front/src/favicon.ico differ diff --git a/front/src/index.html b/front/src/index.html new file mode 100755 index 0000000..946bb50 --- /dev/null +++ b/front/src/index.html @@ -0,0 +1,60 @@ + + + + + loanWizard + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/webapp/.gitignore b/webapp/.gitignore new file mode 100755 index 0000000..a722fd5 --- /dev/null +++ b/webapp/.gitignore @@ -0,0 +1,3 @@ +target/ +*.iml +.idea diff --git a/webapp/pom.xml b/webapp/pom.xml new file mode 100755 index 0000000..199fcb6 --- /dev/null +++ b/webapp/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + biz.eventual + loanwizard + 0.0.1-SNAPSHOT + jar + + loanwizard + Easy way to play with loan figures + + + org.springframework.boot + spring-boot-starter-parent + 1.2.4.RELEASE + + + + + UTF-8 + 1.8 + + + + + + org.springframework.boot + spring-boot-starter-data-rest + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + + mysql + mysql-connector-java + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + 1.16.4 + + + + com.google.code.gson + gson + 2.3.1 + + + + org.mockito + mockito-all + 1.10.19 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-war-plugin + + false + + + + + + diff --git a/webapp/src/main/java/biz/eventual/LoanwizardApplication.java b/webapp/src/main/java/biz/eventual/LoanwizardApplication.java new file mode 100755 index 0000000..1923267 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/LoanwizardApplication.java @@ -0,0 +1,12 @@ +package biz.eventual; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LoanwizardApplication { + + public static void main(String[] args) { + SpringApplication.run(LoanwizardApplication.class, args); + } +} diff --git a/webapp/src/main/java/biz/eventual/bean/LoanPlan.java b/webapp/src/main/java/biz/eventual/bean/LoanPlan.java new file mode 100755 index 0000000..2c78352 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/bean/LoanPlan.java @@ -0,0 +1,106 @@ +package biz.eventual.bean; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +/* +@Entity +@Table(name = "loanplan") +*/ +@Getter +@Setter +public class LoanPlan +{ + public final static int PER_YEAR = 12; + public final static int REFUND_NOW = 0; + +/* @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Getter + @Setter + int id; +*/ + /*** + * Said to be MONTHLY rate + */ + private BigDecimal rate; + + private BigDecimal borrowed; + + @Setter(AccessLevel.NONE) + private BigDecimal totalCost = BigDecimal.ZERO; + + private BigDecimal insurance = BigDecimal.ZERO; + + private BigDecimal fee = BigDecimal.ZERO; + + /*** + * Number of month + */ + private int period = -1; + + /*** + * When starting to refund + */ + private int shiftRefundBy = 0; + + private BigDecimal payment; + + private List payments; + + private List subLoan; + + public LoanPlan() {} + + public LoanPlan(BigDecimal borrowed, BigDecimal rate, int period) { + this.borrowed = borrowed; + this.rate = rate; + this.period = period; + } + + public List getPayments() { + if (payments == null) { + payments = new ArrayList(); + } + + return payments; + } + + /*** + * Calculate the totalt cost by add all Payments + * @return + */ + public BigDecimal getTotalCost() { + + if (totalCost.compareTo(BigDecimal.ZERO) == 0) { + for(Payment payment : payments) { + totalCost = totalCost.add(payment.getBasePayment()); + } + } + + return totalCost.setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public String toJson() + { + ObjectMapper mapper = new ObjectMapper(); + String json; + try { + json = mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + json = "{erreur : Impossible to parse object LoanPlan in JSON}"; + } + + return json; + } +} diff --git a/webapp/src/main/java/biz/eventual/bean/Payment.java b/webapp/src/main/java/biz/eventual/bean/Payment.java new file mode 100755 index 0000000..89552d3 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/bean/Payment.java @@ -0,0 +1,41 @@ +package biz.eventual.bean; + +import java.math.BigDecimal; + +import lombok.Getter; +import lombok.Setter; +import com.google.gson.Gson; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Getter +@Setter +public class Payment +{ + BigDecimal borrowed = BigDecimal.ZERO; + + BigDecimal capital = BigDecimal.ZERO; + + BigDecimal interest = BigDecimal.ZERO; + + int period = 0; + + BigDecimal insurance = BigDecimal.ZERO; + + public BigDecimal getBasePayment() { + return capital.add(interest).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public BigDecimal getFullPayment() { + return capital + .add(interest) + .add(insurance) + .setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public String toJson() { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/webapp/src/main/java/biz/eventual/bean/StepLoan.java b/webapp/src/main/java/biz/eventual/bean/StepLoan.java new file mode 100755 index 0000000..f5fa060 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/bean/StepLoan.java @@ -0,0 +1,41 @@ +package biz.eventual.bean; + +import java.math.BigDecimal; +import java.util.List; + +import lombok.Getter; +import lombok.Setter; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Created by tweety on 20/06/15. + */ +@Getter +@Setter +public class StepLoan { + + private BigDecimal payment; + private List payments; + + private LoanPlan loanPlan; + private List subloans; + + public StepLoan (LoanPlan loanPlan, List subLoans) { + this.loanPlan = loanPlan; + this.subloans = subLoans; + } + + public String toJson() + { + ObjectMapper mapper = new ObjectMapper(); + String json; + try { + json = mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + json = "{erreur : Impossible to parse object StepLoan in JSON}"; + } + + return json; + } +} diff --git a/webapp/src/main/java/biz/eventual/bean/SubLoan.java b/webapp/src/main/java/biz/eventual/bean/SubLoan.java new file mode 100755 index 0000000..b25eba1 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/bean/SubLoan.java @@ -0,0 +1,31 @@ +package biz.eventual.bean; + +import java.math.BigDecimal; + +import lombok.Getter; +import lombok.Setter; +import com.google.gson.Gson; + +import biz.eventual.bean.LoanPlan; + +/** + * Created by tweety on 19/06/15. + */ +@Getter +@Setter +public class SubLoan extends LoanPlan { + + int startingAt = 0; + + public SubLoan() {} + + public SubLoan(BigDecimal borrowed, BigDecimal rate, int period, int startingAt) { + super(borrowed, rate, period); + this.startingAt = startingAt; + } + + public String toJson() { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/webapp/src/main/java/biz/eventual/business/LoanBusiness.java b/webapp/src/main/java/biz/eventual/business/LoanBusiness.java new file mode 100755 index 0000000..61c318a --- /dev/null +++ b/webapp/src/main/java/biz/eventual/business/LoanBusiness.java @@ -0,0 +1,300 @@ +package biz.eventual.business; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import lombok.extern.slf4j.Slf4j; + +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Slf4j +public class LoanBusiness +{ + // http://www.math.hawaii.edu/~hile/math100/consf.htm + public static BigDecimal calculateMonthlyAmount(LoanPlan loanPlan) { + + BigDecimal borrowed = loanPlan.getBorrowed().setScale(2); + + BigDecimal zeroRate = loanPlan.getRate().divide(new BigDecimal(100.00)); + BigDecimal oneRate = zeroRate.add(new BigDecimal(1)); + + int period = loanPlan.getPeriod() - loanPlan.getShiftRefundBy(); // starting at 1 + BigDecimal powerRate = oneRate.pow(period); + + // (borrowed * zeroRate * powerRate) / (powerRate - 1); + BigDecimal top = (borrowed.multiply(zeroRate)).multiply(powerRate); + //top.setScale(2, BigDecimal.ROUND_HALF_UP); + + BigDecimal divideBy = powerRate.subtract(new BigDecimal(1)); + BigDecimal payment = top.divide(divideBy, 2, RoundingMode.HALF_UP); + + return payment.setScale(2, BigDecimal.ROUND_HALF_UP); + } + + public static List getPayments(LoanPlan loanPlan) { + + List payments = new ArrayList(); + + BigDecimal borrowed = loanPlan.getBorrowed(); + BigDecimal zeroRate = loanPlan.getRate().divide(new BigDecimal("100.0")); + + BigDecimal basePayment = LoanBusiness.calculateMonthlyAmount(loanPlan); + int period = loanPlan.getPeriod(); + + for (int i=0; i < period; i++) { + + BigDecimal interest = borrowed.multiply(zeroRate).setScale(2, BigDecimal.ROUND_HALF_UP); + + Payment payment = new Payment(); + payment.setInterest(interest); + + BigDecimal refunded = BigDecimal.ZERO; + + if (i > (loanPlan.getShiftRefundBy()-1)) { + refunded = basePayment.subtract(interest); + } + + payment.setCapital(refunded); + payments.add(payment); + + borrowed = borrowed.subtract(refunded); + } + + return payments; + } + + /*** + * Find how many payments for an amount loaned with a monthly payment according a rate + * @param borrowed + * @param basePayment + * @param rate + * @return + */ + public static List findPeriod(BigDecimal borrowed, BigDecimal basePayment, BigDecimal rate) + { + + List loans = new ArrayList(); + int period = 0; + + BigDecimal zeroRate = rate.divide(new BigDecimal(100.00)); + + BigDecimal left = borrowed; + + BigDecimal interest; + // interest.setScale(2, BigDecimal.ROUND_HALF_UP); + + BigDecimal refunded; + // refunded.setScale(2, BigDecimal.ROUND_HALF_UP); + + do { + + interest = left.multiply(zeroRate); + refunded = basePayment.subtract(interest); + left = left.subtract(refunded); + + period++; + + } while(left.intValue() > 0 && period < 1000); + + loans.add(new LoanPlan(borrowed, rate, (period-1))); + loans.add(new LoanPlan(borrowed, rate, period)); + + return loans; + } + + /*** + * Transform values to get a LoanPlan object + * @param borrowed + * @param period + * @param rate + * @param ratePeriod + * @return LoanPlan + */ + public static LoanPlan getLoanPlan(final BigDecimal borrowed, final int period, final BigDecimal rate, final RatePeriod ratePeriod, final BigDecimal insurance) { + + LoanPlan loan = new LoanPlan(); + loan.setBorrowed(borrowed); + loan.setPeriod(period); + loan.setInsurance(insurance); + + BigDecimal actualRate = rate; + + if (ratePeriod == RatePeriod.ANNUAL) { + actualRate = getMontlyRateFromYearly(actualRate); + } + + loan.setRate(actualRate); + + return loan; + } + + /*** + * Get LoanPlan without shiftRefundBy paramater + * @param borrowed + * @param period + * @param rate + * @param ratePeriod + * @param insurance + * @return + */ + public static LoanPlan getLoanPlan(final double borrowed, + final int period, + final double rate, + final RatePeriod ratePeriod, + final double insurance) { + + return getLoanPlan(borrowed, period,rate, ratePeriod, insurance, LoanPlan.REFUND_NOW); + } + + /*** + * Get LoanPlan including shiftRefundBy paramater + * @param borrowed + * @param period + * @param rate + * @param ratePeriod + * @param insurance + * @param refundStartingAt + * @return + */ + public static LoanPlan getLoanPlan(final double borrowed, + final int period, + final double rate, + final RatePeriod ratePeriod, + final double insurance, + final int refundStartingAt) { + + BigDecimal borrowedBg = new BigDecimal(borrowed).setScale(2, BigDecimal.ROUND_HALF_UP); + BigDecimal actualRate = new BigDecimal(rate).setScale(4, BigDecimal.ROUND_HALF_UP); + BigDecimal insuranceBg = new BigDecimal(insurance).setScale(4, BigDecimal.ROUND_HALF_UP); + + LoanPlan loan = new LoanPlan(); + loan.setBorrowed(borrowedBg); + loan.setPeriod(period); + loan.setInsurance(insuranceBg); + loan.setShiftRefundBy(refundStartingAt); + + if (ratePeriod == RatePeriod.ANNUAL) { + actualRate = getMontlyRateFromYearly(actualRate); + } + + loan.setRate(actualRate); + + return loan; + } + + /*** + * Centralized method to get the period rate according to the annual capital + * @param rate + * @return + */ + public static BigDecimal getMontlyRateFromYearly(BigDecimal rate) + { + return rate.divide(new BigDecimal(LoanPlan.PER_YEAR), 4, BigDecimal.ROUND_HALF_UP); + } + + public static LoanPlan findScheduleStepPayment(LoanPlan loanPlan, TreeMap payments) { + + BigDecimal insurance = loanPlan.getInsurance(); + BigDecimal borrowed = loanPlan.getBorrowed(); + BigDecimal zeroRate = loanPlan.getRate().divide(new BigDecimal("100.0"), 6, BigDecimal.ROUND_HALF_UP); + + BigDecimal basePayment = loanPlan.getPayment(); + BigDecimal baseAjustedPayment = basePayment; + + int period = 1; + do { + + BigDecimal interest = borrowed.multiply(zeroRate).setScale(2, BigDecimal.ROUND_HALF_UP); + + // get the current payment + BigDecimal whichBasePayment = loanPlan.getPayment(); + // If step payment + if (payments != null && payments.size() > 0) { + BigDecimal value = getBasePayment(payments, period); + if (value != null) { + whichBasePayment = value; + } + } + + // changing base + if ( whichBasePayment.compareTo(basePayment) != 0) { // 0 means equals + basePayment = whichBasePayment; + /* TODO: To be found!! + LoanPlan revisedLoan = findPeriod(borrowed, basePayment, loanPlan.getRate()).get(0); + baseAjustedPayment = calculateMonthlyAmount(loanPlan); + */ + baseAjustedPayment = basePayment; + } + + BigDecimal refunded = BigDecimal.ZERO; + // Do I have started to refund ? + if (period > loanPlan.getShiftRefundBy()) { + refunded = baseAjustedPayment.subtract(interest); + } + + borrowed = borrowed.subtract(refunded); + + Payment payment = new Payment(); + payment.setBorrowed(borrowed); + payment.setPeriod(period++); + payment.setInterest(interest); + payment.setCapital(refunded); + payment.setInsurance(insurance); + + loanPlan.getPayments().add(payment); + + } while(borrowed.intValue() > 0 && period < 500); + + return loanPlan; + } + + /*** + * Return the current Payment according to the current period + * @param payments + * @param period + * @return + */ + private static BigDecimal getBasePayment(TreeMap payments, int period) + { + BigDecimal basePayment = null; + + for (Map.Entry entry : payments.entrySet()) { + if (period >= entry.getKey()) { + basePayment = ((Payment) entry.getValue()).getBasePayment(); + } + } + + return basePayment; + } + + public static BigDecimal getBorrowedAmountLeftAt(LoanPlan loanPlan, int until) { + + BigDecimal borrowed = loanPlan.getBorrowed(); + BigDecimal zeroRate = loanPlan.getRate().divide(new BigDecimal("100.0")); + + BigDecimal basePayment = LoanBusiness.calculateMonthlyAmount(loanPlan); + + for (int i=0; i < until; i++) { + + BigDecimal interest = borrowed.multiply(zeroRate).setScale(2, BigDecimal.ROUND_HALF_UP); + + Payment payment = new Payment(); + payment.setInterest(interest); + + BigDecimal refunded = basePayment.subtract(interest); + borrowed = borrowed.subtract(refunded); + } + + return borrowed.setScale(2, BigDecimal.ROUND_HALF_UP); + } + +} diff --git a/webapp/src/main/java/biz/eventual/business/StepLoanBusiness.java b/webapp/src/main/java/biz/eventual/business/StepLoanBusiness.java new file mode 100755 index 0000000..8b92b53 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/business/StepLoanBusiness.java @@ -0,0 +1,125 @@ +package biz.eventual.business; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import biz.eventual.bean.StepLoan; +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.bean.SubLoan; + +/** + * Created by Thibault de Lambilly on 25/06/2015. + */ +public class StepLoanBusiness +{ + + public static StepLoan findPayment(LoanPlan loanPlan, List subLoans) { + return StepLoanBusiness.findPayment(new StepLoan(loanPlan, subLoans)); + } + + public static StepLoan findPayment(StepLoan stepLoan) { + + BigDecimal searchAmount = LoanBusiness.calculateMonthlyAmount(stepLoan.getLoanPlan()); + // keep it for record! + stepLoan.getLoanPlan().setPayment(searchAmount); + + double[] step = new double[] {100, 10, 5, 1, 0.1}; + + for (int i = 0; i < step.length; i++) { + BigDecimal max = findMax(stepLoan, step[i], searchAmount); + if (max.compareTo(searchAmount) == 1) { + searchAmount = max.subtract(new BigDecimal(step[i])).setScale(2, BigDecimal.ROUND_HALF_UP); + } + } + + stepLoan.setPayment(searchAmount); + + return stepLoan; + } + + private static BigDecimal findMax(StepLoan stepLoan, double step, BigDecimal amount) { + + BigDecimal leftAmount = BigDecimal.ZERO; + BigDecimal stepBg = new BigDecimal(step) + .setScale(1, BigDecimal.ROUND_HALF_UP); + + BigDecimal stepAmount = amount.subtract(stepBg); // -step 4 the first round + + do { + stepAmount = stepAmount.add(stepBg); + leftAmount = workAmountForScaleLoan(stepLoan, stepAmount); + } while (leftAmount.intValue() > 0); + + return stepAmount; + } + + + private static BigDecimal workAmountForScaleLoan(StepLoan stepLoan, BigDecimal basePayment) { + + stepLoan.setPayments(new ArrayList()); + + BigDecimal searchAmount = stepLoan.getLoanPlan() + .getBorrowed() + .setScale(2, BigDecimal.ROUND_HALF_UP); + + BigDecimal zeroRate = stepLoan.getLoanPlan() + .getRate() + .divide(new BigDecimal(100)); + + int period = stepLoan.getLoanPlan().getPeriod(); + + if (stepLoan.getSubloans() == null) stepLoan.setSubloans(new ArrayList()); + + List subLoansClone = new ArrayList(stepLoan.getSubloans()); + + SubLoan currentSubLoan = null; + if (subLoansClone != null && subLoansClone.size() > 0) { + currentSubLoan = subLoansClone.get(0); + } + + /*** + * Attention : the sub loan can it be bigger than the monthly interest payment ? + */ + for (int i=0; i < period; i++) { + + BigDecimal interest = searchAmount + .multiply(zeroRate) + .setScale(2, BigDecimal.ROUND_HALF_UP); + + Payment onePayment = new Payment(); + onePayment.setInterest(interest); + + BigDecimal subPaymentDeduction = BigDecimal.ZERO; + if (currentSubLoan != null) { + int start = currentSubLoan.getStartingAt(); + int lastSubPayment = (start + currentSubLoan.getPeriod()) - 1; + + // Getting value + if (i >= (start-1) && i < lastSubPayment ) { + subPaymentDeduction = currentSubLoan.getPayment(); + } + + // subPayment done - switching to the next one + if (i == lastSubPayment - 1) { + subLoansClone.remove(currentSubLoan); + + if (subLoansClone.size() > 0) { + currentSubLoan = subLoansClone.get(0); + } + } + } + + BigDecimal stepAmount = basePayment.subtract(subPaymentDeduction); + BigDecimal refunded = stepAmount.subtract(interest); + + onePayment.setCapital(refunded); + stepLoan.getPayments().add(onePayment); + + searchAmount = searchAmount.subtract(refunded); + } + + return searchAmount; + } +} diff --git a/webapp/src/main/java/biz/eventual/controller/CorsFilter.java b/webapp/src/main/java/biz/eventual/controller/CorsFilter.java new file mode 100644 index 0000000..988fd1e --- /dev/null +++ b/webapp/src/main/java/biz/eventual/controller/CorsFilter.java @@ -0,0 +1,39 @@ +package biz.eventual.controller; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; + +/** + * Created by tweety on 11/07/15. + */ +@Component +public class CorsFilter implements Filter { + + // https://spring.io/blog/2015/06/08/cors-support-in-spring-framework + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + HttpServletResponse response = (HttpServletResponse) res; + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); + chain.doFilter(req, res); + + } + + public void init(FilterConfig filterConfig) { + } + + public void destroy() { + } + +} \ No newline at end of file diff --git a/webapp/src/main/java/biz/eventual/controller/LoanController.java b/webapp/src/main/java/biz/eventual/controller/LoanController.java new file mode 100755 index 0000000..de1ddf6 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/controller/LoanController.java @@ -0,0 +1,106 @@ +package biz.eventual.controller; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.TreeMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.dto.PaymentDto; +import biz.eventual.enums.RatePeriod; +import biz.eventual.service.ServiceLoan; + +/** + * Created by tweety on 29/06/15. + */ +@RestController +public class LoanController { + + @Autowired + ServiceLoan serviceLoan; + + + /* + /!\ Spring considers that anything behind the last dot is a file extension such as .json or .xml and trucate it to retrieve your parameter. + */ + @RequestMapping(value = "/loan/get/{capital}/{period}/{rate:.+}", method = RequestMethod.GET) + public LoanPlan getLoanNoInsurance(@PathVariable double capital, + @PathVariable int period, + @PathVariable double rate) { + + // return serviceLoan.findScheduleStepPayment(54537.35, payments, 180, 3.6, RatePeriod.ANNUAL, 31.82); + return serviceLoan.findScheduleStepPayment(capital, null, period, rate, RatePeriod.ANNUAL, 0); + } + + @RequestMapping(value = "/loan/get/{loan}", method = RequestMethod.POST) + public LoanPlan getLoanFromLoan(@PathVariable LoanPlan loan) { + + // return serviceLoan.findScheduleStepPayment(54537.35, payments, 180, 3.6, RatePeriod.ANNUAL, 31.82); + return serviceLoan.findScheduleStepPayment( + loan.getBorrowed().doubleValue(), + null, + loan.getPeriod(), + loan.getRate().doubleValue(), + RatePeriod.MONTHLY, + 0 + ); + } + + + @RequestMapping(value = "/loan/get/{capital}/{period}/{rate}/{insurance:.+}", method = RequestMethod.GET) + public LoanPlan getLoan(@PathVariable double capital, + @PathVariable int period, + @PathVariable double rate, + @PathVariable double insurance) { + + return serviceLoan.findScheduleStepPayment(capital, null, period, rate, RatePeriod.ANNUAL, insurance); + } + + @RequestMapping(value = "/loan/step/{capital}/{period}/{rate}/{insurance}/{steps:.+}", method = RequestMethod.GET) + public LoanPlan getLoanWithStepPayment(@PathVariable double capital, + @PathVariable int period, + @PathVariable double rate, + @PathVariable double insurance, + @PathVariable String steps) { + + // http://localhost:5001/loan/step/1000.0/12/2.0/0/[{"k":1000, "i":1.0, "p": 6}] + + ObjectMapper mapper = new ObjectMapper(); + List paymentsData = null; + + try { + // List + paymentsData = mapper.readValue(steps, new TypeReference>(){}); + } catch (IOException e) { + e.printStackTrace(); + } + + TreeMap payments = new TreeMap(); + + for (PaymentDto dto : paymentsData) { + Payment payment = new Payment(); + payment.setCapital(new BigDecimal(dto.getK()).setScale(2, BigDecimal.ROUND_HALF_UP)); + payment.setInsurance(new BigDecimal(dto.getI()).setScale(2, BigDecimal.ROUND_HALF_UP)); + + payments.put(dto.getP(), payment); + } + + /* + Payment payment = new Payment(); + payment.setCapital(new BigDecimal(510).setScale(2, BigDecimal.ROUND_HALF_UP)); + payment.setInsurance(new BigDecimal(31.82).setScale(2, BigDecimal.ROUND_HALF_UP)); + payments.put(79, payment); + */ + + return serviceLoan.findScheduleStepPayment(capital, payments, period, rate, RatePeriod.ANNUAL, insurance); + } +} diff --git a/webapp/src/main/java/biz/eventual/dto/PaymentDto.java b/webapp/src/main/java/biz/eventual/dto/PaymentDto.java new file mode 100755 index 0000000..8dc7cc1 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/dto/PaymentDto.java @@ -0,0 +1,17 @@ +package biz.eventual.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * Created by thibault.delambilly on 02/07/2015. + */ +@Getter +@Setter +public class PaymentDto +{ + private double k = 0.0; // capital + private double i = 0.0; // interest + private int p = 0; // period + private int f = 0; // insurance +} diff --git a/webapp/src/main/java/biz/eventual/enums/RatePeriod.java b/webapp/src/main/java/biz/eventual/enums/RatePeriod.java new file mode 100755 index 0000000..4093124 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/enums/RatePeriod.java @@ -0,0 +1,9 @@ +package biz.eventual.enums; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +public enum RatePeriod +{ + MONTHLY, ANNUAL +} diff --git a/webapp/src/main/java/biz/eventual/service/ServiceLoan.java b/webapp/src/main/java/biz/eventual/service/ServiceLoan.java new file mode 100755 index 0000000..ae75d46 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/service/ServiceLoan.java @@ -0,0 +1,31 @@ +package biz.eventual.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.TreeMap; + +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +public interface ServiceLoan +{ + BigDecimal getMonthlyAmount(final double borrowed, final int period, final double rate, final RatePeriod ratePeriod); + BigDecimal getMonthlyAmount(final double borrowed, final int period, final double rate, RatePeriod ratePeriod, final int refundStartingAt); + + List getPayments(final double borrowed, final double rate, final int period, final RatePeriod ratePeriod); + List getPayments(final double borrowed, final double rate, final int period, final RatePeriod ratePeriod, final int refundStartingAt); + + LoanPlan getLoanWithPayments(final double borrowed, + final double rate, + final int period, + final RatePeriod ratePeriod); + + List findPeriod(final double borrowed, final double basePayment, double rate, final RatePeriod ratePeriod); + + LoanPlan findScheduleStepPayment(final double borrowed, final TreeMap payments, final int initialPeriod, final double rate, final RatePeriod ratePeriod, final double insurance); + LoanPlan findScheduleStepPayment(final double borrowed, final TreeMap payments, final int initialPeriod, final double rate, final RatePeriod ratePeriod, final double insurance, final int refundStartingAt); +} \ No newline at end of file diff --git a/webapp/src/main/java/biz/eventual/service/ServiceLoanImpl.java b/webapp/src/main/java/biz/eventual/service/ServiceLoanImpl.java new file mode 100755 index 0000000..39de330 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/service/ServiceLoanImpl.java @@ -0,0 +1,122 @@ +package biz.eventual.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.TreeMap; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import biz.eventual.business.LoanBusiness; +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Slf4j +@Service +public class ServiceLoanImpl implements ServiceLoan +{ + @Override + public BigDecimal getMonthlyAmount(final double borrowed, final int period, final double rate, RatePeriod ratePeriod) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, LoanPlan.REFUND_NOW); + return LoanBusiness.calculateMonthlyAmount(loan); + } + + @Override + public BigDecimal getMonthlyAmount(final double borrowed, final int period, final double rate, RatePeriod ratePeriod, final int refundStartingAt) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, refundStartingAt); + return LoanBusiness.calculateMonthlyAmount(loan); + } + + @Override + public List getPayments(final double borrowed, final double rate, final int period, RatePeriod ratePeriod) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, 0, LoanPlan.REFUND_NOW); + return LoanBusiness.getPayments(loan); + } + + @Override + public List getPayments(final double borrowed, final double rate, final int period, RatePeriod ratePeriod, final int refundStartingAt) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, 0, refundStartingAt); + return LoanBusiness.getPayments(loan); + } + + @Override + public LoanPlan getLoanWithPayments(double borrowed, double rate, int period, RatePeriod ratePeriod) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, 0); + loan.setPayments(LoanBusiness.getPayments(loan)); + + log.debug("getLoanWithPayments: %s", loan.toJson()); + + return loan; + } + + @Override + public List findPeriod(double borrowed, double basePayment, double rate, RatePeriod ratePeriod) { + + BigDecimal borrowedBg = new BigDecimal(borrowed).setScale(2, BigDecimal.ROUND_HALF_UP); + BigDecimal baseBg = new BigDecimal(basePayment).setScale(2, BigDecimal.ROUND_HALF_UP); + BigDecimal actualRate = new BigDecimal(rate).setScale(4, BigDecimal.ROUND_HALF_UP); + + if (ratePeriod == RatePeriod.ANNUAL) { + actualRate = LoanBusiness.getMontlyRateFromYearly(actualRate); + } + + return LoanBusiness.findPeriod(borrowedBg, baseBg, actualRate); + } + + /*** + * Without shiftRefundBy + * @param borrowed + * @param payments + * @param initialPeriod + * @param rate + * @param ratePeriod + * @param insurance + * @return + */ + @Override + public LoanPlan findScheduleStepPayment(final double borrowed, + final TreeMap payments, + final int initialPeriod, + final double rate, + final RatePeriod ratePeriod, + final double insurance) { + + return findScheduleStepPayment(borrowed, payments, initialPeriod, rate, ratePeriod, insurance, LoanPlan.REFUND_NOW); + } + + /*** + * Including shiftRefundBy + * @param borrowed + * @param refundStartingAt + * @param payments + * @param initialPeriod + * @param rate + * @param ratePeriod + * @param insurance + * @return + */ + @Override + public LoanPlan findScheduleStepPayment(final double borrowed, + final TreeMap payments, + final int initialPeriod, + final double rate, + final RatePeriod ratePeriod, + final double insurance, + final int refundStartingAt) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, initialPeriod, rate, ratePeriod, insurance, refundStartingAt); + BigDecimal payment = LoanBusiness.calculateMonthlyAmount(loan); + loan.setPayment(payment); + + return LoanBusiness.findScheduleStepPayment(loan, payments); + } +} diff --git a/webapp/src/main/java/biz/eventual/service/ServiceStepLoan.java b/webapp/src/main/java/biz/eventual/service/ServiceStepLoan.java new file mode 100755 index 0000000..e87740c --- /dev/null +++ b/webapp/src/main/java/biz/eventual/service/ServiceStepLoan.java @@ -0,0 +1,16 @@ +package biz.eventual.service; + +import java.util.List; + +import biz.eventual.bean.StepLoan; +import biz.eventual.bean.SubLoan; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +public interface ServiceStepLoan +{ + StepLoan getPayment(final double borrowed, final int period, final double rate, final RatePeriod ratePeriod, final List subLoans, final double interest); + +} diff --git a/webapp/src/main/java/biz/eventual/service/ServiceStepLoanImpl.java b/webapp/src/main/java/biz/eventual/service/ServiceStepLoanImpl.java new file mode 100755 index 0000000..39fe241 --- /dev/null +++ b/webapp/src/main/java/biz/eventual/service/ServiceStepLoanImpl.java @@ -0,0 +1,34 @@ +package biz.eventual.service; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.StepLoan; +import biz.eventual.bean.SubLoan; +import biz.eventual.business.LoanBusiness; +import biz.eventual.business.StepLoanBusiness; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Slf4j +@Service +public class ServiceStepLoanImpl implements ServiceStepLoan +{ + @Override + public StepLoan getPayment(final double borrowed, + final int period, + final double rate, + final RatePeriod ratePeriod, + final List subLoans, + final double interest) + { + LoanPlan loan = LoanBusiness.getLoanPlan(borrowed, period, rate, ratePeriod, interest); + + return StepLoanBusiness.findPayment(loan, subLoans); + } +} diff --git a/webapp/src/main/resources/application.properties b/webapp/src/main/resources/application.properties new file mode 100755 index 0000000..1df34b1 --- /dev/null +++ b/webapp/src/main/resources/application.properties @@ -0,0 +1,43 @@ +## database config +#spring.datasource.url = jdbc:mysql://localhost:3306/loanwizard +#spring.datasource.username = root +#spring.datasource.password = + +## Show or not log for each sql query +spring.jpa.show-sql = true + +# Hibernate ddl auto (create, create-drop, update) +#spring.jpa.hibernate.ddl-auto = update + +## Naming strategy +spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy + +# Use spring.jpa.properties.* for Hibernate native properties (the prefix is +# stripped before adding them to the entity manager) + +## The SQL dialect makes Hibernate generate better SQL for the chosen database. +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect + +## for automatical reload +spring.thymeleaf.cache=false + +## random port number +server.port:5001 + +# define the messages folder +spring.messages.basename=i18n/messages + +## add /shutdown command +endpoints.shutdown.enabled=true +## health complet +endpoints.health.sensitive=false + +#logging.file= +logging.level.org.springframework.web = DEBUG +logging.level.biz.eventual = DEBUG +logging.level.org.hibernate = DEBUG + +# Log file location (in addition to the console) +logging.file = ./application.log + +debug=true \ No newline at end of file diff --git a/webapp/src/main/resources/banner.txt b/webapp/src/main/resources/banner.txt new file mode 100755 index 0000000..f2b02be --- /dev/null +++ b/webapp/src/main/resources/banner.txt @@ -0,0 +1,5 @@ + _____ _____ _ _ _____ _ _ _ _ ___ _ _ ___ ___ _ _ ___ ___ ___ + | __\ \ / / __| \| |_ _| | | |/_\ | | | _ ) | | / __|_ _| \| | __/ __/ __| + | _| \ V /| _|| .` | | | | |_| / _ \| |__ | _ \ |_| \__ \| || .` | _|\__ \__ \ + |___| \_/ |___|_|\_| |_| \___/_/ \_\____| |___/\___/|___/___|_|\_|___|___/___/ + diff --git a/webapp/src/main/resources/logback.xml.old b/webapp/src/main/resources/logback.xml.old new file mode 100755 index 0000000..2410b2c --- /dev/null +++ b/webapp/src/main/resources/logback.xml.old @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/webapp/src/main/resources/templates/index.html b/webapp/src/main/resources/templates/index.html new file mode 100755 index 0000000..ea1559f --- /dev/null +++ b/webapp/src/main/resources/templates/index.html @@ -0,0 +1,10 @@ + + + + + + + + Yo + + \ No newline at end of file diff --git a/webapp/src/test/java/biz/eventual/Business/LoanBusinessTest.java b/webapp/src/test/java/biz/eventual/Business/LoanBusinessTest.java new file mode 100755 index 0000000..9e431af --- /dev/null +++ b/webapp/src/test/java/biz/eventual/Business/LoanBusinessTest.java @@ -0,0 +1,24 @@ +package biz.eventual.Business; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; + +import biz.eventual.LoanwizardApplication; + +/** + * Created by Thibault de Lambilly on 30/07/2015. + */ +@RunWith(MockitoJUnitRunner.class) +@SpringApplicationConfiguration(classes = LoanwizardApplication.class) +@WebAppConfiguration +public class LoanBusinessTest +{ + @Before + public void setUp() throws Exception + { + + } +} \ No newline at end of file diff --git a/webapp/src/test/java/biz/eventual/LoanwizardApplicationTests.java b/webapp/src/test/java/biz/eventual/LoanwizardApplicationTests.java new file mode 100755 index 0000000..649eeda --- /dev/null +++ b/webapp/src/test/java/biz/eventual/LoanwizardApplicationTests.java @@ -0,0 +1,18 @@ +package biz.eventual; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = LoanwizardApplication.class) +@WebAppConfiguration +public class LoanwizardApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/webapp/src/test/java/biz/eventual/service/ServiceLoanTest.java b/webapp/src/test/java/biz/eventual/service/ServiceLoanTest.java new file mode 100755 index 0000000..7efb28b --- /dev/null +++ b/webapp/src/test/java/biz/eventual/service/ServiceLoanTest.java @@ -0,0 +1,123 @@ +package biz.eventual.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.TreeMap; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import junit.framework.TestCase; + +import biz.eventual.LoanwizardApplication; +import biz.eventual.bean.StepLoan; +import biz.eventual.bean.LoanPlan; +import biz.eventual.bean.Payment; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Slf4j +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = LoanwizardApplication.class) +@WebAppConfiguration +public class ServiceLoanTest extends TestCase +{ + + @Autowired + ServiceLoan serviceLoan; + + @Autowired + ServiceStepLoan serviceStepLoan; + + @Test + public void checkMonthlyAmount() { + + StepLoan stepLoan; + + BigDecimal amount = serviceLoan.getMonthlyAmount(90000.0, 240, 2, RatePeriod.ANNUAL); + log.debug("montant "+amount); + + amount = serviceLoan.getMonthlyAmount(15000.0, 60, 1, RatePeriod.ANNUAL); + log.debug("montant "+amount); + + //double capital = serviceLoan.getMonthlyAmount(1000.0, 24, 18.0, RatePeriod.ANNUAL); + //assertEquals(49.92, capital); + + amount = serviceLoan.getMonthlyAmount(65000, 96, 2.85, RatePeriod.ANNUAL); + assertEquals(758.00, amount.doubleValue()); + + } + + @Test + public void findPeriod() { + + List loans = serviceLoan.findPeriod(20000, 660, 12, RatePeriod.ANNUAL); + assertTrue(loans.size() == 2); + assertEquals(36, loans.get(0).getPeriod()); + assertEquals(37, loans.get(1).getPeriod()); + } + + @Test + public void getPayment() { + + List payments; + + payments = serviceLoan.getPayments(20000, 12, 36, RatePeriod.ANNUAL); + assertEquals(36, payments.size()); + assertEquals(664.29, payments.get(0).getBasePayment().doubleValue()); + + payments = serviceLoan.getPayments(54537.35, 3.6, 180, RatePeriod.ANNUAL); + assertEquals(180, payments.size()); + assertEquals(392.56, payments.get(0).getBasePayment().doubleValue()); + + // shift by 12 payments + payments = serviceLoan.getPayments(54537.35, 3.6, 192, RatePeriod.ANNUAL, 12); + assertEquals(192, payments.size()); + // only interest + assertEquals(163.61, payments.get(0).getBasePayment().doubleValue()); + // last interest + assertEquals(163.61, payments.get(11).getBasePayment().doubleValue()); + // start refunding + assertEquals(392.56, payments.get(12).getBasePayment().doubleValue()); + + } + + @Test + public void findScheduleStepPayment() { + + LoanPlan loanPlan; + + // My loan + BigDecimal amount = serviceLoan.getMonthlyAmount(54537.35, 180, 3.6, RatePeriod.ANNUAL); + assertEquals(392.56, amount.doubleValue()); + + // No change + loanPlan = serviceLoan.findScheduleStepPayment(54537.35, null, 180, 3.6, RatePeriod.ANNUAL, 0); + assertEquals(180, loanPlan.getPayments().size()); + + // With step + TreeMap payments = new TreeMap(); + Payment payment = new Payment(); + + payment.setCapital(new BigDecimal(510).setScale(2, BigDecimal.ROUND_HALF_UP)); + payment.setInsurance(new BigDecimal(31.82).setScale(2, BigDecimal.ROUND_HALF_UP)); + payments.put(79, payment); + + loanPlan = serviceLoan.findScheduleStepPayment(54537.35, payments, 180, 3.6, RatePeriod.ANNUAL, 0); + assertEquals(154, loanPlan.getPayments().size()); + + payment = new Payment(); + payment.setCapital(new BigDecimal(560).setScale(2, BigDecimal.ROUND_HALF_UP)); + payments.put(103, payment); + + loanPlan = serviceLoan.findScheduleStepPayment(54537.35, payments, 180, 3.6, RatePeriod.ANNUAL, 0); + assertEquals(149, loanPlan.getPayments().size()); + } + +} \ No newline at end of file diff --git a/webapp/src/test/java/biz/eventual/service/ServiceStepLoanTest.java b/webapp/src/test/java/biz/eventual/service/ServiceStepLoanTest.java new file mode 100755 index 0000000..3c33b49 --- /dev/null +++ b/webapp/src/test/java/biz/eventual/service/ServiceStepLoanTest.java @@ -0,0 +1,75 @@ +package biz.eventual.service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import junit.framework.TestCase; + +import biz.eventual.LoanwizardApplication; +import biz.eventual.bean.StepLoan; +import biz.eventual.bean.SubLoan; +import biz.eventual.enums.RatePeriod; + +/** + * Created by Thibault de Lambilly on 19/06/2015. + */ +@Slf4j +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = LoanwizardApplication.class) +@WebAppConfiguration +public class ServiceStepLoanTest extends TestCase +{ + + @Autowired + ServiceLoan serviceLoan; + + @Autowired + ServiceStepLoan serviceStepLoan; + + @Test + public void checkStepMonthlyAmount() { + + StepLoan stepLoan; + + // Subloan empty + stepLoan = serviceStepLoan.getPayment(65000.0, 96, 0.2375, RatePeriod.MONTHLY, new ArrayList(), 0); + assertEquals(758.00, stepLoan.getPayment().doubleValue()); + + + List subLoans = new ArrayList(); + BigDecimal borrowed = new BigDecimal(15000.0).setScale(2, BigDecimal.ROUND_HALF_UP); + BigDecimal rate = new BigDecimal(1).setScale(2, BigDecimal.ROUND_HALF_UP); + SubLoan subLoan = new SubLoan(borrowed, rate, 60, 1); + + BigDecimal payment = serviceLoan.getMonthlyAmount(15000.0, 60, 1, RatePeriod.ANNUAL); + subLoan.setPayment(payment); + subLoans.add(subLoan); + + stepLoan = serviceStepLoan.getPayment(90000.0, 240, 2, RatePeriod.ANNUAL, subLoans, 0); + log.debug("XXXXX "+stepLoan.toJson()); + + } + + @Test + public void getStepLoan() { + + BigDecimal monthlyPayment; + + monthlyPayment = serviceLoan.getMonthlyAmount(20000, 36, 12, RatePeriod.ANNUAL); + assertEquals(664.29, monthlyPayment); + + StepLoan stepLoan = serviceStepLoan.getPayment(20000, 36, 12, RatePeriod.ANNUAL, null, 0); + assertEquals(664.29, stepLoan.getPayments().get(0).getBasePayment().doubleValue()); + + } + + +} \ No newline at end of file