├── .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 | [](https://packagist.org/packages/tomirons/laravel-accountant)
3 | [](https://packagist.org/packages/tomirons/laravel-accountant)
4 | [](https://travis-ci.org/tomirons/laravel-accountant)
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Gross Volume
77 | ${{ data.charges.total }}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Successful Payments
87 | {{ data.charges.successful }}
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
Total Refunded
97 | ${{ data.charges.refunded }}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
New Customers
107 | {{ data.customers.count }}
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/resources/assets/js/components/DateRangeComponent.vue:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | to
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
Show between {{ start.format('MM/DD/YYYY') }} and {{ end.format('MM/DD/YYYY') }}
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/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 | Amount |
11 | Description |
12 | Date |
13 |
14 |
15 |
16 | @foreach ($charges as $charge)
17 |
18 |
19 | @if ($charge->refunded)
20 | Refunded
21 | @elseif (!$charge->paid)
22 | Failed
23 | @else
24 | Paid
25 | @endif
26 | |
27 | ${{ Accountant::formatAmount($charge->amount) }} |
28 | {{ $charge->description ? "{$charge->description} - " : null }}{{ $charge->id }} |
29 | {{ Accountant::formatDate($charge->created) }} |
30 |
31 | @endforeach
32 |
33 |
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 |
90 | @foreach ($charge->refunds->data as $refund)
91 | -
92 |
93 |
94 |
95 | -
96 | ID: {{ $refund->id }}
97 |
98 | -
99 | Amount: ${{ Accountant::formatAmount($refund->amount) }} {{ $refund->currency }}
100 |
101 |
102 |
103 |
104 |
105 | -
106 | Reason: {{ $refund->reason ? ucfirst(str_replace('_', ' ', $refund->reason)) : 'Other' }}
107 |
108 | -
109 | Date: {{ Accountant::formatDate($refund->created) }}
110 |
111 |
112 |
113 |
114 |
115 | @endforeach
116 |
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 | Email |
10 | Card |
11 | Created |
12 |
13 |
14 |
15 | @foreach ($customers as $customer)
16 |
17 | {{ $customer->email }} - {{ $customer->id }} |
18 | {{ $customer->card->brand }} - {{ $customer->card->last4 }} - {{ $customer->card->exp_month }}/{{ $customer->card->exp_year }} |
19 | {{ Accountant::formatDate($customer->created) }} |
20 |
21 | @endforeach
22 |
23 |
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 | Amount |
11 | Number |
12 | Customer |
13 | Due |
14 | Created |
15 |
16 |
17 |
18 | @foreach ($invoices as $invoice)
19 |
20 |
21 | @if ($invoice->paid)
22 | Paid
23 | @else
24 | Unpaid
25 | @endif
26 | |
27 | ${{ Accountant::formatAmount($invoice->total) }} |
28 | {{ $invoice->number ? $invoice->number : $invoice->id }} |
29 | {{ $invoice->customer }} |
30 | {{ $invoice->due_date ? Accountant::formatDate($invoice->due_date, 'Y/m/d') : '-' }} |
31 | {{ Accountant::formatDate($invoice->date, 'Y/m/d') }} |
32 |
33 | @endforeach
34 |
35 |
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 | Amount |
88 | Description |
89 | Period |
90 |
91 |
92 |
93 | @foreach ($invoice->items as $item)
94 |
95 | ${{ Accountant::formatAmount($item->amount) }} |
96 | Subscription to {{ Accountant::planToReadable($item->plan) }} |
97 | {{ Accountant::formatDate($item->period->start, 'Y/m/d') }} to {{ Accountant::formatDate($item->period->end, 'Y/m/d') }} |
98 |
99 | @endforeach
100 |
101 |
102 |
103 |
104 |
Summary
105 |
106 | @if ($invoice->tax)
107 | -
108 | Tax
109 | ${{ Accountant::formatAmount($invoice->tax) }}
110 |
111 | @endif
112 | -
113 | Subtotal
114 | ${{ Accountant::formatAmount($invoice->subtotal) }}
115 |
116 | -
117 | Total
118 | ${{ Accountant::formatAmount($invoice->total) }}
119 |
120 | -
121 | Amount Due
122 | ${{ Accountant::formatAmount($invoice->amount_due) }}
123 |
124 |
125 |
126 |
127 | @endsection
--------------------------------------------------------------------------------
/resources/views/partials/invoices.blade.php:
--------------------------------------------------------------------------------
1 | Invoices
2 |
--------------------------------------------------------------------------------
/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 | Customer |
10 | Billing |
11 | Plan |
12 | Created |
13 |
14 |
15 |
16 | @foreach ($subscriptions as $subscription)
17 |
18 | {{ $subscription->customer->email }} |
19 | {{ $subscription->billing == 'charge_automatically' ? 'Auto' : 'Send' }} |
20 | {{ $subscription->plan->name }} |
21 | {{ Carbon\Carbon::createFromTimestamp($subscription->created)->format('Y/m/d h:i:s') }} |
22 |
23 | @endforeach
24 |
25 |
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 | }
--------------------------------------------------------------------------------