├── .gitignore ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── accountant.php ├── gitattributes ├── package.json ├── phpunit.xml.dist ├── public ├── css │ ├── app.css │ └── app.css.map ├── fonts │ └── vendor │ │ ├── bootstrap-sass │ │ └── bootstrap │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── font-awesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── js │ ├── app.js │ └── app.js.map └── mix-manifest.json ├── resources ├── assets │ ├── fonts │ │ └── vendor │ │ │ └── font-awesome │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ ├── js │ │ ├── app.js │ │ ├── bootstrap.js │ │ └── components │ │ │ ├── DashboardComponent.vue │ │ │ └── DateRangeComponent.vue │ └── sass │ │ ├── _custom.scss │ │ └── app.scss └── views │ ├── app.blade.php │ ├── charges │ ├── index.blade.php │ └── show.blade.php │ ├── customers │ ├── index.blade.php │ └── show.blade.php │ ├── dashboard.blade.php │ ├── invoices │ ├── index.blade.php │ └── show.blade.php │ ├── partials │ ├── invoices.blade.php │ └── pagination.blade.php │ └── subscriptions │ ├── index.blade.php │ └── show.blade.php ├── routes └── web.php ├── src ├── Accountant.php ├── AccountantServiceProvider.php ├── Cabinet.php ├── Client.php ├── ClientFactory.php ├── Clients │ ├── Balance.php │ ├── BalanceTransaction.php │ ├── Charge.php │ ├── Customer.php │ ├── Invoice.php │ └── Subscription.php ├── Events │ ├── CacheRefreshStarted.php │ └── CacheRefreshStopped.php ├── Facades │ └── Accountant.php ├── Http │ └── Controllers │ │ ├── BalanceController.php │ │ ├── ChargeController.php │ │ ├── Controller.php │ │ ├── CustomerController.php │ │ ├── HomeController.php │ │ ├── InvoiceController.php │ │ └── SubscriptionController.php ├── Jobs │ └── PutToCache.php ├── Listeners │ ├── CreateRefreshFile.php │ ├── RemoveRefreshFile.php │ └── ValidateCache.php └── Paginator.php ├── tests └── Unit │ └── ClientTest.php └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | composer.lock 5 | yarn.lock 6 | package-lock.json 7 | yarn-error.log -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | 6 | sudo: false 7 | 8 | install: travis_retry composer install --no-interaction --prefer-dist --no-suggest 9 | 10 | script: vendor/bin/phpunit --verbose -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tom Irons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Accountant 2 | [![Version](https://poser.pugx.org/tomirons/laravel-accountant/v/stable.svg)](https://packagist.org/packages/tomirons/laravel-accountant) 3 | [![Total Downloads](https://img.shields.io/packagist/dt/tomirons/laravel-accountant.svg)](https://packagist.org/packages/tomirons/laravel-accountant) 4 | [![Build Status](https://travis-ci.org/tomirons/laravel-accountant.svg)](https://travis-ci.org/tomirons/laravel-accountant) 5 | [![License](https://poser.pugx.org/tomirons/laravel-accountant/license.svg)](https://packagist.org/packages/tomirons/laravel-accountant) 6 | 7 | ## Introduction 8 | 9 | Accountant is a beautiful dashboard where you can view Stripe data without ever having to leave your application. 10 | 11 | ## Requirements 12 | 13 | - PHP >= 7.1 14 | - Laravel 5.5.* 15 | - Configured Queue Driver 16 | 17 | ## Installation 18 | 19 | 1) Run the following command to install the package: 20 | 21 | ````shell 22 | composer require tomirons/laravel-accountant 23 | ```` 24 | 25 | 2) Run the following command to publish the assets and configuration 26 | 27 | ````shell 28 | php artisan vendor:publish --provider="TomIrons\Accountant\AccountantServiceProvider" 29 | ```` 30 | **Note:** When updating, `--force` will need to be suffixed to replace all assets. If you've updated the configuration file, you'll want to also add `--tag=accountant-assets` so it doesn't get replaced. 31 | 32 | 33 | ## Usage 34 | 35 | All routes for accountant are prefixed with `accountant`, so to view the dashboard head to `http://example.dev/accountant`. By default we use the `auth` middleware, feel free to add or change this in the configuration. 36 | 37 | ## License 38 | 39 | Laravel Accountant is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomirons/laravel-accountant", 3 | "description": "Dashboard for Stripe.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Tom Irons", 8 | "email": "tom.irons@hotmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=7.1.0", 13 | "illuminate/contracts": "5.5.*", 14 | "illuminate/support": "5.5.*", 15 | "illuminate/pagination": "5.5.*", 16 | "stripe/stripe-php": "^5.1" 17 | }, 18 | "require-dev": { 19 | "mockery/mockery": "~0.9.4", 20 | "phpunit/phpunit": "^6.3" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "TomIrons\\Accountant\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "TomIrons\\Accountant\\Tests\\": "tests" 30 | } 31 | }, 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "1.0-dev" 35 | }, 36 | "laravel": { 37 | "providers": [ 38 | "TomIrons\\Accountant\\AccountantServiceProvider" 39 | ], 40 | "aliases": { 41 | "Accountant": "TomIrons\\Accountant\\Facades\\Accountant" 42 | } 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /config/accountant.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'limit' => 15, 14 | ], 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Middleware 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This is the middleware used for all routes of the package. By default 22 | | it uses the 'auth' middleware, but it can be configured how ever 23 | | you'd like. You can use a string of middleware(s) or an array. 24 | | 25 | | Supported types: string, array 26 | */ 27 | 28 | 'middleware' => 'auth', 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Cache Settings 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Here you can change the settings that are used when utilizing the cache. 36 | */ 37 | 38 | 'cache' => [ 39 | 'driver' => 'file', 40 | 'time' => 60, 41 | ], 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Queue Settings 46 | |-------------------------------------------------------------------------- 47 | | 48 | | Here you can change the queue name that is used when dispatching jobs. 49 | | By default this is null, if you'd like to use a named queue you'll 50 | | have to create a worker for the specified queue. 51 | */ 52 | 53 | 'queue' => null, 54 | ]; 55 | -------------------------------------------------------------------------------- /gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | /tests export-ignore 4 | .gitattributes export-ignore 5 | .gitignore export-ignore 6 | .travis.yml export-ignore 7 | phpunit.xml export-ignore 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.16.2", 14 | "bootstrap-notify": "^3.1.3", 15 | "bootstrap-sass": "^3.3.7", 16 | "chart.js": "^2.7.0", 17 | "font-awesome": "^4.7.0", 18 | "jquery": "^3.1.0", 19 | "lodash": "^4.16.2", 20 | "vue": "^2.2.0", 21 | "vue-bootstrap-datetimepicker": "^3.1.2", 22 | "vue-chartjs": "^2.8.7" 23 | }, 24 | "devDependencies": { 25 | "cross-env": "^5.0.1", 26 | "es6-promise": "^4.0.5", 27 | "laravel-mix": "^1.4.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/css/app.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"/css/app.css","sources":[],"mappings":";;;;A;;;;;;;;;;;;;A","sourceRoot":""} -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/font-awesome/FontAwesome.otf -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/font-awesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/font-awesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/font-awesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/fonts/vendor/font-awesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/public/fonts/vendor/font-awesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js?id=a57e20164df13ddad58e", 3 | "/css/app.css": "/css/app.css?id=2b8cb61d6a4e220ce827", 4 | "/js/app.js.map": "/js/app.js.map?id=3caa4d2bcb372de23880", 5 | "/css/app.css.map": "/css/app.css.map?id=c4dce3a23739acdb6ec2" 6 | } -------------------------------------------------------------------------------- /resources/assets/fonts/vendor/font-awesome/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/resources/assets/fonts/vendor/font-awesome/FontAwesome.otf -------------------------------------------------------------------------------- /resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomirons/laravel-accountant/5e99350f382fc32f933b8c2f56d883f8b4b59782/resources/assets/fonts/vendor/font-awesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap') 2 | 3 | Vue.component('dashboard-component', require('./components/DashboardComponent.vue')); 4 | 5 | const app = new Vue({ 6 | el: '#app' 7 | }); 8 | 9 | $(function() { 10 | $(".clickable").click(function() { 11 | window.location = $(this).data("href"); 12 | }); 13 | $('[data-toggle="tooltip"]').tooltip(); 14 | }); -------------------------------------------------------------------------------- /resources/assets/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | 3 | try { 4 | window.$ = window.jQuery = require('jquery'); 5 | 6 | require('bootstrap-sass'); 7 | } catch (e) {} 8 | 9 | /* Axios Configuration */ 10 | window.axios = require('axios'); 11 | 12 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 13 | 14 | let token = document.head.querySelector('meta[name="csrf-token"]'); 15 | 16 | if (token) { 17 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 18 | } 19 | 20 | /* Plugin Imports */ 21 | window.moment = require('moment') 22 | 23 | window.Vue = require('vue'); 24 | 25 | Vue.prototype.$http = axios.create(); -------------------------------------------------------------------------------- /resources/assets/js/components/DashboardComponent.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /resources/assets/js/components/DateRangeComponent.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | -------------------------------------------------------------------------------- /resources/assets/sass/_custom.scss: -------------------------------------------------------------------------------- 1 | $theme-color: #3b8edc; 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Rubik:400,500,700'); 4 | 5 | body { 6 | padding-top: 110px; 7 | background: #f3f3f3; 8 | font-family: Rubik, sans-serif; 9 | color: #313131; 10 | } 11 | 12 | .btn { 13 | text-transform: uppercase; 14 | margin-bottom: 10px; 15 | &.btn-default { 16 | border: 1px solid $theme-color; 17 | } 18 | &.btn-clear { 19 | border: 1px solid $theme-color; 20 | background: none; 21 | color: #777; 22 | border: none; 23 | padding: 6px; 24 | margin: 3px; 25 | &:focus { 26 | outline: none; 27 | } 28 | } 29 | &.btn-default:hover, &.btn-clear:hover { 30 | color: #333; 31 | } 32 | &:active, &.active { 33 | box-shadow: none; 34 | } 35 | } 36 | 37 | .pager > { 38 | li > { 39 | a, span { 40 | border-color: $theme-color; 41 | padding: 10px 15px; 42 | color: #444; 43 | border-radius: 4px; 44 | text-transform: uppercase; 45 | } 46 | a { 47 | &:focus, &:hover { 48 | border-color: $theme-color; 49 | } 50 | } 51 | span { 52 | &:focus, &:hover { 53 | border-color: $theme-color; 54 | } 55 | } 56 | } 57 | .disabled { 58 | a, span { 59 | border-color: #ddd; 60 | background-color: transparent; 61 | } 62 | a { 63 | &:focus, &:hover { 64 | border-color: #ddd; 65 | background-color: transparent; 66 | } 67 | } 68 | span { 69 | &:focus, &:hover { 70 | border-color: #ddd; 71 | background-color: transparent; 72 | } 73 | } 74 | } 75 | } 76 | 77 | .table > { 78 | thead > tr > { 79 | td { 80 | padding: 8px; 81 | text-transform: uppercase; 82 | } 83 | th { 84 | border-width: 1px; 85 | text-transform: uppercase; 86 | padding: 15px; 87 | } 88 | } 89 | tbody { 90 | tr.clickable:hover { 91 | cursor: pointer; 92 | } 93 | > tr > { 94 | th { 95 | vertical-align: middle; 96 | } 97 | td, th { 98 | padding: 15px; 99 | } 100 | } 101 | } 102 | tfoot > tr > { 103 | td, th { 104 | padding: 15px; 105 | } 106 | } 107 | } 108 | 109 | .navbar { 110 | min-height: 80px; 111 | .navbar-header { 112 | .navbar-toggle { 113 | margin-top: 22px; 114 | } 115 | .navbar-brand { 116 | margin: 14px 0; 117 | color: $theme-color; 118 | } 119 | } 120 | } 121 | 122 | .navbar-default { 123 | background: #fff; 124 | .navbar-nav > li { 125 | &.active > a { 126 | background: transparent; 127 | border-bottom: 3px solid $theme-color; 128 | &:focus, &:hover { 129 | background: transparent; 130 | border-bottom: 3px solid $theme-color; 131 | } 132 | } 133 | > a { 134 | line-height: 48px; 135 | } 136 | } 137 | } 138 | 139 | @media (max-width: $screen-xs-max) { 140 | .navbar-default .navbar-nav > li.active > a { 141 | border: none; 142 | } 143 | } 144 | 145 | .nav { 146 | > li { 147 | > a { 148 | padding: 15px 0; 149 | margin: 0 20px; 150 | text-transform: uppercase; 151 | color: #9e9e9e; 152 | } 153 | &.active { 154 | font-weight: 500; 155 | } 156 | } 157 | .fa.fa-github { 158 | font-size: 2em; 159 | vertical-align: middle; 160 | padding: 10px; 161 | } 162 | } 163 | 164 | @media (min-width: $screen-md-min) { 165 | .nav > li:first-of-type > a { 166 | margin-left: 0; 167 | } 168 | } 169 | 170 | @media (max-width: $screen-xs-max) { 171 | .nav > li > a { 172 | padding: 0; 173 | line-height: 40px; 174 | } 175 | } 176 | 177 | .panel { 178 | &.panel-info { 179 | border-color: #ddd; 180 | } 181 | .panel-body { 182 | background: #fff; 183 | padding: 18px; 184 | border-radius: 4px; 185 | ul { 186 | margin-bottom: 0; 187 | } 188 | h3.panel-title { 189 | margin: 0 0 12px; 190 | font-weight: 500; 191 | text-transform: uppercase; 192 | font-size: 16px; 193 | > span.stat { 194 | font-size: 24px; 195 | font-weight: 100; 196 | line-height: .7em; 197 | color: $theme-color; 198 | float: right; 199 | } 200 | } 201 | h4.subtitle { 202 | margin: 12px 0; 203 | font-weight: 500; 204 | font-size: 16px; 205 | color: #9e9e9e; 206 | } 207 | .view-all { 208 | margin-top: 10px; 209 | } 210 | } 211 | &.panel-info .panel-body { 212 | padding: 25px; 213 | } 214 | } 215 | 216 | .label { 217 | padding: 0.3em 0.6em 0.3em; 218 | font-weight: 500; 219 | } 220 | 221 | .invoices.list-group > .list-group-item > span:not(:first-of-type) { 222 | margin-left: 15px; 223 | } 224 | 225 | span { 226 | &.attribute-label { 227 | font-weight: 500; 228 | } 229 | &.not-available { 230 | font-style: italic; 231 | color: #a5a5a5; 232 | } 233 | &.filtered-range { 234 | color: #a5a5a5; 235 | margin-right: 10px; 236 | strong { 237 | font-weight: 500; 238 | color: #999; 239 | } 240 | } 241 | } 242 | 243 | .data-actions { 244 | margin-bottom: 10px; 245 | } 246 | 247 | .input-group-addon.middle { 248 | border-right: none; 249 | border-left: none; 250 | } 251 | 252 | .bootstrap-datetimepicker-widget { 253 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 254 | } 255 | 256 | [v-cloak] { 257 | display: none; 258 | } -------------------------------------------------------------------------------- /resources/assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | // Fonts 2 | @import url("https://fonts.googleapis.com/css?family=Raleway:300,400,600"); 3 | 4 | // Fontawesome 5 | $fa-font-path: "/vendor/accountant/fonts/vendor/font-awesome"; 6 | @import "~font-awesome/scss/font-awesome"; 7 | 8 | // Bootstrap 9 | $icon-font-path: "/vendor/accountant/fonts/vendor/bootstrap-sass/bootstrap/"; 10 | @import "~bootstrap-sass/assets/stylesheets/bootstrap"; 11 | @import "~eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.css"; 12 | 13 | 14 | // Custom 15 | @import "custom"; -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Accountant 12 | 13 | 14 | 15 | 16 | 17 | 18 | 55 | 56 |
57 |
58 |
59 | @yield('content') 60 |
61 |
62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /resources/views/charges/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @foreach ($charges as $charge) 17 | 18 | 27 | 28 | 29 | 30 | 31 | @endforeach 32 | 33 |
AmountDescriptionDate
19 | @if ($charge->refunded) 20 | Refunded 21 | @elseif (!$charge->paid) 22 | Failed 23 | @else 24 | Paid 25 | @endif 26 | ${{ Accountant::formatAmount($charge->amount) }}{{ $charge->description ? "{$charge->description} - " : null }}{{ $charge->id }}{{ Accountant::formatDate($charge->created) }}
34 |
35 | {{ $charges->links('accountant::partials.pagination') }} 36 |
37 |
38 |
39 | @endsection -------------------------------------------------------------------------------- /resources/views/charges/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 |

${{ Accountant::formatAmount($charge->amount) }} {{ $charge->currency }} 7 | @if ($charge->refunded) 8 |
Refunded
9 | @elseif (!$charge->paid) 10 |
Failed
11 | @else 12 |
Paid
13 | @endif 14 |

15 |
16 |
17 |
Payment Details
18 |
19 |
20 |
21 |
    22 |
  • 23 | ID: {{ $charge->id }} 24 |
  • 25 |
  • 26 | Amount: ${{ Accountant::formatAmount($charge->amount) }} {{ $charge->currency }} 27 |
  • 28 | @if ($charge->refunded) 29 |
  • 30 | Amount Refunded: ${{ Accountant::formatAmount($charge->amount_refunded) }} {{ $charge->currency }} 31 |
  • 32 | @endif 33 |
  • 34 | Fee: ${{ ($charge->refunded || !$charge->balance) ? '0.00' : Accountant::formatAmount($charge->balance->fee) }} 35 |
  • 36 |
37 |
38 |
39 |
    40 |
  • 41 | Date: {{ Accountant::formatDate($charge->created) }} 42 |
  • 43 |
  • 44 | Description: {!! $charge->description ?? 'Not available' !!} 45 |
  • 46 |
47 |
48 |
49 |
50 |
Card
51 |
52 |
53 |
54 |
    55 |
  • 56 | ID: {{ $charge->source->id }} 57 |
  • 58 |
  • 59 | Name: {!! $charge->source->name ?? 'Not available' !!} 60 |
  • 61 |
  • 62 | Number: ************{{ $charge->source->last4 }} 63 |
  • 64 |
  • 65 | Fingerprint: {{ $charge->source->fingerprint }} 66 |
  • 67 |
  • 68 | Expires: {{ $charge->source->exp_month . '/' . $charge->source->exp_year }} 69 |
  • 70 |
  • 71 | Type: {{ $charge->source->brand }} 72 |
  • 73 |
74 |
75 |
76 |
    77 |
  • 78 | Zip code: {{ $charge->source->address_zip }} 79 |
  • 80 |
  • 81 | Zip check: 82 |
  • 83 |
84 |
85 |
86 |
87 | @if ($charge->refunds->total_count) 88 |
Refunds
89 | 117 | @endif 118 |
119 |
120 | @endsection -------------------------------------------------------------------------------- /resources/views/customers/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach ($customers as $customer) 16 | 17 | 18 | 19 | 20 | 21 | @endforeach 22 | 23 |
EmailCardCreated
{{ $customer->email }} - {{ $customer->id }}{{ $customer->card->brand }} - {{ $customer->card->last4 }} - {{ $customer->card->exp_month }}/{{ $customer->card->exp_year }}{{ Accountant::formatDate($customer->created) }}
24 |
25 | {{ $customers->links('accountant::partials.pagination') }} 26 |
27 |
28 |
29 | @endsection -------------------------------------------------------------------------------- /resources/views/customers/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 |

{{ $customer->email }} - {{ $customer->id }}

7 |
8 |
9 |
Details
10 |
11 |
12 |
13 |
    14 |
  • 15 | ID: {{ $customer->id }} 16 |
  • 17 |
  • 18 | Created: {{ Accountant::formatDate($customer->created) }} 19 |
  • 20 |
21 |
22 |
23 |
    24 |
  • 25 | Email: {{ $customer->email }} 26 |
  • 27 |
  • 28 | Description: {!! $customer->description ?? 'Not available' !!} 29 |
  • 30 |
31 |
32 |
33 |
34 | @if ($customer->cards->count()) 35 |
Cards
36 |
37 | @foreach ($customer->cards as $card) 38 |
39 | 47 |
48 |
49 |
50 |
    51 |
  • 52 | ID: {{ $card->id }} 53 |
  • 54 |
  • 55 | Name: {!! $card->name ?? 'No name provided' !!} 56 |
  • 57 |
  • 58 | Number: ****{{ $card->last4 }} 59 |
  • 60 |
  • 61 | Fingerprint: {{ $card->fingerprint }} 62 |
  • 63 |
  • 64 | Expires: {{ $card->exp_month }}/{{ $card->exp_year }} 65 |
  • 66 |
67 |
68 |
69 |
    70 |
  • 71 | Type: {{ $card->brand }} 72 |
  • 73 |
  • 74 | Postal code: {{ $card->address_zip }} 75 |
  • 76 |
  • 77 | Origin: {!! $card->address_country ?? 'Not available' !!} 78 |
  • 79 |
  • 80 | CVC check: 81 |
  • 82 |
  • 83 | Zip check: 84 |
  • 85 |
86 |
87 |
88 |
89 |
90 | @endforeach 91 |
92 | @endif 93 | @if ($customer->subscriptions->count()) 94 |
Active Subscriptions
95 | 107 | @endif 108 | @includeWhen($customer->invoices, 'accountant::partials.invoices', ['invoices' => $customer->invoices]) 109 |
110 |
111 | @endsection -------------------------------------------------------------------------------- /resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 | 5 | @endsection -------------------------------------------------------------------------------- /resources/views/invoices/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | @foreach ($invoices as $invoice) 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | @endforeach 34 | 35 |
AmountNumberCustomerDueCreated
21 | @if ($invoice->paid) 22 | Paid 23 | @else 24 | Unpaid 25 | @endif 26 | ${{ Accountant::formatAmount($invoice->total) }}{{ $invoice->number ? $invoice->number : $invoice->id }}{{ $invoice->customer }}{{ $invoice->due_date ? Accountant::formatDate($invoice->due_date, 'Y/m/d') : '-' }}{{ Accountant::formatDate($invoice->date, 'Y/m/d') }}
36 |
37 | {{ $invoices->links('accountant::partials.pagination') }} 38 |
39 |
40 |
41 | @endsection -------------------------------------------------------------------------------- /resources/views/invoices/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 |

Invoice

7 |
{{ $invoice->id }}
8 |
9 |
10 |
Details
11 |
12 |
13 |
14 |
    15 |
  • 16 | ID: {{ $invoice->id }} 17 |
  • 18 | @if ($invoice->number) 19 |
  • 20 | Number: {{ $invoice->number }} 21 |
  • 22 | @endif 23 |
  • 24 | Date: {{ Accountant::formatDate($invoice->date) }} 25 |
  • 26 |
  • 27 | Period: {{ Accountant::formatDate($invoice->period_start) }} to {{ Accountant::formatDate($invoice->period_end) }} 28 |
  • 29 |
30 |
31 |
32 |
    33 |
  • 34 | Customer: {{ $invoice->customer->id }} 35 |
  • 36 |
  • 37 | Subscription: {{ $invoice->subscription }} 38 |
  • 39 |
  • 40 | Description: {!! $invoice->description ?? 'No description' !!} 41 |
  • 42 |
  • 43 | Billing: {{ $invoice->billing == 'charge_automatically' ? 'Charge automatically' : 'Send invoice' }} 44 |
  • 45 | @if ($invoice->due_date) 46 |
  • 47 | Due date: {{ Accountant::formatDate($invoice->due_date, 'Y/m/d') }} 48 |
  • 49 | @endif 50 |
51 |
52 |
53 |
54 |
Status
55 |
56 |
57 |
58 |
    59 |
  • 60 | Status: 61 | @if ($invoice->paid) 62 | Paid 63 | @else 64 | Awaiting payment. 65 | @endif 66 |
  • 67 | @if ($invoice->charge && $invoice->paid) 68 |
  • 69 | Payment: {{ $invoice->charge }} 70 |
  • 71 | @endif 72 |
73 |
74 | @if ($invoice->attempt_count > 1 && $invoice->paid) 75 | 78 | @endif 79 |
80 |
81 |
Items
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | @foreach ($invoice->items as $item) 94 | 95 | 96 | 97 | 98 | 99 | @endforeach 100 | 101 |
AmountDescriptionPeriod
${{ Accountant::formatAmount($item->amount) }}Subscription to {{ Accountant::planToReadable($item->plan) }}{{ Accountant::formatDate($item->period->start, 'Y/m/d') }} to {{ Accountant::formatDate($item->period->end, 'Y/m/d') }}
102 |
103 |
104 |
Summary
105 | 125 |
126 |
127 | @endsection -------------------------------------------------------------------------------- /resources/views/partials/invoices.blade.php: -------------------------------------------------------------------------------- 1 |
Invoices
2 |
3 | @foreach ($invoices->take(5) as $invoice) 4 | 5 | ${{ Accountant::formatAmount($invoice->total)}} 6 | @if ($invoice->paid) 7 | Paid 8 | @else 9 | Unpaid 10 | @endif 11 | {{ $invoice->customer }} 12 | 13 | 14 | 15 | @endforeach 16 | @if ($invoices->count() > 5) 17 | 18 | View all invoices 19 | 20 | @endif 21 |
-------------------------------------------------------------------------------- /resources/views/partials/pagination.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 17 | @endif 18 | -------------------------------------------------------------------------------- /resources/views/subscriptions/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @foreach ($subscriptions as $subscription) 17 | 18 | 19 | 20 | 21 | 22 | 23 | @endforeach 24 | 25 |
CustomerBillingPlanCreated
{{ $subscription->customer->email }}{{ $subscription->billing == 'charge_automatically' ? 'Auto' : 'Send' }}{{ $subscription->plan->name }}{{ Carbon\Carbon::createFromTimestamp($subscription->created)->format('Y/m/d h:i:s') }}
26 |
27 | {{ $subscriptions->links('accountant::partials.pagination') }} 28 |
29 |
30 |
31 | @endsection -------------------------------------------------------------------------------- /resources/views/subscriptions/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('accountant::app') 2 | 3 | @section('content') 4 |
5 |
6 |

{{ $subscription->customer->email }} on {{ $subscription->plan->name }}

7 | @if ($subscription->status == 'active') 8 |
9 | Active 10 | @if ($subscription->cancel_at_period_end) 11 | - Will end at {{ Accountant::formatDate($subscription->current_period_end) }} 12 | @endif 13 |
14 | @endif 15 |
16 |
17 |
Details
18 |
19 |
20 |
21 |
    22 |
  • 23 | ID: {{ $subscription->id }} 24 |
  • 25 |
  • 26 | Customer: {{ $subscription->customer->email }} 27 |
  • 28 |
  • 29 | Plan: {{ $subscription->plan->name }} (${{ Accountant::formatAmount($subscription->plan->amount) . ($subscription->plan->interval_count > 1 ? ' every ' . str_plural($subscription->plan->interval) : '/' . $subscription->plan->interval) }}) 30 |
  • 31 |
  • 32 | Quantity: {{ $subscription->quantity }} 33 |
  • 34 |
35 |
36 |
37 |
    38 |
  • 39 | Current period: {{ Accountant::formatDate($subscription->current_period_start, 'Y/m/d') }} to {{ Accountant::formatDate($subscription->current_period_end, 'Y/m/d') }} 40 |
  • 41 |
  • 42 | Created: {{ Accountant::formatDate($subscription->created, 'Y/m/d') }} 43 |
  • 44 |
  • 45 | Tax percent: {!! $subscription->tax_percent > 0 ? $subscription->tax_percent : 'No tax applied' !!} 46 |
  • 47 |
  • 48 | Billing: {{ $subscription->billing == 'charge_automatically' ? 'Charge automatically' : 'Send invoice' }} 49 |
  • 50 | @if ($subscription->days_until_due) 51 |
  • 52 | Days until due: {{ $subscription->days_until_due }} 53 |
  • 54 | @endif 55 |
56 |
57 |
58 |
59 | @includeWhen($subscription->invoices, 'accountant::partials.invoices', ['invoices' => $subscription->invoices]) 60 |
61 |
62 | @endsection -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | config('accountant.middleware', 'auth')], function () { 6 | // Balance routes... 7 | Route::get('/balance', 'BalanceController@index'); 8 | 9 | // Charge routes... 10 | Route::get('/charges', 'ChargeController@index'); 11 | Route::get('/charges/{id}', 'ChargeController@show'); 12 | Route::get('/charges/{type?}/{id?}', 'ChargeController@index'); 13 | 14 | // Customer routes... 15 | Route::get('/customers', 'CustomerController@index'); 16 | Route::get('/customers/{id}', 'CustomerController@show'); 17 | 18 | // Subscription routes... 19 | Route::get('/subscriptions', 'SubscriptionController@index'); 20 | Route::get('/subscriptions/{id}', 'SubscriptionController@show'); 21 | 22 | // Invoice routes... 23 | Route::get('/invoices', 'InvoiceController@index'); 24 | Route::get('/invoices/{id}', 'InvoiceController@show'); 25 | Route::get('/invoices/{type?}/{id?}', 'InvoiceController@index'); 26 | 27 | Route::get('/', 'HomeController@index'); 28 | Route::post('/data', 'HomeController@data'); 29 | Route::get('/refresh', 'HomeController@refresh'); 30 | Route::get('/filters', 'HomeController@filters'); 31 | Route::get('{view}', 'HomeController@index')->where('view', '(.*)'); 32 | }); 33 | -------------------------------------------------------------------------------- /src/Accountant.php: -------------------------------------------------------------------------------- 1 | format($format); 31 | } 32 | 33 | /** 34 | * Format the plan name and interval into a readable string. 35 | * 36 | * @param $plan 37 | * @return string 38 | */ 39 | public function planToReadable($plan) 40 | { 41 | $interval = $plan->interval_count > 1 ? ' every '.$plan->interval_count.' '.str_plural($plan->interval) : '/'.$plan->interval; 42 | 43 | return "{$plan->name} (\${$this->formatAmount($plan->amount)}{$interval})"; 44 | } 45 | 46 | /** 47 | * Check if the "refresh" file exists. 48 | * 49 | * @return bool 50 | */ 51 | public function isCacheRefreshing() 52 | { 53 | return File::exists(storage_path('laravel-accountant/refresh')); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/AccountantServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 18 | Listeners\CreateRefreshFile::class, 19 | Listeners\ValidateCache::class, 20 | ], 21 | Events\CacheRefreshStopped::class => [ 22 | Listeners\RemoveRefreshFile::class, 23 | ], 24 | ]; 25 | 26 | /** 27 | * Bootstrap any application services. 28 | * 29 | * @return void 30 | */ 31 | public function boot() 32 | { 33 | $this->registerEvents(); 34 | $this->registerRoutes(); 35 | $this->registerResources(); 36 | $this->defineAssetPublishing(); 37 | } 38 | 39 | /** 40 | * Register any application services. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->mergeConfigFrom( 47 | __DIR__.'/../config/accountant.php', 'accountant' 48 | ); 49 | 50 | $this->offerPublishing(); 51 | } 52 | 53 | /** 54 | * Register the Horizon job events. 55 | * 56 | * @return void 57 | */ 58 | protected function registerEvents() 59 | { 60 | $events = $this->app->make(Dispatcher::class); 61 | 62 | foreach ($this->events as $event => $listeners) { 63 | foreach ($listeners as $listener) { 64 | $events->listen($event, $listener); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Register the Accountant routes. 71 | * 72 | * @return void 73 | */ 74 | protected function registerRoutes() 75 | { 76 | Route::group([ 77 | 'prefix' => 'accountant', 78 | 'namespace' => 'TomIrons\Accountant\Http\Controllers', 79 | 'middleware' => 'web', 80 | ], function () { 81 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 82 | }); 83 | } 84 | 85 | /** 86 | * Register the Accountant resources. 87 | * 88 | * @return void 89 | */ 90 | protected function registerResources() 91 | { 92 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'accountant'); 93 | } 94 | 95 | /** 96 | * Define the asset publishing configuration. 97 | * 98 | * @return void 99 | */ 100 | public function defineAssetPublishing() 101 | { 102 | $this->publishes([ 103 | __DIR__.'/../public' => public_path('vendor/accountant'), 104 | ], 'accountant-assets'); 105 | } 106 | 107 | /** 108 | * Setup the resource publishing groups. 109 | * 110 | * @return void 111 | */ 112 | protected function offerPublishing() 113 | { 114 | if ($this->app->runningInConsole()) { 115 | $this->publishes([ 116 | __DIR__.'/../config/accountant.php' => config_path('accountant.php'), 117 | ], 'accountant-config'); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Cabinet.php: -------------------------------------------------------------------------------- 1 | driver = Cache::driver(config('accountant.config.driver', 'file')); 52 | 53 | $this->validate(); 54 | } 55 | 56 | /** 57 | * Check the cache if each type exists, fire the refresh event if it doesn't. 58 | * 59 | * @return void 60 | */ 61 | protected function validate() 62 | { 63 | if (! Accountant::isCacheRefreshing()) { 64 | foreach ($this->types as $type) { 65 | if (! $this->driver->has('accountant.'.$type)) { 66 | event(new CacheRefreshStarted($this->types)); 67 | break; 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Delete all data from the cache. 75 | * 76 | * @return void 77 | */ 78 | public function empty() 79 | { 80 | $this->driver->deleteMultiple(array_map(function ($type) { 81 | return 'accountant.'.$type; 82 | }, $this->types)); 83 | } 84 | 85 | /** 86 | * Set the start and end dates using the session or default values. 87 | * 88 | * @return $this 89 | */ 90 | public function setDates() 91 | { 92 | $this->start = session('accountant.start', Carbon::now()->subMonth()); 93 | $this->end = session('accountant.end', Carbon::now()); 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Return the collection of a specific type from the cache. 100 | * 101 | * @param string $type 102 | * @return Collection 103 | */ 104 | public function search($type) 105 | { 106 | return collect($this->driver->get('accountant.'.$type)) 107 | ->where('created', '>=', $this->start->startOfDay()->getTimestamp()) 108 | ->where('created', '<=', $this->end->endOfDay()->getTimestamp()); 109 | } 110 | 111 | /** 112 | * Get collection of days to use for the chart depending on the start/end dates. 113 | * 114 | * @return Collection 115 | */ 116 | protected function days() 117 | { 118 | if ($this->start->gt($this->end)) { 119 | return; 120 | } 121 | 122 | $interval = CarbonInterval::day(); 123 | $periods = new DatePeriod($this->start->copy(), $interval, $this->end->copy()); 124 | 125 | foreach ($periods as $period) { 126 | $days[] = Carbon::createFromDate($period->format('Y'), $period->format('m'), $period->format('d')); 127 | } 128 | 129 | return collect($days); 130 | } 131 | 132 | /** 133 | * Get collection of months to use for the chart depending on the start/end dates. 134 | * 135 | * @return Collection 136 | */ 137 | protected function months() 138 | { 139 | if ($this->start->gt($this->end)) { 140 | return; 141 | } 142 | 143 | $interval = CarbonInterval::month(); 144 | $periods = new DatePeriod($this->start->copy()->startOfMonth(), $interval, $this->end->copy()->endOfMonth()); 145 | 146 | foreach ($periods as $period) { 147 | $months[] = $period->format('M Y'); 148 | } 149 | 150 | return collect($months); 151 | } 152 | 153 | /** 154 | * Return array of data for the gross chart. 155 | * 156 | * @return array 157 | */ 158 | protected function grossData() 159 | { 160 | $charges = $this->filterUnuccessfulCharges(); 161 | 162 | return $this->days()->mapToGroups(function ($day) use ($charges) { 163 | return [$day->format('M Y') => $this->sumChargesOnDay($charges, $day)]; 164 | }); 165 | } 166 | 167 | /** 168 | * Return array of data for the payments chart. 169 | * 170 | * @return array 171 | */ 172 | protected function paymentsData() 173 | { 174 | $charges = $this->filterUnuccessfulCharges(); 175 | 176 | return $this->days()->mapToGroups(function ($day) use ($charges) { 177 | return [$day->format('M Y') => $charges->filter(function ($charge) use ($day) { 178 | return $charge->created >= $day->startOfDay()->getTimestamp() 179 | && $charge->created <= $day->endOfDay()->getTimestamp(); 180 | })->count()]; 181 | }); 182 | } 183 | 184 | /** 185 | * Return array of data for the customers chart. 186 | * 187 | * @return array 188 | */ 189 | protected function customersData() 190 | { 191 | $customers = $this->search('customer'); 192 | 193 | return $this->days()->mapToGroups(function ($day) use ($customers) { 194 | return [$day->format('M Y') => $customers->filter(function ($customer) use ($day) { 195 | return $customer->created >= $day->startOfDay()->getTimestamp() 196 | && $customer->created <= $day->endOfDay()->getTimestamp(); 197 | })->count()]; 198 | }); 199 | } 200 | 201 | /** 202 | * Return array of data for the refunded chart. 203 | * 204 | * @return array 205 | */ 206 | protected function refundedData() 207 | { 208 | $charges = $this->search('charge'); 209 | 210 | return $this->days()->mapToGroups(function ($day) use ($charges) { 211 | return [$day->format('M Y') => $charges->filter(function ($charge) use ($day) { 212 | return $charge->created >= $day->startOfDay()->getTimestamp() 213 | && $charge->created <= $day->endOfDay()->getTimestamp(); 214 | })->sum(function ($charge) { 215 | return $charge->amount_refunded / 100; 216 | })]; 217 | }); 218 | } 219 | 220 | /** 221 | * Filter out all unsuccessful charges. 222 | * 223 | * @return Collection 224 | */ 225 | protected function filterUnuccessfulCharges() 226 | { 227 | return $this->search('charge')->filter(function ($charge) { 228 | return $charge->paid && $charge->status == 'succeeded'; 229 | }); 230 | } 231 | 232 | /** 233 | * Calculate the sum of all charges for a specific day. 234 | * 235 | * @param $charges 236 | * @param $day 237 | * @return string 238 | */ 239 | protected function sumChargesOnDay($charges, $day) 240 | { 241 | return Accountant::formatAmount($charges->filter(function ($charge) use ($day) { 242 | return $this->isChargeOnDay($charge, $day); 243 | })->sum->amount); 244 | } 245 | 246 | /** 247 | * Check if the charge was made on a specific day. 248 | * 249 | * @param $charge 250 | * @param $day 251 | * @return bool 252 | */ 253 | protected function isChargeOnDay($charge, $day) 254 | { 255 | return $charge->created >= $day->startOfDay()->getTimestamp() 256 | && $charge->created <= $day->endOfDay()->getTimestamp(); 257 | } 258 | 259 | /** 260 | * Return array of data for a specific chart. 261 | * 262 | * @param $name 263 | * @return array 264 | */ 265 | protected function chartData($name) 266 | { 267 | if (! method_exists($this, $method = $name.'Data')) { 268 | throw new BadMethodCallException("Method [$method] doesn't exist in this class."); 269 | } 270 | 271 | return [ 272 | 'labels' => $this->months()->toArray(), 273 | 'datasets' => [ 274 | 0 => [ 275 | 'backgroundColor' => 'rgba(59,141,236, .3)', 276 | 'borderColor' => 'rgb(59,141,236)', 277 | 'data' => $this->$method()->map(function ($item) { 278 | return number_format($item->sum(), 2); 279 | })->values(), 280 | ], 281 | ], 282 | ]; 283 | } 284 | 285 | /** 286 | * Generate an array of data. 287 | * 288 | * @return array 289 | */ 290 | public function generate() 291 | { 292 | $charges = $this->search('charge'); 293 | $customers = $this->search('customer'); 294 | $subscriptions = $this->search('subscription'); 295 | 296 | return [ 297 | 'charts' => [ 298 | 'customers' => $this->chartData('customers'), 299 | 'gross' => $this->chartData('gross'), 300 | 'payments' => $this->chartData('payments'), 301 | 'refunded' => $this->chartData('refunded'), 302 | ], 303 | 'charges' => [ 304 | 'count' => $charges->count(), 305 | 'refunded' => Accountant::formatAmount($charges->sum->amount_refunded), 306 | 'successful' => $charges->filter(function ($charge) { 307 | return $charge->paid && $charge->status == 'succeeded'; 308 | })->count(), 309 | 'total' => Accountant::formatAmount($charges->filter(function ($charge) { 310 | return $charge->paid && $charge->status == 'succeeded'; 311 | })->sum->amount), 312 | ], 313 | 'customers' => [ 314 | 'count' => $customers->count(), 315 | ], 316 | 'subscriptions' => [ 317 | 'revenue' => $subscriptions->filter(function ($subscription) { 318 | return ! in_array($subscription->status, ['canceled', 'trialing']) 319 | && $subscription->ended_at == null 320 | && $subscription->quantity > 0; 321 | })->sum(function ($subscription) { 322 | return $subscription->plan->amount; 323 | }), 324 | ], 325 | ]; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | methods || in_array($method, $this->methods)) { 52 | return $this->getStripeClass()::$method(...$args); 53 | } 54 | 55 | throw new BadMethodCallException("Method [{$method}] doesn't exist or is not in the list of allowed methods."); 56 | } 57 | 58 | /** 59 | * Gets the name of the Stripe Client name. 60 | * 61 | * @return string 62 | */ 63 | abstract public function getClientName(): string; 64 | 65 | /** 66 | * Return the stripe class for the client. 67 | * 68 | * @return \Illuminate\Foundation\Application|mixed 69 | */ 70 | public function getStripeClass() 71 | { 72 | return app('Stripe\\'.studly_case($this->getClientName())); 73 | } 74 | 75 | /** 76 | * Set the data for the pagination. 77 | * 78 | * @param array $params 79 | * @return $this 80 | */ 81 | public function list(array $params = []) 82 | { 83 | $this->data = $this->getStripeClass()::all(array_merge($params, [ 84 | 'limit' => $this->limit(), 85 | 'ending_before' => $this->end(), 86 | 'starting_after' => $this->start(), 87 | ])); 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Paginate the results. 94 | * 95 | * @param string $path 96 | * @param string $query 97 | * @return Paginator 98 | */ 99 | public function paginate($path, $query): Paginator 100 | { 101 | if ($this->data->object !== 'list' || ! is_array($this->data->data)) { 102 | throw new LogicException("Object must be a 'list' in order to paginate."); 103 | } 104 | 105 | $collection = new Collection($this->data->data); 106 | 107 | $this->points($collection->first()->id, $collection->last()->id); 108 | 109 | return new Paginator( 110 | $collection, 111 | $this->limit(), 112 | $this->currentPage(), 113 | compact('path', 'query') + [ 114 | 'morePages' => $this->getStripeClass()->all([ 115 | 'starting_after' => $collection->last()->id, 116 | ])->has_more, 117 | ] 118 | ); 119 | } 120 | 121 | /** 122 | * Get the 'starting_after' value for the API call. 123 | * 124 | * @return string 125 | */ 126 | protected function start() 127 | { 128 | return request('start', null); 129 | } 130 | 131 | /** 132 | * Get the 'ending_before' value for the API call. 133 | * 134 | * @return string 135 | */ 136 | protected function end() 137 | { 138 | return request('end', null); 139 | } 140 | 141 | /** 142 | * Get the number of items shown per page. 143 | * 144 | * @return int 145 | */ 146 | protected function limit(): int 147 | { 148 | return config('accountant.pagination.limit', 10); 149 | } 150 | 151 | /** 152 | * Get / set the start and end points. 153 | * 154 | * @param string $start 155 | * @param string|null $end 156 | */ 157 | protected function points($start, $end = null) 158 | { 159 | if (str_contains($start, ['start', 'end'])) { 160 | return session()->get('accountant.api.'.$start); 161 | } 162 | 163 | session()->put([ 164 | 'accountant.api.start' => $end, 165 | 'accountant.api.end' => $start, 166 | ]); 167 | } 168 | 169 | /** 170 | * Get / set the current page. 171 | * 172 | * @param string|int $page 173 | * @return $this 174 | */ 175 | public function currentPage($page = null) 176 | { 177 | if (is_null($page)) { 178 | return $this->currentPage; 179 | } 180 | 181 | $this->currentPage = $page; 182 | 183 | return $this; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/ClientFactory.php: -------------------------------------------------------------------------------- 1 | app = $app; 24 | } 25 | 26 | /** 27 | * Magic method for retrieving a client. 28 | * 29 | * @param $method 30 | * @return mixed 31 | */ 32 | public function __get($method) 33 | { 34 | $class = __NAMESPACE__.'\\Clients\\'.studly_case($method); 35 | 36 | return $this->app->make($class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Clients/Balance.php: -------------------------------------------------------------------------------- 1 | $id, 40 | ])->data); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Events/CacheRefreshStarted.php: -------------------------------------------------------------------------------- 1 | types = $types; 32 | $this->filesystem = new Filesystem; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Events/CacheRefreshStopped.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Facades/Accountant.php: -------------------------------------------------------------------------------- 1 | client->balance(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Http/Controllers/ChargeController.php: -------------------------------------------------------------------------------- 1 | $id] : []; 21 | $charges = $this->factory->charge->list($parameters) 22 | ->currentPage($request->get('page', 1)) 23 | ->paginate($request->url(), $request->query()); 24 | 25 | return view('accountant::charges.index', compact('charges')); 26 | } 27 | 28 | /** 29 | * Show information about a charge. 30 | * 31 | * @param Request $request 32 | * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 33 | */ 34 | public function show($id) 35 | { 36 | $charge = $this->factory->charge->retrieve($id); 37 | 38 | if ($charge->balance_transaction) { 39 | $charge->balance = $this->factory->balance_transaction->retrieve($charge->balance_transaction); 40 | } 41 | 42 | return view('accountant::charges.show', compact('charge')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | cabinet = $cabinet; 33 | $this->factory = $factory; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Controllers/CustomerController.php: -------------------------------------------------------------------------------- 1 | factory->customer->list() 19 | ->currentPage($request->get('page', 1)) 20 | ->paginate($request->url(), $request->query()); 21 | 22 | foreach ($customers as $customer) { 23 | $customer->card = (new Collection($customer->sources->data))->first(); 24 | } 25 | 26 | return view('accountant::customers.index', compact('customers')); 27 | } 28 | 29 | /** 30 | * Show information about a specific customer. 31 | * 32 | * @param $id 33 | * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 34 | */ 35 | public function show($id) 36 | { 37 | $customer = $this->factory->customer->retrieve($id); 38 | $customer->cards = new Collection($customer->sources->all([ 39 | 'object' => 'card', 40 | ])->data); 41 | $customer->subscriptions = new Collection($customer->subscriptions->data); 42 | $customer->invoices = new Collection($customer->invoices()->data); 43 | 44 | return view('accountant::customers.show', compact('customer')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | with([ 18 | 'stats' => $this->data(new Request), 19 | ]); 20 | } 21 | 22 | /** 23 | * Refresh all data. 24 | * 25 | * @return \Illuminate\Http\RedirectResponse 26 | */ 27 | public function refresh() 28 | { 29 | $this->cabinet->empty(); 30 | 31 | return redirect()->back(); 32 | } 33 | 34 | /** 35 | * Generate array based on the start and end date. 36 | * 37 | * @param Request $request 38 | * @return array 39 | */ 40 | public function data(Request $request) 41 | { 42 | if ($request->has(['start', 'end'])) { 43 | session()->put([ 44 | 'accountant.start' => Carbon::createFromTimestamp($request->get('start')), 45 | 'accountant.end' => Carbon::createFromTimestamp($request->get('end')), 46 | ]); 47 | } 48 | 49 | return $this->cabinet->setDates()->generate(); 50 | } 51 | 52 | /** 53 | * Return the dates for the date filter. 54 | * 55 | * @return array 56 | */ 57 | public function filters() 58 | { 59 | return [ 60 | 'start' => session()->get('accountant.start', Carbon::now()->subMonth())->format('m/d/Y'), 61 | 'end' => session()->get('accountant.end', Carbon::now())->format('m/d/Y'), 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Http/Controllers/InvoiceController.php: -------------------------------------------------------------------------------- 1 | $id] : []; 21 | $invoices = $this->factory->invoice->list($parameters) 22 | ->currentPage($request->get('page', 1)) 23 | ->paginate($request->url(), $request->query()); 24 | 25 | return view('accountant::invoices.index', compact('invoices')); 26 | } 27 | 28 | /** 29 | * Show information about a specific invoice. 30 | * 31 | * @param $id 32 | */ 33 | public function show($id) 34 | { 35 | $invoice = $this->factory->invoice->retrieve($id); 36 | $invoice->customer = $this->factory->customer->retrieve($invoice->customer); 37 | $invoice->items = new Collection($invoice->lines->all()->data); 38 | 39 | return view('accountant::invoices.show', compact('invoice')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/Controllers/SubscriptionController.php: -------------------------------------------------------------------------------- 1 | factory->subscription->list() 18 | ->currentPage($request->get('page', 1)) 19 | ->paginate($request->url(), $request->query()); 20 | 21 | foreach ($subscriptions as $subscription) { 22 | $subscription->customer = $this->factory->customer->retrieve($subscription->customer); 23 | } 24 | 25 | return view('accountant::subscriptions.index', compact('subscriptions')); 26 | } 27 | 28 | /** 29 | * Show information about a specific subscription. 30 | * 31 | * @param $id 32 | * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View 33 | */ 34 | public function show($id) 35 | { 36 | $subscription = $this->factory->subscription->retrieve($id); 37 | $subscription->customer = $this->factory->customer->retrieve($subscription->customer); 38 | $subscription->invoices = $this->factory->subscription->invoices($id); 39 | 40 | return view('accountant::subscriptions.show', compact('subscription')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Jobs/PutToCache.php: -------------------------------------------------------------------------------- 1 | type = $type; 41 | $this->types = $types; 42 | } 43 | 44 | /** 45 | * Add new collection of data to the cache. 46 | * 47 | * @return void 48 | */ 49 | public function handle(ClientFactory $factory, $data = []) 50 | { 51 | $driver = Cache::driver(config('accountant.config.driver', 'file')); 52 | $items = $factory->{$this->type}->all(); 53 | 54 | $driver->add('accountant.'.$this->type, iterator_to_array($items->autoPagingIterator()), config('accountant.cache.time', 60)); 55 | 56 | if ($this->finished()) { 57 | event(new CacheRefreshStopped); 58 | } 59 | } 60 | 61 | /** 62 | * Check if all data exists. 63 | * 64 | * @return bool 65 | */ 66 | private function finished() 67 | { 68 | return empty(array_filter($this->types, function ($type) { 69 | return ! cache()->has('accountant.'.$type); 70 | })); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Listeners/CreateRefreshFile.php: -------------------------------------------------------------------------------- 1 | filesystem->exists($path)) { 20 | $event->filesystem->makeDirectory($path); 21 | } 22 | 23 | $event->filesystem->put($path.'/refresh', null); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Listeners/RemoveRefreshFile.php: -------------------------------------------------------------------------------- 1 | filesystem->deleteDirectory(storage_path('laravel-accountant')); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Listeners/ValidateCache.php: -------------------------------------------------------------------------------- 1 | types) 19 | ->reject(function ($type) { 20 | return cache()->has('accountant.'.$type); 21 | }) 22 | ->each(function ($type) use ($event) { 23 | dispatch(new PutToCache($type, $event->types))->onQueue(config('accountant.queue')); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Paginator.php: -------------------------------------------------------------------------------- 1 | resetQuery(); 17 | 18 | if ($this->currentPage() > 2) { 19 | $this->appends('end', session()->get('accountant.api.end')); 20 | } 21 | 22 | return parent::previousPageUrl(); 23 | } 24 | 25 | /** 26 | * Get the URL for the next page. 27 | * 28 | * @return string|null 29 | */ 30 | public function nextPageUrl() 31 | { 32 | $this->resetQuery(); 33 | 34 | if ($this->items->count() == $this->perPage()) { 35 | $this->appends('start', session()->get('accountant.api.start')); 36 | 37 | return $this->url($this->currentPage() + 1); 38 | } 39 | } 40 | 41 | /** 42 | * Remove start and end points from the query. 43 | */ 44 | protected function resetQuery() 45 | { 46 | array_forget($this->query, ['start', 'end']); 47 | } 48 | 49 | /** 50 | * Determine if there are enough items to split into multiple pages. 51 | * 52 | * @return null|string 53 | */ 54 | public function hasPages() 55 | { 56 | return $this->nextPageUrl() || $this->previousPageUrl(); 57 | } 58 | 59 | /** 60 | * Determine if there are more items in the data source. 61 | * 62 | * @return null|string 63 | */ 64 | public function hasMorePages() 65 | { 66 | return $this->morePages; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/ClientTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix 15 | .setPublicPath('public') 16 | .js('resources/assets/js/app.js', 'public/js') 17 | .sass('resources/assets/sass/app.scss', 'public/css') 18 | .copy('resources/assets/img', 'public/img') 19 | .copy('resources/assets/fonts', 'public/fonts') 20 | .sourceMaps() 21 | .copy('public', '../../Sites/laravel-55/public/vendor/accountant'); 22 | 23 | if (mix.inProduction()) { 24 | mix.version(); 25 | } --------------------------------------------------------------------------------