Compare commits

..

187 Commits
test ... master

Author SHA1 Message Date
3c7e2bbbc9 Move DB queries into jobs, so that the scheduler and artisan command calls doesnt evaluate them until the job is actually run
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-06 20:01:42 +10:00
844d509834 Move invoice blades around. Added invoices in credit view
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Failing after 28s
Create Docker Image / Final Docker Image Manifest (push) Has been skipped
2024-07-06 19:28:14 +10:00
f458bb19c5 Put back daily schedule job to collect broadband traffic 2024-07-06 11:16:18 +10:00
76d43a81c8 Temporarily fix invoice emailing 2024-07-06 11:06:07 +10:00
b3d5bf05a9 Fix handling of discounts in invoiceSummary(), added invoiceSummaryCredit() to show invoices in credit 2024-07-06 11:06:07 +10:00
f0ec35f463 Update laravel/leenooks module 2024-07-06 11:06:07 +10:00
326b1dcfc5 User optimisation and code cleanup 2024-07-06 11:06:07 +10:00
b6b036e06d Optimisations for resellers home page 2024-07-06 11:06:07 +10:00
e7ac329d24 Home page performance optimisations 2024-07-06 11:06:07 +10:00
648d941893 Code refactor work. New optimised query to get invoice status summary for an account 2024-07-06 11:06:07 +10:00
59dc825bf7 Update laravel framework from 9 to 11, removed some old packages 2024-07-06 11:06:07 +10:00
1b4504cee2 Fix dashboard SQL query error, doesnt work with postgres 2024-07-06 09:00:36 +10:00
74864e66cb Remove the error dump from a 500 message 2024-07-06 09:00:36 +10:00
c0cffc3e2c Framework update 2024-07-06 09:00:36 +10:00
4e450d4efc Updated for gitea 2024-07-06 09:00:36 +10:00
1374d38545 Redo database migrations for pgsql 2024-07-06 09:00:36 +10:00
7c91082ca8 Fix user creation in order - language_id doesnt have a default value 2024-02-02 10:58:29 +11:00
27720ee882 Telephone is now phone 2023-06-16 15:10:36 +10:00
8f283f83f2 Fix exception in AccountingInvoiceAdd 2023-05-14 21:39:54 +10:00
c1bb20dec0 Add event to process webhook payments 2023-05-13 23:51:27 +10:00
12b63a506f Minor bug fixes for payment update, internal product link and order billing interval 2023-05-13 22:19:22 +10:00
a195e4b55b Update to get an individual payment from intuit 2023-05-13 22:01:43 +10:00
8ad9e73abb Added intuit payments 2023-05-13 21:20:56 +10:00
Deon George
a518158ccf Framework update 2023-05-12 23:05:43 +10:00
Deon George
ad2f6f3a7f Inuit sync of tax, product accounting, accounts and invoices 2023-05-12 20:09:51 +10:00
Deon George
e2d8f8a096 Command to add accounts to accounting 2023-05-10 17:56:52 +09:00
Deon George
11b554daad Rename account_provider 2023-05-10 16:39:31 +09:00
Deon George
17ebbb71e8 Integration with Intuit - get accounting details for products 2023-05-10 12:59:42 +09:00
Deon George
dde11f73f5 Changed home screen to use account models instead of user model. Home screen now shows multiple accounts 2023-05-09 19:28:51 +09:00
Deon George
790ece14d1 Change product name_detail, name to name, pid 2023-05-09 17:12:07 +09:00
Deon George
45dd74aad4 Added product report, showing just active products and number of services 2023-05-09 16:50:39 +09:00
Deon George
b3539e6c7e Added Account report, renamed Product to Service List 2023-05-09 16:32:02 +09:00
Deon George
f3ecc12494 Only show intuit link to reseller users 2023-05-09 10:14:45 +09:00
Deon George
fe4bc3acef Deprecate price_overriden 2023-05-09 10:09:00 +09:00
a32e8e9d05 Added webhook to capture incoming webhooks 2023-05-06 21:48:46 +10:00
013bb632d3 Reimplmement service changes going to the service__change table 2023-05-06 17:21:56 +10:00
691180b3f0 More product cleanup 2023-05-06 13:53:50 +10:00
dc74a064ba Normalise usage of Model into Model::class strings 2023-05-05 16:29:57 +10:00
820ff2be00 More Product Model optimisation 2023-05-05 16:29:57 +10:00
96f799f535 Product Model optimisation 2023-05-05 10:51:28 +10:00
0f91ce4940 Updates to Product Model, product updates, enable pricing update, improved formating of product services 2023-05-04 22:21:14 +10:00
95bb55aad8 Optimize Groups 2023-05-04 11:50:54 +10:00
0ac35c3d43 Product class optimisation 2023-05-04 10:02:25 +10:00
a5238bfbdc No longer need to test for type, it should exist 2023-05-03 23:41:48 +10:00
25dab73a83 model/model_id is now required on products 2023-05-03 23:02:52 +10:00
72648ea14d Framework update, and moved markup() helper to new helpers.php 2023-05-03 18:24:14 +10:00
4f19da5987 Fix broadband plan change update 2023-05-03 18:09:29 +10:00
fd110f5c6f Minor bug fixes from live site 2023-05-01 17:18:12 +10:00
8fb888a395 Update upstream urls and framework update 2023-03-15 16:21:53 +11:00
10931bd156 Updated docker base image and synchronise consistent docker build/test 2023-03-15 16:21:53 +11:00
a6f01d0864 Fix display of supplier products and offerings - wasnt including all services 2023-03-15 16:21:53 +11:00
Deon George
b719efb58c Rework on product name/description and translate 2022-10-18 23:23:45 +11:00
Deon George
bfd17b0686 Fix invoices not being generated when price is null, service update rendering updates 2022-10-18 17:24:53 +11:00
Deon George
a87560ff96 Fix invoices being generated for suspended/external billing and zero price items 2022-10-18 15:58:14 +11:00
Deon George
c96d264c8f Enable creation of domains and domain service editing 2022-10-18 14:38:57 +11:00
Deon George
0b4e3a9341 Dont save payments with a zero alloc amount, delete payments with a zero alloc amount, fix javascript calculating payment balance 2022-10-18 10:23:11 +11:00
Deon George
6aa30f537b Fix invoice view when not logged in 2022-09-30 09:35:58 +10:00
Deon George
ffcab790fc Fix invoice emailing, and improved formatting 2022-09-30 08:21:16 +10:00
Deon George
b065d15f60 Service paid_to can be null if no invoices created yet 2022-09-30 08:21:15 +10:00
Deon George
319fa32754 Fix phone contract length display 2022-09-30 08:21:15 +10:00
Deon George
15a3b11d2e Enable editing of address for phone/broadband, fix billing start for phone 2022-09-30 08:21:15 +10:00
Deon George
ec99a5ff75 Add Compoships for multile key relationships, first implemented with Service::class 2022-09-30 08:21:15 +10:00
Deon George
2a19f14adb Fixes to emailing cancel requests. Changes to email jobs with no site_id 2022-09-29 12:49:38 +10:00
Deon George
1667b8c1df Fix broadband traffic import, when new services cannot be found. Fix broadband update so that start_at = connect_at 2022-09-29 11:22:16 +10:00
Deon George
5b4aa5c73e WIP: Fix invoice_next_at date on service update for broadband - must do the others, and create a service validator 2022-09-07 15:15:02 +10:00
Deon George
3cae12b256 Framework update, intuit update 2022-08-25 11:25:03 +10:00
Deon George
39db6303c2 Fix email generation and sending via CLI 2022-08-25 11:08:10 +10:00
Deon George
8955df84cd Editing product and enabled updating the accounting field 2022-08-20 23:35:41 +10:00
Deon George
8d920e1ba1 Some product rework 2022-08-20 23:01:03 +10:00
Deon George
71b252843c Show accounting link on accounts that are linked 2022-08-19 20:12:08 +10:00
Deon George
1deda523b4 Fix queue configuration, fix AccountingAccountSync description 2022-08-19 17:18:37 +10:00
Deon George
73d92f25c1 Added cost import via web 2022-08-19 15:12:56 +10:00
Deon George
798608cebd Fix site_id table references 2022-08-19 13:52:23 +10:00
Deon George
4b85e01e93 Customer account sync with Intuit 2022-08-19 13:52:23 +10:00
Deon George
c1a64e2094 Implement token refresh 2022-08-18 23:29:49 +10:00
Deon George
8fd79ce23e Initial integration with Quicken (Intuit API), rework oauth tables, update/test google login 2022-08-12 14:53:06 +10:00
Deon George
70571cb6ac Include ID in Supplier Account Sync and add supplier id integrity constraint 2022-08-11 11:04:35 +10:00
Deon George
f5d535daa7 Fix supplier_user being unique for each supplier and id, fix search looking for users with a supplier_id 2022-08-10 17:05:57 +10:00
Deon George
d358620b46 Update dreamscape api to get all domains 2022-08-10 16:10:08 +10:00
Deon George
e4c1305da5 Added Supplier Domain importing - using dreamscape API 2022-08-10 15:30:39 +10:00
Deon George
53c665787e Add SameSite attribute to cookies 2022-08-10 15:30:39 +10:00
Deon George
2722c92bcf Add supplier linking 2022-08-10 15:30:39 +10:00
Deon George
a1fd36aa6f Update supplier details to use form helpers, and added in api settings 2022-08-06 00:22:22 +10:00
Deon George
ae3f97d890 Fix supplier configuration 2022-08-05 22:21:17 +10:00
Deon George
17f07dafc0 Added leenooks/dreamscape 2022-08-05 17:08:22 +10:00
Deon George
20c91e8e31 Using search while the session has expired will generate a 401 2022-08-03 16:34:23 +10:00
Deon George
a52c20993b Fix existing cancel workflow 2022-08-03 15:47:09 +10:00
Deon George
dd76fda274 Hosting domain_name input, ensure we redirect back to the update page 2022-08-03 15:28:27 +10:00
Deon George
2bac177618 Some service host/domain updates, including schema updates 2022-08-02 22:36:35 +10:00
Deon George
06b1eca306 Updates to service updating - phone 2022-08-02 08:49:17 +10:00
Deon George
360182b6bb Fix old products report 2022-08-01 21:06:57 +10:00
Deon George
7feec266b8 Updates to service updating - broadband 2022-08-01 20:34:10 +10:00
Deon George
de3f1a534b Framework update 2022-08-01 15:00:56 +10:00
Deon George
97f5c84f23 Consolidate service resources into a top level services/ directory 2022-08-01 14:04:04 +10:00
Deon George
7f6df8d032 Update test to use standard docker image now 2022-07-29 16:31:32 +10:00
Deon George
39ded93a42 Update checkout, enable editing of checkout, show details on invoices 2022-07-29 16:31:32 +10:00
Deon George
4f7a27dd8d Framework update 2022-07-26 00:29:07 +10:00
Deon George
cc906e9b22 Fixes for viewing unprocessed charges 2022-07-25 23:59:28 +10:00
Deon George
bb44c1a216 Hack to make services active when changed after ordering 2022-07-25 23:50:46 +10:00
Deon George
3ff7bf1571 Fixes for error 500 in order and fix layout when updating changed services 2022-07-25 23:37:31 +10:00
Deon George
5297ae8a62 Enable editing of supplier products and listing services connected to them 2022-06-30 23:51:20 +10:00
Deon George
fb416306e7 Optimising Supplier Layout and source code placement 2022-06-28 23:20:56 +10:00
Deon George
464407e7ee Update leenooks/laravel and framework updates 2022-06-28 21:57:55 +10:00
Deon George
3723d644e6 Framework update, and include nunomaduro/laravel-console-summary 2022-06-28 18:11:57 +10:00
Deon George
f6f502618d Log sent emails 2022-06-25 00:18:57 +10:00
Deon George
b0a317d709 Fixes to cost display 2022-06-14 17:23:14 +10:00
Deon George
8ba6a93214 Fixes for testing to pass 2022-06-14 17:03:58 +10:00
Deon George
768744ad27 Initial support for importing supplier costs 2022-06-14 16:55:17 +10:00
Deon George
606f357839 Improved search performance 2022-06-14 16:51:18 +10:00
Deon George
8777024cd8 Enable email test 2022-06-13 20:55:39 +10:00
Deon George
642446e592 Cast Invoice::reminders to json 2022-06-13 19:58:53 +10:00
Deon George
d1fbb4b42b Count negative payment_items towards payment balance 2022-06-13 18:01:29 +10:00
Deon George
5e82f091c0 Payments need to be active to be recognised 2022-06-13 17:31:11 +10:00
Deon George
28438c6423 Fix for ezypay payment import 2022-06-13 16:44:24 +10:00
Deon George
49a4830d89 Fix payment entry 2022-06-13 15:46:38 +10:00
Deon George
9e889008bf Fix recording charges 2022-06-13 14:36:12 +10:00
Deon George
2590997b1a Bring back some services logic to still needed by charges 2022-06-13 14:21:48 +10:00
Deon George
c1080481ec Enabled changing broadband services and adjusting invoices 2022-06-12 18:32:54 +10:00
Deon George
c8162b8eb9 Fixes to CI testing 2022-06-12 18:32:19 +10:00
Deon George
849fd8d56b Framework update 2022-06-12 11:23:06 +10:00
Deon George
cc94426902 Optimising product category and category names 2022-06-12 11:21:20 +10:00
Deon George
360c1e46a1 Updates to charges table 2022-06-11 17:43:09 +10:00
Deon George
b8f85960aa Updates to charges table 2022-06-11 17:39:25 +10:00
Deon George
b9ec64fd4f Updates for new mail enviroment for laravel 9 2022-05-12 14:34:14 +10:00
Deon George
69e5a5f12c Add site() to SiteID trait, so site models can be referenced (eg: in email) 2022-05-12 12:45:18 +10:00
Deon George
2d7437fc0d Fix some missing date_* attributes for Service that have been missed 2022-05-12 09:09:49 +10:00
Deon George
03f37f33ff Fix authorisation for resellers to only see their own accounts 2022-04-22 16:35:01 +10:00
Deon George
16cc0c9f8d Optimise product tables 2022-04-22 16:01:41 +10:00
Deon George
e1a4db700f Removed redundant functions from Invoice, optimised Invoice tables 2022-04-22 16:01:41 +10:00
Deon George
a16277d9bb Cleanup usage of Leenooks\Carbon, change ServiceType to be consistent with Products/ Suppliers/ 2022-04-22 15:25:14 +10:00
Deon George
8ed9e38290 Removed redundant functions from Service::class 2022-04-22 15:25:13 +10:00
Deon George
d53643ef55 Removed many redundant functions from User::class 2022-04-22 15:25:13 +10:00
Deon George
796c72dd09 Home screen improvements, testing for role, work on user/account models 2022-04-22 14:41:53 +10:00
Deon George
40d12b906b Upgrade framework to laravel 9 2022-04-22 14:41:53 +10:00
Deon George
3fb6c0a052 Fixes identified by CI testing 2022-04-20 17:05:49 +10:00
Deon George
16b7e0b493 Update and fix broadband traffic 2022-04-20 16:28:54 +10:00
Deon George
621a132e35 Start work on updating services 2022-04-20 12:43:10 +10:00
Deon George
ebf08ea414 Fix error 500 when cancelling and ordering broadband 2022-04-04 20:29:45 +10:00
Deon George
9659621ba0 Added hosting report and enabled updating hosting details 2022-04-02 20:26:59 +11:00
Deon George
edc06e51fb Enable editing num accounts for email 2022-04-02 18:16:02 +11:00
Deon George
a4ed29b560 Added in email hosting, and other misc cosmetic fixes 2022-04-02 18:06:34 +11:00
Deon George
7775105da6 Rename supplier home screen to home 2022-04-02 09:56:30 +11:00
Deon George
d7b5d9a272 Fix VOIP services from 500 errors 2022-03-05 13:49:50 +11:00
Deon George
62f587d7e6 Fix invoice generation and other minor cosmetic items 2022-03-05 11:05:35 +11:00
Deon George
6dab5bded8 Query performance improvements for home 2022-02-02 17:12:17 +11:00
Deon George
e9ada2468e Minor fixes to get charging working 2022-02-02 16:01:12 +11:00
Deon George
db0ded44e0 Framework updates 2022-02-02 12:07:27 +11:00
Deon George
d1a7e399dc Fixes for CI/CD 2022-02-02 10:56:28 +11:00
Deon George
b9b4416737 More works on products 2022-02-02 10:43:59 +11:00
Deon George
1e9f15b40f Work on products, first completed broadband 2022-02-02 10:43:59 +11:00
Deon George
8f5293662e Comments on Middleware/Role 2022-02-02 10:43:59 +11:00
Deon George
d0a56de07e DB rework id, site_id and relations 2022-02-02 10:43:59 +11:00
Deon George
bdcfe07fb0 Created Suppliers 2022-02-01 16:45:35 +11:00
Deon George
0aa7ff3b2c Update leenooks/laravel and now using @js @css shortcuts 2022-02-01 16:45:35 +11:00
Deon George
b7b6a575bc Site related updates 2022-02-01 16:45:35 +11:00
Deon George
8d194c5523 Add link to account from service view 2021-12-20 17:24:31 +11:00
Deon George
05c5d35dbf Ensure users can only login from the right site 2021-12-20 17:24:31 +11:00
Deon George
99a62828f5 Rework Site and top level tables 2021-12-20 17:24:31 +11:00
Deon George
b4e569ccc8 Change docker base image 2021-12-17 15:02:48 +11:00
Deon George
48a76bacb6 Fix invoice creation with charges. Fix including discounts in invoices 2021-12-16 11:55:59 +11:00
Deon George
8a8e299c7b Fix when VOIP order doesnt have an address or number 2021-12-14 13:14:14 +11:00
Deon George
7908676063 Fix error when new order created and notes is blank 2021-12-14 12:38:45 +11:00
Deon George
6fa88daeb0
Minor fix to SearchController, updated composer.json based on other projects 2021-10-08 12:34:56 +11:00
Deon George
17e7b47cdc
Add product info on broadband change requests 2021-10-08 12:34:56 +11:00
Deon George
cc49692545
Missed addition of 2 new js files in 7c536920 2021-10-01 15:10:47 +10:00
Deon George
1a92d89911
More autoincrement definition to after unique key creation 2021-10-01 15:04:59 +10:00
Deon George
7c5369203c
Optimise charge table, implemented charge recording, optimised payment recording 2021-10-01 14:59:04 +10:00
Deon George
c0ad46ba65
Show service detail on invoices 2021-09-30 12:54:44 +10:00
Deon George
5be4fe6784
Redirect Invoice Download to invoice view 2021-09-29 17:36:55 +10:00
Deon George
7acb9e964b
Enable users to execute workflows 2021-09-29 17:11:46 +10:00
Deon George
4243da9c32
Fix Model Policies from matching user_id's and account_id's, and other minor cosmetic fixes 2021-09-29 16:20:22 +10:00
Deon George
f7439172b6
Service cancellation and ordering 2021-09-29 14:57:25 +10:00
Deon George
b2e45fcaee
Added unique validation for broadband orders, add more visual display that order had errors 2021-09-28 15:14:43 +10:00
Deon George
eae1b16797
Error when accepting VOIP orders 2021-09-28 12:56:29 +10:00
Deon George
4cbe990ec1
Changes to orders to stop existing services being submitted 2021-09-28 12:43:54 +10:00
Deon George
7277d7407a
Framework updates and minor cosmetic fixes 2021-09-28 10:09:56 +10:00
Deon George
ccd6a11c8a
Missed PaymentsImport in previous commit 2021-07-29 13:18:39 +10:00
Deon George
00f215b780
Minor job cleanup, import Ezypay payments update 2021-07-29 13:11:14 +10:00
Deon George
10e6c73b2b
Missed some references to ab_payments in d463239 2021-07-23 17:49:59 +10:00
Deon George
7a963c8461
Fix test installing mariadb-client 2021-07-23 17:38:57 +10:00
Deon George
d463239b17
Rework payment tables, enable payment editing 2021-07-23 17:36:53 +10:00
Deon George
fa62a47680
Add VOIP to search 2021-07-19 16:42:37 +10:00
Deon George
f22813bb5b
Remove more references to ab_account, Docker build tweaks, Fix error rendering charges 2021-07-19 16:23:38 +10:00
Deon George
8b08f79877
Fix 500 error Service::isBilled 2021-07-13 17:07:47 +10:00
Deon George
9515e67493
Re-enable google auth 2021-07-13 15:00:01 +10:00
Deon George
53fc25612b
Fix domain name uniqueness during update, taking into account tld 2021-07-13 14:56:14 +10:00
Deon George
71d2faedb1
Try new schema dump to trim migrations - remove database testing from docker image 2021-07-13 13:31:30 +10:00
456 changed files with 23003 additions and 12498 deletions

View File

@ -1,32 +1,34 @@
APP_DEBUG=false
APP_NAME=OSB APP_NAME=OSB
APP_NAME_HTML_LONG="<b>Graytech</b>Hosting" APP_NAME_HTML_LONG="<b>Graytech</b>Hosting"
APP_NAME_HTML_SHORT="<b>G</b>H" APP_NAME_HTML_SHORT="<b>G</b>H"
APP_ENV=production APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=false APP_TIMEZONE=Australia/Melbourne
APP_URL=https://www.graytech.net.au APP_URL=https://www.graytech.net.au
LOG_CHANNEL=stack LOG_CHANNEL=daily
DB_CONNECTION=mysql DB_CONNECTION=pgsql
DB_HOST=database DB_HOST=postgres
DB_PORT=3306 DB_PORT=5432
DB_DATABASE=database DB_DATABASE=graytech
DB_USERNAME=homestead DB_USERNAME=graytech
DB_PASSWORD=secret DB_PASSWORD=
DB_SCHEMA=billing
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_STORE=file
SESSION_DRIVER=file SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
QUEUE_DRIVER=database QUEUE_CONNECTION=database
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_DRIVER=smtp MAIL_MAILER=smtp
MAIL_HOST=MAIL MAIL_HOST=smtp
MAIL_PORT=25 MAIL_PORT=25
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
@ -43,13 +45,13 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
EZYPAY_TOKEN= EZYPAY_TOKEN=
EZYPAY_GUID= EZYPAY_GUID=
QUICKBOOKS_CLIENT_ID=
QUICKBOOKS_CLIENT_SECRET=
QUICKBOOKS_API_URL=Production
AUTH_GOOGLE_CLIENT_ID= AUTH_GOOGLE_CLIENT_ID=
AUTH_GOOGLE_SECRET= AUTH_GOOGLE_SECRET=
AUTH_INTUIT_CLIENT_ID=
AUTH_INTUIT_SECRET_KEY=
INTUIT_VERIFYTOKEN=
PAYPAL_MODE=sandbox PAYPAL_MODE=sandbox
PAYPAL_SANDBOX_CLIENT_ID= PAYPAL_SANDBOX_CLIENT_ID=
PAYPAL_SANDBOX_SECRET= PAYPAL_SANDBOX_SECRET=

View File

@ -0,0 +1,147 @@
name: Create Docker Image
run-name: ${{ gitea.actor }} Building Docker Image 🐳
on: [push]
env:
VERSION: latest
DOCKER_HOST: tcp://127.0.0.1:2375
jobs:
# test:
# strategy:
# matrix:
# arch:
# - x86_64
# # arm64
#
# name: Test Application
# runs-on: docker-${{ matrix.arch }}
# container:
# image: gitea.dege.au/docker/php:8.3-fpm-pgsql-server-test
#
# steps:
# - name: Environment Setup
# run: |
# # If we have a proxy use it
# if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# # Some pre-reqs
# apk add git nodejs
# ## Some debugging info
# # env|sort
#
# - name: Code Checkout
# uses: actions/checkout@v4
#
# - name: Run Tests
# run: |
# mv .env.testing .env
# # Install Composer and project dependencies.
# mkdir -p ${COMPOSER_HOME}
# if [ -n "${{ secrets.COMPOSER_GITHUB_TOKEN }}" ]; then echo ${{ secrets.COMPOSER_GITHUB_TOKEN }} > ${COMPOSER_HOME}/auth.json; fi
# composer install
# # Generate an application key. Re-cache.
# php artisan key:generate
# php artisan migrate
# php artisan db:seed
# # run laravel tests
# touch storage/app/test/*ZIP storage/app/test/file/*
# XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
build:
strategy:
matrix:
arch:
- x86_64
# - arm64
# needs: [test]
name: Build Docker Image
runs-on: docker-${{ matrix.arch }}
container:
image: docker:dind
privileged: true
env:
ARCH: ${{ matrix.arch }}
VERSIONARCH: ${{ env.VERSION }}-${{ env.ARCH }}
steps:
- name: Environment Setup
run: |
# If we have a proxy use it
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# Some pre-reqs
apk add git curl nodejs
# Start docker
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
## Some debugging info
# docker info && docker version
env|sort
echo "PRT: ${{ secrets.PKG_WRITE_TOKEN }}"
- name: Registry FQDN Setup
id: registry
run: |
registry=${{ github.server_url }}
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
- name: Container Registry Login
uses: docker/login-action@v2
with:
registry: ${{ steps.registry.outputs.registry }}
username: ${{ gitea.actor }}
password: ${{ secrets.PKG_WRITE_TOKEN }}
- name: Code Checkout
uses: actions/checkout@v4
- name: Record version
run: |
pwd
ls -al
echo ${GITHUB_SHA::8} > VERSION
cat VERSION
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSIONARCH }}"
manifest:
name: Final Docker Image Manifest
runs-on: docker-x86_64
container:
image: docker:dind
privileged: true
needs: [build]
steps:
- name: Environment Setup
run: |
# If we have a proxy use it
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
# Some pre-reqs
apk add git curl nodejs
# Start docker
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
- name: Registry FQDN Setup
id: registry
run: |
registry=${{ github.server_url }}
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
- name: Container Registry Login
uses: docker/login-action@v2
with:
registry: ${{ steps.registry.outputs.registry }}
username: ${{ gitea.actor }}
password: ${{ secrets.PKG_WRITE_TOKEN }}
- name: Build Docker Manifest
run: |
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-x86_64
# ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-arm64
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}

View File

@ -1,14 +0,0 @@
stages:
- test
- build
# This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
include:
- .gitlab-test.yml
- .gitlab-docker-x86_64.yml

View File

@ -1,33 +0,0 @@
docker:
image: docker:latest
stage: build
services:
- docker:dind
variables:
VERSION: latest
CACHETAG: build-${VERSION}
DOCKER_HOST: tcp://docker:2375
tags:
- docker
- x86_64
only:
- master
before_script:
- docker info
- docker version
- echo "$CI_JOB_TOKEN" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > auth.json; fi
script:
- if [ -f init ]; then chmod 500 init; fi
- ([ -z "$REFRESH" ] && docker pull ${CI_REGISTRY_IMAGE}:${CACHETAG}) || echo "true"
- echo -n ${CI_COMMIT_SHORT_SHA} > VERSION
- rm -rf vendor/
- docker build --cache-from ${CI_REGISTRY_IMAGE}:${CACHETAG} -t ${CI_REGISTRY_IMAGE}:${VERSION} -t ${CI_REGISTRY_IMAGE}:${CACHETAG} .
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
- docker push ${CI_REGISTRY_IMAGE}:${CACHETAG}

View File

@ -1,48 +0,0 @@
test:
image: registry.leenooks.net/leenooks/php:8.0-fpm-ext-test
stage: test
# NOTE: This service is dependant on project file configuration, which is not there if the cache was deleted
# resulting in the testing to fail on the first run.
services:
- mariadb:10.5
variables:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: test
MYSQL_USER: test
MYSQL_PASSWORD: test
tags:
- php
only:
- master
- test
before_script:
- mv .env.testing .env
# Install Composer and project dependencies.
- mkdir -p /root/.config/composer
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > /root/.config/composer/auth.json ; fi
- composer install
# Add mysql client for schema pre-load
- apt update && apt install -f mariadb-client
# Generate an application key. Re-cache.
- php artisan key:generate --env=testing
- php artisan config:cache --env=testing
- php artisan migrate
- php artisan db:seed
script:
# run laravel tests
- XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
# run frontend tests
# if you have any task for testing frontend
# set it in your package.json script
# comment this out if you don't have a frontend test
# npm test

View File

@ -1,13 +0,0 @@
FROM registry.leenooks.net/leenooks/php:8.0-fpm-image
COPY . /var/www/html/
RUN export COMPOSER_HOME=/var/www/.composer \
&& mkdir -p /var/www/.composer \
&& ([ -r auth.json ] && mv auth.json /var/www/.composer/) || true \
&& touch .composer.refresh \
&& mv .env.example .env \
&& FORCE_PERMS=1 NGINX_START=FALSE /sbin/init \
&& chmod +x /var/www/html/artisan \
&& /var/www/html/artisan storage:link \
&& rm -rf /var/www/.composer

View File

@ -1,7 +0,0 @@
<?php
namespace App\Classes\External;
abstract class Accounting
{
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Classes\External\Accounting;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use QuickBooksOnline\API\Data\IPPCustomer;
use App\Classes\External\Accounting as Base;
use App\Models\User;
class Quickbooks extends Base
{
private $api = NULL;
public function __construct(User $uo)
{
if (Auth::user())
throw new \Exception('User logged in - *TODO* handle this');
Auth::loginUsingId($uo->id);
$this->api = app('Spinen\QuickBooks\Client');
}
public function getCustomers($refresh=FALSE): Collection
{
if ($refresh)
Cache::forget(__METHOD__);
return Cache::remember(__METHOD__,86400,function() {
return collect($this->api->getDataService()->Query('SELECT * FROM Customer'));
});
}
public function getInvoice(int $id,$refresh=FALSE)
{
if ($refresh)
Cache::forget(__METHOD__.$id);
return Cache::remember(__METHOD__.$id,86400,function() use ($id) {
return $this->api->getDataService()->Query(sprintf("SELECT * FROM Invoice where id = '%s'",$id));
});
}
public function getInvoices($refresh=FALSE): Collection
{
if ($refresh)
Cache::forget(__METHOD__);
return Cache::remember(__METHOD__,86400,function() {
return collect($this->api->getDataService()->Query('SELECT * FROM Invoice'));
});
}
public function updateCustomer(IPPCustomer $r,array $args)
{
$r->sparse = TRUE;
foreach ($args as $k=>$v)
{
$r->{$k} = $v;
}
return $this->api->getDataService()->Update($r);
}
}

View File

@ -30,8 +30,8 @@ class Ezypay extends Payments
$api_remain = Arr::get($result->getHeader('X-RateLimit-Remaining'),0); $api_remain = Arr::get($result->getHeader('X-RateLimit-Remaining'),0);
$api_reset = Arr::get($result->getHeader('X-RateLimit-Reset'),0); $api_reset = Arr::get($result->getHeader('X-RateLimit-Reset'),0);
if ($api_remain == 0) { if ($api_remain === 0) {
Log::error('API Throttle.',['m'=>__METHOD__]); Log::error('API Throttle.',['m'=>__METHOD__,'api_reset'=>$api_reset]);
Cache::put('api_throttle',$api_reset,now()->addSeconds($api_reset)); Cache::put('api_throttle',$api_reset,now()->addSeconds($api_reset));
} }
@ -45,6 +45,56 @@ class Ezypay extends Payments
}); });
} }
/**
* Get a list of configured customers
*
* @return Collection
* @todo Hard coded at 100 clients - need to make this more dynamic
*
* {#1079
* +"TermsAndConditions": true
* +"Account": {#1082
* +"PaymentMethodId": 0
* }
* +"Address1": "1 Road Street "
* +"BillingStatus": "Active"
* +"BusinessAccountReference": "12345"
* +"CountryCode": "AU"
* +"Email": "user@example.com"
* +"EzypayReferenceNumber": 12345678
* +"Firstname": "Bart"
* +"Id": "6219c42b-8e56-4e4e-af3a-76ddf4b0e4e1"
* +"MobilePhone": ""
* +"Postcode": "1234"
* +"ReferenceId": "01nnnn"
* +"State": "VIC"
* +"Suburb": "TOWN"
* +"Towncity": ""
* +"Surname": "Simpson "
* +"PaymentPlanId": "00000000-0000-0000-0000-000000000000"
* +"DateOfBirth": "1970-01-01T00:00:00.000"
* +"Gender": "M"
* +"DebitType": 0
* +"RecurringAmount": 0.0
* +"Frequency": 0
* +"FrequencyType": 0
* +"StartDate": "0001-01-01T00:00:00.000"
* +"RecurringDebitEndType": 0
* +"TotalAmountCollected": 0.0
* +"MinimumNumberOfPayment": 0
* +"RecurringWithDifferentFirstDebitAmount": 0.0
* +"RecurringDebitFirstDebitDate": "0001-01-01T00:00:00.000"
* +"RecurringDebitAmount": 0.0
* +"RecurringDebitFrequency": 0
* +"RecurringDebitFrequencyType": 0
* +"RecurringDebitStartDate": "0001-01-01T00:00:00.000"
* +"RecurringDebitDifferentFirstAmountEndType": 0
* +"RecurringDebitTotalAmountCollected": 0.0
* +"RecurringDebitMinimumNumberOfPayment": 0
* +"OnceOffAmount": 0.0
* +"OnceOffStartDate": "0001-01-01T00:00:00.000"
* }
*/
public function getCustomers(): Collection public function getCustomers(): Collection
{ {
return Cache::remember(__METHOD__,86400,function() { return Cache::remember(__METHOD__,86400,function() {
@ -52,6 +102,29 @@ class Ezypay extends Payments
}); });
} }
/**
* Get Specific debits for a client.
*
* @param array $opt
* @return Collection
*
* Illuminate\Support\Collection^ {#1077
* #items: array:4 [
* 0 => {#1826
* +"Amount": 99.99
* +"Id": "76666ef1-106c-458c-9162-e77fe746517c"
* +"CustomerId": "6219c42b-8e56-4e4e-af3a-76ddf4b0e4e1"
* +"Date": "2021-10-01T00:00:00.000"
* +"Status": "Pending"
* }
* 1 => {#2075
* +"Amount": 99.99
* +"Id": "80ba201d-fb6f-4700-b1b7-2b13fbfa91d5"
* +"CustomerId": "6219c42b-8e56-4e4e-af3a-76ddf4b0e4e1"
* +"Date": "2021-09-01T00:00:00.000"
* +"Status": "Pending"
* }
*/
public function getDebits($opt=[]): Collection public function getDebits($opt=[]): Collection
{ {
return Cache::remember(__METHOD__.http_build_query($opt),86400,function() use ($opt) { return Cache::remember(__METHOD__.http_build_query($opt),86400,function() use ($opt) {

View File

@ -17,6 +17,8 @@ abstract class Supplier
protected $o = NULL; protected $o = NULL;
protected $_columns = []; protected $_columns = [];
public const traffic_connection_keys = ['user','pass','url'];
public function __construct(Model $o) public function __construct(Model $o)
{ {
$this->o = $o; $this->o = $o;
@ -26,24 +28,29 @@ abstract class Supplier
/** /**
* Connect and pull down traffic data * Connect and pull down traffic data
* *
* @param array $connection
* @param string $type
* @return Collection * @return Collection
* @throws \Exception
*/ */
public function fetch(): Collection public function fetch(array $connection,string $type): Collection
{ {
if (count(array_intersect(array_keys($connection),self::traffic_connection_keys)) !== 3)
throw new \Exception('No or missing connection details for:'.$type);
if ($x=$this->mustPause()) { if ($x=$this->mustPause()) {
Log::notice(sprintf('%s:API Throttle, waiting [%s]...',self::LOGKEY,$x),['m'=>__METHOD__]); Log::notice(sprintf('%s:API Throttle, waiting [%s]...',self::LOGKEY,$x),['m'=>__METHOD__]);
sleep($x); sleep($x);
} }
Log::debug(sprintf('%s:Supplier [%d], fetch data for [%s]...',self::LOGKEY,$this->o->id,$this->o->stats_lastupdate),['m'=>__METHOD__]); Log::debug(sprintf('%s:Supplier [%d], fetch data for [%s]...',self::LOGKEY,$this->o->id,Arr::get($connection,'last')),['m'=>__METHOD__]);
$key = 'Supplier:'.$this->o->id.$this->o->stats_lastupdate; $key = 'Supplier:'.$this->o->id.Arr::get($connection,'last');
$result = Cache::remember($key,86400,function() {
$client = $this->getClient();
$response = Http::get($this->o->stats_url,[ $result = Cache::remember($key,86400,function() use ($connection) {
$this->login_user_field => $this->o->stats_username, $response = Http::get(Arr::get($connection,'url'),[
$this->login_pass_field => $this->o->stats_password, $this->login_user_field => Arr::get($connection,'user'),
$this->date_field => $this->o->stats_lastupdate->format('Y-m-d'), $this->login_pass_field => Arr::get($connection,'pass'),
$this->date_field => Arr::get($connection,'last'),
]); ]);
// @todo These API rate limiting is untested. // @todo These API rate limiting is untested.
@ -55,7 +62,6 @@ abstract class Supplier
Cache::put('api_throttle',$api_reset,now()->addSeconds($api_reset)); Cache::put('api_throttle',$api_reset,now()->addSeconds($api_reset));
} }
//dd($response->header('Content-Type'),$response->headers());
// Assume the supplier provides an ASCII output for text/html // Assume the supplier provides an ASCII output for text/html
if (preg_match('#^text/html;#',$x=$response->header('Content-Type'))) { if (preg_match('#^text/html;#',$x=$response->header('Content-Type'))) {
return collect(explode("\n",$response->body()))->filter(); return collect(explode("\n",$response->body()))->filter();

View File

@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Jobs\AccountingCustomerUpdate;
use Intuit\Models\Customer as AccAccount;
use App\Models\{Account,ProviderOauth,Site,User};
class AccountingAccountAdd extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:account:add'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Account ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add an account to the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$o = Account::findOrFail($this->argument('id'));
$acc = new AccAccount;
$acc->PrimaryEmailAddr = (object)['Address'=>$o->user->email];
$acc->ResaleNum = $o->sid;
$acc->GivenName = $o->user->firstname;
$acc->FamilyName = $o->user->lastname;
$acc->CompanyName = $o->name;
$acc->DisplayName = $o->name;
$acc->FullyQualifiedName = $o->name;
$acc->Active = (bool)$o->active;
return AccountingCustomerUpdate::dispatchSync($to,$acc);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Exceptions\ConnectionIssueException;
use App\Models\{ProviderOauth,Site,User};
class AccountingAccountGet extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:account:get'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Account ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get an account from the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
try {
$api = $to->API();
dump($api->getAccountQuery($this->argument('id')));
} catch (ConnectException|ConnectionIssueException $e) {
$this->error($e->getMessage());
return Command::FAILURE;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{ProviderOauth,Site,User};
use App\Jobs\AccountingAccountSync as Job;
/**
* Synchronise Customers with Accounts
*/
class AccountingAccountSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:account:sync'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise accounts with accounting system';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
Job::dispatchSync($to);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Jobs\AccountingInvoiceUpdate;
use Intuit\Models\Invoice as AccInvoice;
use App\Models\{Invoice,ProviderOauth,Site,User};
class AccountingInvoiceAdd extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:invoice:add'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Invoice ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add an invoice to the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$o = Invoice::findOrFail($this->argument('id'));
// Check the customer exists
if ($o->account->providers->where('pivot.provider_oauth_id',$so->id)->count() !== 1)
throw new \Exception(sprintf('Account [%d] for Invoice [%d] not defined',$o->account_id,$o->id));
$ao = $o->account->providers->where('pivot.provider_oauth_id',$so->id)->pop();
// Some validation
if (! $ao->pivot->ref) {
$this->error(sprintf('Accounting not defined for account [%d]',$o->account_id));
exit(1);
}
$acc = new AccInvoice;
$acc->CustomerRef = (object)['value'=>$ao->pivot->ref];
$acc->DocNumber = $o->lid;
$acc->TxnDate = $o->created_at->format('Y-m-d');
$acc->DueDate = $o->due_at->format('Y-m-d');
$lines = collect();
$c = 0;
// @todo Group these by ItemRef and the Same Unit Price and Description, so that we can then use quantity to represent the number of them.
foreach ($o->items->groupBy(function($item) use ($so) {
return sprintf('%s.%s.%s.%s',$item->item_type_name,$item->price_base,$item->product->provider_ref($so),$item->taxes->pluck('description')->join('|'));
}) as $os)
{
$key = $os->first();
// Some validation
if (! ($ref=$key->product->provider_ref($so))) {
$this->error(sprintf('Accounting not defined in product [%d]',$key->product_id));
exit(1);
}
if ($key->taxes->count() !== 1) {
$this->error(sprintf('Cannot handle when there is not just 1 tax line [%d]',$key->id));
exit(1);
}
$c++;
$line = new \stdClass;
$line->Id = $c;
$line->DetailType = 'SalesItemLineDetail';
$line->Description = $key->item_type_name;
$line->SalesItemLineDetail = (object)[
'Qty' => $os->sum('quantity'),
'UnitPrice' => $key->price_base,
'ItemRef' => ['value'=>$ref],
// @todo It is assumed there is only 1 tax category
'TaxCodeRef' => ['value'=>$key->taxes->first()->tax->provider_ref($so)],
];
$line->Amount = $os->sum('quantity')*$key->price_base;
$lines->push($line);
}
$acc->Line = $lines;
return AccountingInvoiceUpdate::dispatchSync($to,$acc);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Exceptions\ConnectionIssueException;
use App\Models\{ProviderOauth,Site,User};
class AccountingInvoiceGet extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:invoice:get'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Invoice ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get an invoice from the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
try {
$api = $to->API();
dump($api->getInvoiceQuery($this->argument('id')));
} catch (ConnectException|ConnectionIssueException $e) {
$this->error($e->getMessage());
return Command::FAILURE;
}
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\{Product, ProviderOauth, Site, User};
use App\Jobs\AccountingItemSync as Job;
class AccountingItemList extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:item:list'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise items with accounting system';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
$uo = User::where('email',$this->argument('user'))->singleOrFail();
if (($x=$so->tokens->where('user_id',$uo->id))->count() !== 1)
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$to = $x->pop();
// Current Products used by services
$products = Product::select(['products.*'])
->distinct('products.id')
->join('services',['services.product_id'=>'products.id'])
->where('services.active',TRUE)
->get();
$api = $so->API($to,TRUE); // @todo Remove TRUE
$acc = $api->getItems()->pluck('pid','FullyQualifiedName');
foreach ($products as $po) {
if (! $po->accounting)
$this->error(sprintf('Product [%d](%s) doesnt have accounting set',$po->id,$po->name));
elseif ($acc->has($po->accounting) === FALSE)
$this->error(sprintf('Product [%d](%s) accounting [%s] doesnt exist?',$po->id,$po->name,$po->accounting));
else
$this->info(sprintf('Product [%d](%s) set to accounting [%s]',$po->id,$po->name,$po->accounting));
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Console\Commands;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Intuit\Exceptions\ConnectionIssueException;
use App\Jobs\AccountingPaymentSync as Job;
use App\Models\{ProviderOauth,Site,User};
class AccountingPaymentGet extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:payment:get'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}'
.' {id : Payment ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get a payment from the accounting provider';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
try {
$api = $to->API();
$acc = $api->getPayment($this->argument('id'));
dump($acc);
} catch (ConnectException|ConnectionIssueException $e) {
$this->error($e->getMessage());
return Command::FAILURE;
}
if ($acc)
Job::dispatchSync($to,$acc);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{ProviderOauth,Site,User};
use App\Jobs\AccountingPaymentSync as Job;
class AccountingPaymentSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:payment:sync'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise payments with accounting system';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$api = $to->API();
foreach ($api->getPayments() as $acc)
Job::dispatchSync($to,$acc);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{ProviderOauth,Site,User};
use App\Jobs\AccountingTaxSync as Job;
/**
* Synchronise TAX ids with our taxes.
*/
class AccountingTaxSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:tax:sync'
.' {siteid : Site ID}'
.' {provider : Provider Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise taxes with accounting system';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$uo = User::where('email',$this->argument('user'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
Job::dispatchSync($to);
}
}

View File

@ -5,7 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Jobs\BroadbandTraffic as Job; use App\Jobs\BroadbandTraffic as Job;
use App\Models\AdslSupplier; use App\Models\Supplier;
class BroadbandTraffic extends Command class BroadbandTraffic extends Command
{ {
@ -14,7 +14,8 @@ class BroadbandTraffic extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'broadband:traffic:import'; protected $signature = 'broadband:traffic:import'.
' {--s|supplier= : Supplier Name}';
/** /**
* The console command description. * The console command description.
@ -30,7 +31,14 @@ class BroadbandTraffic extends Command
*/ */
public function handle() public function handle()
{ {
foreach (AdslSupplier::active()->get() as $o) if ($this->option('supplier')) {
Job::dispatch($o); $o = Supplier::where('name','like',$this->option('supplier'))->singleOrFail();
Job::dispatchSync($o->id);
return;
}
foreach (Supplier::active()->get() as $o)
Job::dispatchSync($o->id);
} }
} }

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\ImportCosts as Job;
use App\Models\{Site,Supplier};
class ImportCosts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'costs:import {siteid : Site ID} {supplier : Supplier Name} {file : Filename} {date : Date}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import Costs from file';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
if (! str_starts_with($this->argument('file'),'files/'))
throw new \Exception('Filename must start with files/');
Job::dispatchSync(
Site::findOrFail($this->argument('siteid')),
Supplier::where('name',$this->argument('supplier'))->singleOrFail(),
Carbon::create($this->argument('date')),
$this->argument('file'),
);
}
}

View File

@ -2,10 +2,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Invoice; use App\Models\{Invoice,Site};
class InvoiceEmail extends Command class InvoiceEmail extends Command
{ {
@ -14,7 +15,7 @@ class InvoiceEmail extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'invoice:email {id}'; protected $signature = 'invoice:email {site} {id}';
/** /**
* The console command description. * The console command description.
@ -23,16 +24,6 @@ class InvoiceEmail extends Command
*/ */
protected $description = 'Email Invoices to be client'; protected $description = 'Email Invoices to be client';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
@ -40,19 +31,21 @@ class InvoiceEmail extends Command
*/ */
public function handle() public function handle()
{ {
Config::set('site',Site::findOrFail($this->argument('site')));
$o = Invoice::findOrFail($this->argument('id')); $o = Invoice::findOrFail($this->argument('id'));
Mail::to($o->account->user->email)->send(new \App\Mail\InvoiceEmail($o)); $result = Mail::to($o->account->user->email)->send(new \App\Mail\InvoiceEmail($o));
if (Mail::failures()) { try {
dump('Failure?');
dump(Mail::failures());
} else {
$o->print_status = TRUE; $o->print_status = TRUE;
$o->reminders = $o->reminders('send'); //$o->reminders = $o->reminders('send');
$o->save(); $o->save();
}
} catch (\Exception $e) {
dd($e);
}
dump($result->getDebug());
} }
} }

View File

@ -2,10 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{Account,Invoice,Site};
class InvoiceGenerate extends Command class InvoiceGenerate extends Command
{ {
@ -14,7 +14,7 @@ class InvoiceGenerate extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'invoice:generate {account?} {--p|preview : Preview} {--l|list : List Items}'; protected $signature = 'invoice:generate {site} {account?} {--p|preview : Preview} {--l|list : List Items}';
/** /**
* The console command description. * The console command description.
@ -23,16 +23,6 @@ class InvoiceGenerate extends Command
*/ */
protected $description = 'Generate Invoices to be Sent'; protected $description = 'Generate Invoices to be Sent';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
@ -40,6 +30,8 @@ class InvoiceGenerate extends Command
*/ */
public function handle() public function handle()
{ {
Config::set('site',Site::findOrFail($this->argument('site')));
if ($this->argument('account')) if ($this->argument('account'))
$accounts = collect()->push(Account::find($this->argument('account'))); $accounts = collect()->push(Account::find($this->argument('account')));
else else

View File

@ -2,11 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay; use App\Classes\External\Payments\Ezypay;
use App\Models\{Account,Checkout,Payment}; use App\Jobs\PaymentsImport as Job;
class PaymentsEzypayImport extends Command class PaymentsEzypayImport extends Command
{ {
@ -24,16 +23,6 @@ class PaymentsEzypayImport extends Command
*/ */
protected $description = 'Retrieve payments from Ezypay'; protected $description = 'Retrieve payments from Ezypay';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
@ -41,67 +30,6 @@ class PaymentsEzypayImport extends Command
*/ */
public function handle() public function handle()
{ {
$poo = new Ezypay(); Job::dispatchSync(new Ezypay);
// Get our checkout IDs for this plugin
$cos = Checkout::where('plugin',config('services.ezypay.plugin'))->pluck('id');
foreach ($poo->getCustomers() as $c)
{
if ($c->BillingStatus == 'Inactive')
{
$this->info(sprintf('Ignoring INACTIVE: [%s] %s %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname));
continue;
}
// Load Account Details from ReferenceId
$ao = Account::where('site_id',(int)substr($c->ReferenceId,0,2))
->where('id',(int)substr($c->ReferenceId,2,4))
->first();
if (! $ao)
{
$this->warn(sprintf('Missing: [%s] %s %s (%s)',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId));
continue;
}
// Find the last payment logged
$last = Carbon::createFromTimestamp(Payment::whereIN('checkout_id',$cos)->where('account_id',$ao->id)->max('date_payment'));
$o = $poo->getDebits([
'customerId'=>$c->Id,
'dateFrom'=>$last->format('Y-m-d'),
'dateTo'=>$last->addQuarter()->format('Y-m-d'),
'pageSize'=>100,
]);
// Load the payments
if ($o->count())
{
foreach ($o->reverse() as $p)
{
// If not success, ignore it.
if ($p->Status != 'Success')
continue;
$pd = Carbon::createFromFormat('Y-m-d?H:i:s.u',$p->Date);
$lp = $ao->payments->last();
if ($lp AND (($pd == $lp->date_payment) OR ($p->Id == $lp->checkout_data)))
continue;
// New Payment
$po = new Payment;
$po->site_id = 1; // @todo
$po->date_payment = $pd;
$po->checkout_id = '999'; // @todo
$po->checkout_data = $p->Id;
$po->total_amt = $p->Amount;
$ao->payments()->save($po);
$this->info(sprintf('Recorded: Payment for [%s] %s %s (%s) on %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$po->id,$pd));
}
}
}
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay; use App\Classes\External\Payments\Ezypay;
@ -23,16 +24,6 @@ class PaymentsEzypayNext extends Command
*/ */
protected $description = 'Load next payments, and ensure they cover the next invoice'; protected $description = 'Load next payments, and ensure they cover the next invoice';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
@ -40,12 +31,10 @@ class PaymentsEzypayNext extends Command
*/ */
public function handle() public function handle()
{ {
$poo = new Ezypay(); $poo = new Ezypay;
foreach ($poo->getCustomers() as $c) foreach ($poo->getCustomers() as $c) {
{ if ($c->BillingStatus == 'Inactive') {
if ($c->BillingStatus == 'Inactive')
{
$this->info(sprintf('Ignoring INACTIVE: [%s] %s %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname)); $this->info(sprintf('Ignoring INACTIVE: [%s] %s %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname));
continue; continue;
} }
@ -53,10 +42,9 @@ class PaymentsEzypayNext extends Command
// Load Account Details from ReferenceId // Load Account Details from ReferenceId
$ao = Account::where('site_id',(int)substr($c->ReferenceId,0,2)) $ao = Account::where('site_id',(int)substr($c->ReferenceId,0,2))
->where('id',(int)substr($c->ReferenceId,2,4)) ->where('id',(int)substr($c->ReferenceId,2,4))
->first(); ->single();
if (! $ao) if (! $ao) {
{
$this->warn(sprintf('Missing: [%s] %s %s (%s)',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId)); $this->warn(sprintf('Missing: [%s] %s %s (%s)',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId));
continue; continue;
} }
@ -70,12 +58,33 @@ class PaymentsEzypayNext extends Command
'dateTo'=>now()->addQuarter()->format('Y-m-d'), 'dateTo'=>now()->addQuarter()->format('Y-m-d'),
])->reverse()->first(); ])->reverse()->first();
if ($next_pay->Status !== 'Pending') {
$this->warn(sprintf('Next payment is not pending for (%s)',$ao->name));
continue;
}
$next_paydate = Carbon::createFromTimeString($next_pay->Date);
if ($next_pay->Amount < $account_due) if ($next_pay->Amount < $account_due)
$this->warn(sprintf('Next payment for (%s) [%s] not sufficient for outstanding balance [%s]',$ao->name,number_format($next_pay->Amount,2),number_format($account_due,2))); $this->warn(sprintf('Next payment on [%s] for (%s) [%s] not sufficient for outstanding balance [%s]',
$next_paydate->format('Y-m-d'),
$ao->name,
number_format($next_pay->Amount,2),
number_format($account_due,2)));
elseif ($next_pay->Amount > $account_due) elseif ($next_pay->Amount > $account_due)
$this->warn(sprintf('Next payment for (%s) [%s] is too much for outstanding balance [%s]',$ao->name,number_format($next_pay->Amount,2),number_format($account_due,2))); $this->warn(sprintf('Next payment on [%s] for (%s) [%s] is too much for outstanding balance [%s]',
$next_paydate->format('Y-m-d'),
$ao->name,
number_format($next_pay->Amount,2),
number_format($account_due,2)));
else else
$this->info(sprintf('Next payment for (%s) [%s] will cover outstanding balance [%s]',$ao->name,number_format($next_pay->Amount,2),number_format($account_due,2))); $this->info(sprintf('Next payment on [%s] for (%s) [%s] will cover outstanding balance [%s]',
$next_paydate->format('Y-m-d'),
$ao->name,
number_format($next_pay->Amount,2),
number_format($account_due,2)));
} }
} }
} }

View File

@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{ProviderOauth,Site,User};
use App\Jobs\ProviderTokenRefresh as Job;
class ProviderTokenRefresh extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'provider:token:refresh'
.' {siteid : Site ID}'
.' {provider : Supplier Name}'
.' {user : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Refresh users access/refresh token';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$site = Site::findOrFail($this->argument('siteid'));
Config::set('site',$site);
$so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail();
$uo = User::where('email',$this->argument('user'))->singleOrFail();
if (($x=$so->tokens->where('user_id',$uo->id))->count() !== 1)
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
Job::dispatchSync($x->pop());
}
}

View File

@ -1,261 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use QuickBooksOnline\API\Data\IPPEmailAddress;
use QuickBooksOnline\API\Data\IPPPhysicalAddress;
use App\Classes\External\Accounting\Quickbooks;
use App\Models\{Account,External\Integrations,User};
class QuickAccounts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'external:sync:accounts {--m|match : Match Display Name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync Account numbers with External Sources';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
foreach (Integrations::active()->type('ACCOUNTING')->get() as $into)
{
switch ($into->name)
{
case 'quickbooks':
$api = new Quickbooks($into->user);
break;
default:
throw new \Exception('No handler for: ',$into-name);
}
foreach ($api->getCustomers(TRUE) as $r)
{
$this->info(sprintf('Checking [%s] (%s)',$r->Id,$r->DisplayName));
if ($r->Notes == 'Cash Only')
{
$this->warn(sprintf('Skipping [%s] (%s)',$r->Id,$r->DisplayName));
continue;
}
if (! $this->option('match') AND (! $r->CompanyName AND (! $r->FamilyName AND ! $r->GivenName)))
{
$this->error(sprintf('No COMPANY or PERSONAL details for [%s] (%s)',$r->Id,$r->DisplayName));
continue;
}
if ($this->option('match')) {
$ao = Account::where('company',$r->DisplayName);
if (! $ao->count()) {
$uo = User::where('lastname',$r->FamilyName);
if ($r->GivenName)
$uo->where('firstname',$r->GivenName);
if ($uo->count() > 1 OR (! $uo->count()))
{
$this->error(sprintf('No SINGLE Users matched for [%s] (%s)',$r->Id,$r->DisplayName));
continue;
}
$uo = $uo->first();
$ao = $uo->accounts->where('active',TRUE);
}
} else {
if ($r->CompanyName) {
$ao = Account::where('company',$this->option('match') ? $r->DisplayName : $r->CompanyName);
} else {
$uo = User::where('lastname',$r->FamilyName);
if ($r->GivenName)
$uo->where('firstname',$r->GivenName);
if ($uo->count() > 1 OR (! $uo->count()))
{
$this->error(sprintf('No SINGLE matched for [%s] (%s)',$r->Id,$r->DisplayName));
continue;
}
$uo = $uo->first();
$ao = $uo->accounts->where('active',TRUE);
}
}
if (! $ao->count())
{
$this->error(sprintf('No Accounts matched for [%s] (%s)',$r->Id,$r->DisplayName));
continue;
}
if ($ao->count() > 1)
{
$this->error(sprintf('Too Many Accounts (%s) matched for [%s] (%s)',$ao->count(),$r->Id,$r->DisplayName));
continue;
}
$ao = $ao->first();
// If we are matching on DisplayName, make sure the account is updated correct for Business or Personal Accounts
$oldr = clone $r;
// @NOTE: This overwrites the ABN if it exists.
if ($r->PrimaryTaxIdentifier)
{
$this->warn(sprintf('ABN Overwrite for (%s)',$r->DisplayName));
}
switch ($ao->type)
{
case 'Business':
$r->CompanyName = $ao->company;
$r->ResaleNum = $ao->AccountId;
$r->SalesTermRef = '7'; // @todo
if ($ao->first_name)
$r->GivenName = chop($ao->user->firstname); // @todo shouldnt be required
if ($ao->last_name)
$r->FamilyName = $ao->user->lastname;
if ($ao->address1)
{
if (! $r->BillAddr)
$r->BillAddr = new IPPPhysicalAddress;
$r->BillAddr->Line1 = $ao->user->address1;
$r->BillAddr->Line2 = $ao->user->address2;
$r->BillAddr->City = $ao->user->city;
$r->BillAddr->CountrySubDivisionCode = strtoupper($ao->user->state);
$r->BillAddr->PostalCode = $ao->user->postcode;
$r->BillAddr->Country = 'Australia'; // @todo
//$r->ShipAddr = $r->BillAddr;
}
if ($ao->email) {
if (! $r->PrimaryEmailAddr)
$r->PrimaryEmailAddr = new IPPEmailAddress;
$r->PrimaryEmailAddr->Address = $ao->user->email;
$r->PreferredDeliveryMethod = 'Email';
}
if (! $r->Balance)
$r->Active = $ao->active ? 'true' : 'false';
break;
case 'Private':
$r->CompanyName = NULL;
$r->DisplayName = sprintf('%s %s',$ao->user->lastname,$ao->user->firstname);
$r->ResaleNum = $ao->AccountId;
$r->SalesTermRef = '7'; // @todo
if ($ao->first_name)
$r->GivenName = chop($ao->user->firstname); // @todo shouldnt be required
if ($ao->last_name)
$r->FamilyName = $ao->user->lastname;
if ($ao->address1)
{
if (! $r->BillAddr)
$r->BillAddr = new IPPPhysicalAddress;
$r->BillAddr->Line1 = $ao->user->address1;
$r->BillAddr->Line2 = $ao->user->address2;
$r->BillAddr->City = $ao->user->city;
$r->BillAddr->CountrySubDivisionCode = strtoupper($ao->user->state);
$r->BillAddr->PostalCode = $ao->user->postcode;
$r->BillAddr->Country = 'Australia'; // @todo
//$r->ShipAddr = $r->BillAddr;
}
if ($ao->email) {
if (! $r->PrimaryEmailAddr)
$r->PrimaryEmailAddr = new IPPEmailAddress;
$r->PrimaryEmailAddr->Address = $ao->user->email;
$r->PreferredDeliveryMethod = 'Email';
}
if (! $r->Balance)
$r->Active = $ao->active ? 'true' : 'false';
break;
default:
throw new \Exception('Unhandled account type: '.$ao->type);
}
// If something changed, lets update it.
if (count(array_diff_assoc(object_to_array($r,FALSE),object_to_array($oldr,FALSE))))
{
$api->updateCustomer($r,[]);
}
// If external integration doesnt exist, lets create it.
if (! $ao->ExternalAccounting($into))
{
$ao->external()->attach([$into->id=>['site_id'=>1,'link'=>$r->Id]]); // @todo site_id
}
// If the integration ID doesnt exist in the integration source, add it.
if (! $r->ResaleNum)
{
$api->updateCustomer($r,['ResaleNum'=>$ao->AccountId]);
}
// If integration exist, double check the numbers match
if ($r->ResaleNum != $ao->AccountId) {
$this->warn(sprintf('Integration ID Mismatch AID [%s] ID [%s]',$ao->id,$r->Id));
continue;
}
}
}
}
}
function object_to_array($object,$encode=TRUE)
{
// For child arrays, we just encode
if ($encode)
return json_encode($object);
if (is_object($object)) {
return array_map(__FUNCTION__,get_object_vars($object));
} else if (is_array($object)) {
return array_map(__FUNCTION__,$object);
} else {
return $object;
}
}

View File

@ -3,10 +3,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\Service; use App\Models\{Service,Site};
class ServiceList extends Command class ServiceList extends Command
{ {
@ -16,8 +15,8 @@ class ServiceList extends Command
* @var string * @var string
*/ */
protected $signature = 'service:list'. protected $signature = 'service:list'.
'{--a|active : Active Only}'. ' {--i|inactive : Include Inactive}'.
'{--category= : Category}'. ' {--t|type= : Type}'.
' {--f|fix : Fix start_date}'; ' {--f|fix : Fix start_date}';
/** /**
@ -27,16 +26,6 @@ class ServiceList extends Command
*/ */
protected $description = 'List all services'; protected $description = 'List all services';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/** /**
* Execute the console command. * Execute the console command.
* *
@ -44,50 +33,50 @@ class ServiceList extends Command
*/ */
public function handle() public function handle()
{ {
DB::listen(function($query) { $header = '|%13s|%-14s|%-35s|%-40s|%8s|%17s|%12s|%12s|%12s|%12s|%14s|';
Log::debug('- SQL',['sql'=>$query->sql,'binding'=>$query->bindings]);
});
$this->warn(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|', $this->warn(sprintf($header,
'ID', 'ID',
'CAT', 'Type',
'Product', 'Product',
'Name', 'Name',
'active', 'Active',
'status', 'Status',
'invoice next', 'Next Invoice',
'start date', 'Start Date',
'stop date', 'Stop Date',
'connect date', 'Connect Date',
'first invoice' 'First Invoice'
)); ));
foreach (Service::all() as $o) { foreach (Service::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->with(['site'])->cursor() as $o) {
if ($this->option('active') AND ! $o->isActive()) if ((! $this->option('inactive')) AND ! $o->isActive())
continue; continue;
if ($this->option('category') AND $o->product->category !== $this->option('category')) Config::set('site',$o->site);
if ($this->option('type') AND ($o->product->getCategoryAttribute() !== $this->option('type')))
continue; continue;
$c = $o->invoice_items->filter(function($item) {return $item->item_type === 0; })->sortby('date_start')->first(); $c = $o->invoice_items->filter(function($item) {return $item->item_type === 0; })->sortby('start_at')->first();
if ($this->option('fix') AND ! $o->date_start AND $c AND $c->date_start AND $o->type AND $o->type->service_connect_date AND $c->date_start->format('Y-m-d') == $o->type->service_connect_date->format('Y-m-d')) { if ($this->option('fix') AND ! $o->start_at AND $c AND $c->start_at AND $o->type AND $o->type->connect_at AND $c->start_at->format('Y-m-d') == $o->type->connect_at->format('Y-m-d')) {
$o->date_start = $o->type->service_connect_date; $o->start_at = $o->type->connect_at;
$o->save(); $o->save();
} }
$this->info(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|', $this->info(sprintf($header,
$o->sid, $o->sid,
$o->product->category, $o->product->getCategoryNameAttribute(),
$o->product_name, substr($o->product->getNameAttribute(),0,35),
$o->name_short, substr($o->name_short,0,40),
$o->active ? 'active' : 'inactive', $o->active ? 'active' : 'inactive',
$o->status, $o->status,
$o->invoice_next ? $o->invoice_next->format('Y-m-d') : NULL, $o->invoice_next?->format('Y-m-d'),
$o->date_start ? $o->date_start->format('Y-m-d') : NULL, $o->start_at?->format('Y-m-d'),
$o->date_end ? $o->date_end->format('Y-m-d') : NULL, $o->stop_at?->format('Y-m-d'),
($o->type AND $o->type->service_connect_date) ? $o->type->service_connect_date->format('Y-m-d') : NULL, ($o->type AND $o->type->connect_at) ? $o->type->connect_at->format('Y-m-d') : NULL,
$c ? $c->date_start->format('Y-m-d') : NULL, ($c && $c->date_start) ? $c->date_start->format('Y-m-d') : NULL,
)); ));
} }
} }

View File

@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\{Site,Supplier,User};
class SupplierAccountSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'supplier:account:sync'
.' {siteid : Site ID}'
.' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync accounts with a supplier';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Config::set('site',Site::findOrFail($this->argument('siteid')));
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail();
foreach ($so->API($this->option('forceprod'))->getCustomers(['fetchall'=>true]) as $customer) {
// Check if we have this customer already (by ID)
if ($so->users->where('pivot.id',$customer->id)->count()) {
$this->info(sprintf('User already linked (%s:%s)',$customer->id,$customer->email));
} elseif ($x=User::where('email',$customer->email)->single()) {
//dump($x->suppliers->first());
if ($x->suppliers->count()) {
$this->alert(sprintf('User [%d:%s] already linked to this supplier with ID (%s)',$customer->id,$customer->email,$x->suppliers->first()->pivot->id));
} else {
$this->warn(sprintf('User [%d:%s] has same email (%s:%s) - Linked',$x->id,$x->email,$customer->id,$customer->email));
$so->users()->syncWithoutDetaching([
$x->id => [
'id'=>$customer->id,
'site_id'=>$x->site_id, // @todo See if we can have this handled automatically
'created_at'=>Carbon::create($customer->date_added),
]
]);
}
} else {
$this->error(sprintf('User doesnt exist with email (%s:%s)',$customer->id,$customer->email));
}
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\{Site,Supplier};
use App\Jobs\SupplierDomainSync as Job;
class SupplierDomainSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'supplier:domain:sync'
.' {siteid : Site ID}'
.' {supplier : Supplier Name}'
.' {--f|forceprod : Force Prod API on dev environment}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync domains from a supplier';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$so = Supplier::where('name',$this->argument('supplier'))->singleOrFail();
Job::dispatchSync(Site::findOrFail($this->argument('siteid')),$so,$this->option('forceprod'));
}
}

View File

@ -3,10 +3,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Mail\TestEmail as MailTest; use App\Mail\TestEmail as MailTest;
use App\Models\User; use App\Models\{Site,User};
class TestEmail extends Command class TestEmail extends Command
{ {
@ -15,7 +16,7 @@ class TestEmail extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'test:email {id}'; protected $signature = 'test:email {site : Site ID} {id : User ID} {email? : Alternative Email}';
/** /**
* The console command description. * The console command description.
@ -41,9 +42,11 @@ class TestEmail extends Command
*/ */
public function handle() public function handle()
{ {
Config::set('site',Site::findOrFail($this->argument('site')));
$uo = User::find($this->argument('id')); $uo = User::find($this->argument('id'));
Mail::to($uo->email) Mail::to($this->argument('email') ?? $uo->email)
->send(new MailTest($uo)); ->send(new MailTest($uo));
} }
} }

View File

@ -1,46 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Models\AdslSupplier;
use App\Jobs\BroadbandTraffic;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// @todo This needs to be more generic and dynamic
// Exetel Traffic
$schedule->job(new BroadbandTraffic(AdslSupplier::find(1)))->timezone('Australia/Melbourne')->dailyAt('10:00');
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProviderPaymentCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public array $paymentData;
public string $provider;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(string $provider,array $paymentData)
{
$this->provider = $provider;
$this->paymentData = $paymentData;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -22,6 +22,7 @@ class Handler extends ExceptionHandler
* @var array * @var array
*/ */
protected $dontFlash = [ protected $dontFlash = [
'current_password',
'password', 'password',
'password_confirmation', 'password_confirmation',
]; ];

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Collection;
use App\Models\{ProviderOauth,User};
class AccountingController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Query the accounting system and get a valid list of accounting codes
*
* @param string $provider
* @return Collection
*/
public static function list(string $provider): Collection
{
// @todo This should be a variable
$uo = User::findOrFail(1);
$so = ProviderOauth::where('name',$provider)->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$api = $to->API();
return $api->getItems()
->pluck('pid','Id')
->transform(function($item,$value) { return ['id'=>$value,'value'=>$item]; })
->values();
}
}

View File

@ -4,14 +4,81 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use App\Models\{Account,Payment,PaymentItem,Service,SiteDetail}; use App\Models\{Account,
Charge,
Invoice,
InvoiceItem,
Payment,
PaymentItem,
Service,
SiteDetail};
/**
* The AdminController governs all routes that are prefixed with 'a/'.
*
* This is everything about the configuration of the application as a whole, or administration of a site.
*/
class AdminController extends Controller class AdminController extends Controller
{ {
// @todo Move to reseller
public function service(Service $o) public function service(Service $o)
{ {
return View('a.service',['o'=>$o]); return view('theme.backend.adminlte.a.service')
->with('o',$o);
}
// @todo Move to reseller
public function charge_addedit(Request $request,Charge $o)
{
if ($request->post()) {
$request->validate([
'account_id' => 'required|exists:accounts,id',
'charge_at' => 'required|date',
'service_id' => 'required|exists:services,id',
'quantity' => 'required|numeric|not_in:0',
'amount' => 'required|numeric|min:0.01',
'sweep_type' => 'required|numeric|in:'.implode(',',array_keys(Charge::sweep)),
'type' => 'required|numeric|in:'.implode(',',array_keys(InvoiceItem::type)),
'taxable' => 'nullable|boolean',
'description' => 'nullable|string|max:128',
]);
if (! $o->exists) {
$o->site_id = config('site')->site_id;
$o->user_id = Auth::id();
$o->active = TRUE;
}
$o->forceFill($request->only(['account_id','charge_at','service_id','quantity','amount','sweep_type','type','taxable','description']));
$o->save();
return redirect()
->back()
->with('success','Charge recorded: '.$o->id);
}
return view('theme.backend.adminlte.a.charge.addedit')
->with('o',$o);
}
// @todo Move to reseller
public function charge_pending_account(Request $request,Account $o)
{
return view('theme.backend.adminlte.a.charge.widgets.pending')
->with('list',$o->charges->where('active',TRUE)->where('processed',NULL)->except($request->exclude));
}
/**
* List unprocessed charges
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
// @todo Move to reseller
public function charge_unprocessed()
{
return view('theme.backend.adminlte.a.charge.unprocessed');
} }
/** /**
@ -21,56 +88,98 @@ class AdminController extends Controller
* @param Payment $o * @param Payment $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/ */
public function pay_add(Request $request,Payment $o) // @todo Move to reseller
public function pay_addedit(Request $request,Payment $o)
{ {
if ($request->post()) { if ($request->post()) {
$validation = $request->validate([ $validation = $request->validate([
'account_id' => 'required|exists:ab_account,id', 'account_id' => 'required|exists:accounts,id',
'date_payment' => 'required|date', 'paid_at' => 'required|date',
'checkout_id' => 'required|exists:ab_checkout,id', 'checkout_id' => 'required|exists:checkouts,id',
'total_amt' => 'required|numeric|min:0.01', 'total_amt' => 'required|numeric|min:0.01',
'fees_amt' => 'nullable|numeric|lt:total_amt', 'fees_amt' => 'nullable|numeric|lt:total_amt',
'source_id' => 'nullable|exists:ab_account,id', 'source_id' => 'nullable|exists:accounts,id',
'pending' => 'nullable|boolean', 'pending' => 'nullable|boolean',
'notes' => 'nullable|string', 'notes' => 'nullable|string',
'ip' => 'nullable|ip', 'ip' => 'nullable|ip',
'invoices' => ['nullable','array',function ($attribute,$value,$fail) use ($request) { 'invoices' => ['required','array',function ($attribute,$value,$fail) use ($request) {
if (collect($value)->sum() > $request->post('total_amt')) if (collect($value)->sum('id') > $request->post('total_amt'))
$fail('Allocation is greater than payment total.'); $fail('Allocation is greater than payment total.');
}], }],
'invoices.*.id' => 'nullable|exists:ab_invoice,id', 'invoices.*.id' => ['required',function ($attribute,$value,$fail) {
if (! Invoice::exists(str_replace(str_replace($attribute,'invoice\.','',),'.id','')))
$fail('Invoice doesnt exist in DB');
}],
]); ]);
$oo = new Payment; if (! $o->exists) {
$oo->forceFill($request->only(['account_id','date_payment','checkout_id','checkout_id','total_amt','fees_amt','source_id','pending','notes','ip'])); $o->site_id = config('site')->site_id;
$oo->site_id = config('SITE')->site_id; $o->active = TRUE;
$oo->save(); }
$o->forceFill($request->only(['account_id','paid_at','checkout_id','total_amt','fees_amt','source_id','pending','notes','ip']));
$o->save();
foreach ($validation['invoices'] as $id => $amount) { foreach ($validation['invoices'] as $id => $amount) {
$ooo = new PaymentItem; // See if we already have a payment item that we need to update
$ooo->invoice_id = $id; $items = $o->items->filter(function($item) use ($id) { return $item->invoice_id == $id; });
$ooo->alloc_amt = $amount;
$ooo->site_id = config('SITE')->site_id; if ($items->count() == 1) {
$oo->items()->save($ooo); $oo = $items->pop();
if ($amount['id'] == 0) {
$oo->delete();
continue;
} }
return redirect()->back() } else {
->with('success','Payment recorded'); $oo = new PaymentItem;
$oo->invoice_id = $id;
} }
return view('a.payment.add') $oo->amount = ($oo->invoice->due >= 0) && ($oo->invoice->due-$amount['id'] >= 0) ? $amount['id'] : 0;
// If the amount is empty, ignore it.
if (! $oo->amount)
continue;
$oo->site_id = config('site')->site_id;
$oo->active = TRUE;
$o->items()->save($oo);
}
return redirect()
->back()
->with('success','Payment recorded: '.$o->id);
}
return view('theme.backend.adminlte.a.payment.addedit')
->with('o',$o); ->with('o',$o);
} }
/**
* List unapplied payments
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
// @todo Move to reseller
public function pay_unapplied()
{
return view('theme.backend.adminlte.a.payment.unapplied');
}
/** /**
* Show a list of invoices to apply payments to * Show a list of invoices to apply payments to
* *
* @param Request $request
* @param Account $o * @param Account $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/ */
public function pay_invoices(Account $o) // @todo Move to reseller
public function pay_invoices(Request $request,Account $o)
{ {
return view('a.payment.widgets.invoices') return view('theme.backend.adminlte.a.payment.widgets.invoices')
->with('pid',$request->pid)
->with('o',$o); ->with('o',$o);
} }
@ -85,13 +194,13 @@ class AdminController extends Controller
{ {
if ($request->post()) { if ($request->post()) {
$validated = $request->validate([ $validated = $request->validate([
'site_name' => 'required|string|max:255', 'site_name' => 'required|string|min:2|max:255',
'site_email' => 'required|string|email|max:255', 'site_email' => 'required|string|email|max:255',
'site_address1' => 'required|string|max:255', 'site_address1' => 'required|string|min:2|max:255',
'site_address2' => 'nullable|string|max:255', 'site_address2' => 'nullable|string|min:2|max:255',
'site_city' => 'required|string|max:64', 'site_city' => 'required|string|min:2|max:64',
'site_state' => 'required|string|max:32', 'site_state' => 'required|string|min:2|max:32',
'site_postcode' => 'required|string|max:8', 'site_postcode' => 'required|string|min:2|max:8',
'site_description' => 'nullable|string|min:5', 'site_description' => 'nullable|string|min:5',
'site_phone' => 'nullable|regex:/[0-9 ]+/|min:6|max:12', 'site_phone' => 'nullable|regex:/[0-9 ]+/|min:6|max:12',
'site_fax' => 'nullable|regex:/[0-9 ]+/|min:6|max:12', 'site_fax' => 'nullable|regex:/[0-9 ]+/|min:6|max:12',
@ -102,7 +211,7 @@ class AdminController extends Controller
'email_logo' => 'nullable|image', 'email_logo' => 'nullable|image',
]); ]);
$site = config('SITE'); $site = config('site');
// @todo - not currently rendered in the home page // @todo - not currently rendered in the home page
$validated['social'] = []; $validated['social'] = [];
@ -130,10 +239,11 @@ class AdminController extends Controller
$site->details()->save($oo); $site->details()->save($oo);
} }
return redirect()->back() return redirect()
->back()
->with('success','Settings saved'); ->with('success','Settings saved');
} }
return view('a.setup'); return view('theme.backend.adminlte.theme.backend.adminlte.a.setup');
} }
} }

View File

@ -4,8 +4,6 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller class ForgotPasswordController extends Controller
{ {
@ -21,29 +19,4 @@ class ForgotPasswordController extends Controller
*/ */
use SendsPasswordResetEmails; use SendsPasswordResetEmails;
public function showLinkRequestForm()
{
return view('adminlte::auth.passwords.email');
}
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// If the account is not active, or doesnt exist, we'll send a fake "sent" message.
if (! ($x=$this->broker()->getUser($this->credentials($request))) || (! $x->active))
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$this->credentials($request)
);
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}
} }

View File

@ -3,8 +3,6 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -30,7 +28,7 @@ class LoginController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = RouteServiceProvider::HOME; protected $redirectTo = '/home';
/** /**
* Create a new controller instance. * Create a new controller instance.
@ -39,7 +37,8 @@ class LoginController extends Controller
*/ */
public function __construct() public function __construct()
{ {
$this->middleware('guest')->except('logout'); $this->middleware('auth')
->only('logout');
} }
public function login(Request $request) public function login(Request $request)
@ -73,6 +72,7 @@ class LoginController extends Controller
if (file_exists('login_note.txt')) if (file_exists('login_note.txt'))
$login_note = file_get_contents('login_note.txt'); $login_note = file_get_contents('login_note.txt');
return view('adminlte::auth.login')->with('login_note',$login_note); return view('adminlte::auth.login')
->with('login_note',$login_note);
} }
} }

View File

@ -1,89 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'min:3', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'token' => 'required|doorman:email',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return User
*/
protected function create(array $data): User
{
$fields = [
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
];
if (config('auth.providers.users.field','email') === 'username' && isset($data['username'])) {
$fields['username'] = $data['username'];
}
try {
Doorman::redeem($data['token'],$data['email']);
// @todo Want to direct or display an appropriate error message (although the form validation does it anyway).
} catch (DoormanException $e) {
redirect('/error');
abort(403);
}
return User::create($fields);
}
}

View File

@ -3,9 +3,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller class ResetPasswordController extends Controller
{ {
@ -27,22 +25,5 @@ class ResetPasswordController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = RouteServiceProvider::HOME; protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
public function showResetForm(Request $request, $token = null)
{
return view('adminlte::auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -10,26 +11,27 @@ use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\SocialLink; use App\Mail\SocialLink;
use App\Models\Oauth; use App\Models\{ProviderOauth,ProviderToken,User,UserOauth};
use App\Models\AccountOauth;
use App\Models\User;
use App\Providers\RouteServiceProvider;
class SocialLoginController extends Controller class SocialLoginController extends Controller
{ {
public function redirectToProvider($provider) public function redirectToProvider($provider)
{ {
return Socialite::with($provider)->redirect(); return Socialite::with($provider)
->redirect();
} }
public function handleProviderCallback($provider) public function handleProviderCallback($provider)
{ {
$openiduser = Socialite::with($provider)->user(); $openiduser = Socialite::with($provider)->user();
$oo = Oauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]); if (! $openiduser)
return redirect('/home')->with('error','No user details obtained.');
$oo = ProviderOauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]);
// See if this user has connected and linked previously // See if this user has connected and linked previously
$aoo = $oo->accounts->where('userid',$openiduser->id); $aoo = $oo->users->where('userid',$openiduser->id);
if ($aoo->count() == 1) { if ($aoo->count() == 1) {
$aoo = $aoo->first(); $aoo = $aoo->first();
@ -59,10 +61,10 @@ class SocialLoginController extends Controller
// See if their is an account with this email address // See if their is an account with this email address
if ($uo->count() == 1) { if ($uo->count() == 1) {
$aoo = new AccountOauth; $aoo = new UserOauth;
$aoo->userid = $openiduser->id; $aoo->userid = $openiduser->id;
$aoo->oauth_data = $openiduser->user; $aoo->oauth_data = $openiduser->user;
$oo->accounts()->save($aoo); $oo->users()->save($aoo);
return $this->link($provider,$aoo,$uo->first()); return $this->link($provider,$aoo,$uo->first());
@ -75,19 +77,47 @@ class SocialLoginController extends Controller
} }
} }
return redirect()->intended(RouteServiceProvider::HOME); return redirect()
->intended('/home');
}
public function handleBearerTokenCallback($provider)
{
$openiduser = Socialite::with($provider)->user();
if (! $openiduser)
return redirect('/home')->with('error','No user details obtained.');
$po = ProviderOauth::where('name',$provider)->singleOrFail();
$uoo = ProviderToken::where('user_id',Auth::id())->where('provider_oauth_id',$po->id)->firstOrNew();
$uoo->user_id = Auth::id();
$uoo->access_token = $openiduser->token;
$uoo->access_token_expires_at = Carbon::now()->addSeconds($openiduser->expiresIn);
$uoo->refresh_token = $openiduser->refreshToken;
$uoo->refresh_token_expires_at = Carbon::now()->addSeconds($openiduser->refresh_token_expires_in);
$uoo->realm_id = $openiduser->realmid;
$po->tokens()->save($uoo);
return redirect()
->intended('/home')
->with('success','Token refreshed.');
} }
/** /**
* We have identified the user and oauth, just need them to confirm the link * We have identified the user and oauth, just need them to confirm the link
* *
* @param $provider * @param $provider
* @param UserOauth $ao
* @param User $uo * @param User $uo
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function link($provider,AccountOauth $ao,User $uo) public function link($provider,UserOauth $ao,User $uo): \Illuminate\View\View
{ {
Mail::to($uo->email)->send(new SocialLink($ao)); // @note If this is sent now (send()), it results in the caller to be executed a second time (handleProviderCallback()).
Mail::to($uo->email)->queue(new SocialLink($ao));
return view('auth.social_link') return view('auth.social_link')
->with('oauthid',$ao->id) ->with('oauthid',$ao->id)
@ -97,7 +127,7 @@ class SocialLoginController extends Controller
public function linkcomplete(Request $request,$provider) public function linkcomplete(Request $request,$provider)
{ {
// Load our oauth id // Load our oauth id
$aoo = AccountOauth::findOrFail($request->post('oauthid')); $aoo = UserOauth::findOrFail($request->post('oauthid'));
// Check our email matches // Check our email matches
if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email')) if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email'))
@ -112,8 +142,9 @@ class SocialLoginController extends Controller
$aoo->user_id = $uo->id; $aoo->user_id = $uo->id;
$aoo->save(); $aoo->save();
Auth::login($uo,FALSE); Auth::login($uo);
return redirect()->intended(RouteServiceProvider::HOME); return redirect()
->intended('/home');
} }
} }

View File

@ -2,13 +2,42 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Invoice;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View;
use App\Models\Checkout; use App\Http\Requests\CheckoutAddEdit;
use App\Models\{Checkout,Invoice};
class CheckoutController extends Controller class CheckoutController extends Controller
{ {
/**
* Update a suppliers details
*
* @param CheckoutAddEdit $request
* @param Checkout $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function addedit(CheckoutAddEdit $request,Checkout $o)
{
$this->middleware(['auth','wholesaler']);
foreach ($request->except(['_token','active','submit']) as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->active;
try {
$o->save();
} catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput();
}
return redirect()
->back()
->with('success','Payment saved');
}
public function cart_invoice(Request $request,Invoice $o=NULL) public function cart_invoice(Request $request,Invoice $o=NULL)
{ {
if ($o) { if ($o) {
@ -16,9 +45,10 @@ class CheckoutController extends Controller
} }
if (! $request->session()->get('invoice.cart')) if (! $request->session()->get('invoice.cart'))
return redirect()->to('u/home'); return redirect()
->to('u/home');
return View('u.invoice.cart') return view('theme.backend.adminlte.u.invoice.cart')
->with('invoices',Invoice::find(array_values($request->session()->get('invoice.cart')))); ->with('invoices',Invoice::find(array_values($request->session()->get('invoice.cart'))));
} }
@ -27,8 +57,29 @@ class CheckoutController extends Controller
return $o->fee($request->post('total',0)); return $o->fee($request->post('total',0));
} }
/**
* Render a specific invoice for the user
*
* @return View
*/
public function home(): View
{
return view('theme.backend.adminlte.payment.home');
}
public function pay(Request $request,Checkout $o) public function pay(Request $request,Checkout $o)
{ {
return redirect('pay/paypal/authorise'); return redirect('pay/paypal/authorise');
} }
/**
* View details on a specific payment method
*
* @param Checkout $o
* @return View
*/
public function view(Checkout $o): View
{
return view('theme.backend.adminlte.payment.view',['o'=>$o]);
}
} }

View File

@ -2,12 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class Controller extends BaseController abstract class Controller extends \Illuminate\Routing\Controller
{ {
use AuthorizesRequests, DispatchesJobs, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
} }

View File

@ -4,11 +4,9 @@ namespace App\Http\Controllers;
use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode}; use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode};
use Clarkeash\Doorman\Facades\Doorman; use Clarkeash\Doorman\Facades\Doorman;
use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\View\View; use Illuminate\View\View;
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
use App\Models\{Invoice,Service,User}; use App\Models\{Invoice,Service,User};
@ -22,60 +20,19 @@ use App\Models\{Invoice,Service,User};
*/ */
class HomeController extends Controller class HomeController extends Controller
{ {
public function __construct()
{
$this->middleware('auth');
}
/** /**
* Logged in users home page * Logged in users home page
* *
* @return Factory|View * @param User $o
* @return View
*/ */
public function home(User $o): View public function home(User $o): View
{ {
// If we are passed a user to view, we'll open up their home page. if (! $o->exists)
if ($o->exists) {
$o->load(['accounts','services']);
return View('u.home',['o'=>$o]);
}
// If User was null, then test and see what type of logged on user we have
$o = Auth::user(); $o = Auth::user();
switch (Auth::user()->role()) { return view('theme.backend.adminlte.home')
case 'customer': ->with(['o'=>$o]);
return View('u.home',['o'=>$o]);
case 'reseller':
case 'wholesaler':
return View('r.home',['o'=>$o]);
default:
abort(404,'Unknown role: '.$o->role());
}
}
/**
* Render a specific invoice for the user
*
* @param Invoice $o
* @return View
*/
public function invoice(Invoice $o): View
{
return View('u.invoice.home',['o'=>$o]);
}
/**
* Return the invoice in PDF format, ready to download
*
* @param Invoice $o
* @return mixed
*/
public function invoice_pdf(Invoice $o)
{
return PDF::loadView('u.invoice.home',['o'=>$o])->stream(sprintf('%s.pdf',$o->invoice_account_id));
} }
/** /**
@ -101,18 +58,7 @@ class HomeController extends Controller
abort(404); abort(404);
} }
return $this->invoice_pdf($o); return $this->invoice($o);
}
/**
* Return details on the users service
*
* @param Service $o
* @return View
*/
public function service(Service $o): View
{
return View('u.service.home',['o'=>$o]);
} }
/** /**
@ -122,6 +68,7 @@ class HomeController extends Controller
* @param Service $o * @param Service $o
* @param string $status * @param string $status
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* @deprecated
*/ */
public function service_progress(Service $o,string $status) public function service_progress(Service $o,string $status)
{ {

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers;
use Clarkeash\Doorman\Exceptions\{ExpiredInviteCode,InvalidInviteCode,NotYourInviteCode};
use Clarkeash\Doorman\Facades\Doorman;
use Illuminate\View\View;
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
use App\Models\Invoice;
/**
* Class InvoiceController
* This controller manages invoices
*
* The methods to this controller should be projected by the route
*
* @package App\Http\Controllers
*/
class InvoiceController extends Controller
{
/**
* Return the invoice in PDF format, ready to download
*
* @param Invoice $o
* @return mixed
*/
public function pdf(Invoice $o)
{
return PDF::loadView('theme.backend.adminlte.u.invoice.home',['o'=>$o])
->stream(sprintf('%s.pdf',$o->sid));
}
/**
* Render a specific invoice for the user
*
* @param Invoice $o
* @param string|null $code
* @return View
*/
public function view(Invoice $o,string $code=NULL): View
{
if ($code) {
try {
Doorman::redeem($code,$o->account->user->email);
} catch (ExpiredInviteCode|InvalidInviteCode|NotYourInviteCode $e) {
abort(404);
}
}
return view('theme.backend.adminlte.invoice.view')
->with('o',$o);
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers;
use Image;
class MediaController extends Controller
{
/**
* Create a generic image
*
* @param $width
* @param $height
* @param string $color
* @return mixed
*/
public function image($width,$height,$color='#ccc') {
$io = Image::canvas($width,$height,$color);
$io->text(sprintf('IMAGE-%sx%s',$width,$height),$width/2,$height/2,function($font) {
$font->file(5);
$font->align('center');
$font->valign('middle');
});
return $io->response();
}
}

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Igaster\LaravelTheme\Facades\Theme;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -14,33 +13,36 @@ use App\Models\{Account,Product,Service,User};
class OrderController extends Controller class OrderController extends Controller
{ {
// @todo To check
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
} }
// @todo To check
public function index() public function index()
{ {
return view('order'); return view('theme.backend.adminlte.order.home');
} }
// @todo To check
public function product_order(Product $o) public function product_order(Product $o)
{ {
Theme::set('metronic-fe'); return view('theme.backend.adminlte.order.widget.order')
->with('o',$o);
return view('widgets.product_order',['o'=>$o]);
} }
// @todo To check
public function product_info(Product $o) public function product_info(Product $o)
{ {
Theme::set('metronic-fe'); return view('theme.backend.adminlte.order.widget.info')
->with('o',$o);
return view('widgets.product_description',['o'=>$o]);
} }
// @todo To check
public function submit(Request $request) public function submit(Request $request)
{ {
Validator::make($request->all(),['product_id'=>'required|exists:ab_product,id']) Validator::make($request->all(),['product_id'=>'required|exists:products,id'])
// Reseller // Reseller
->sometimes('account_id','required|email',function($input) use ($request) { ->sometimes('account_id','required|email',function($input) use ($request) {
return is_null($input->account_id) AND is_null($input->order_email_manual); return is_null($input->account_id) AND is_null($input->order_email_manual);
@ -59,7 +61,7 @@ class OrderController extends Controller
$po = Product::findOrFail($request->input('product_id')); $po = Product::findOrFail($request->input('product_id'));
// Check we have the custom attributes for the product // Check we have the custom attributes for the product
$options = $po->orderValidation($request); $order = $po->orderValidation($request);
if ($request->input('order_email_manual')) { if ($request->input('order_email_manual')) {
$uo = User::firstOrNew(['email'=>$request->input('order_email_manual')]); $uo = User::firstOrNew(['email'=>$request->input('order_email_manual')]);
@ -67,12 +69,13 @@ class OrderController extends Controller
// If this is a new client // If this is a new client
if (! $uo->exists) { if (! $uo->exists) {
// @todo Make this automatic // @todo Make this automatic
$uo->site_id = config('SITE')->site_id; $uo->site_id = config('site')->site_id;
$uo->active = FALSE; $uo->active = FALSE;
$uo->firstname = ''; $uo->firstname = '';
$uo->lastname = ''; $uo->lastname = '';
$uo->country_id = config('SITE')->country_id; // @todo This might be wrong $uo->country_id = config('site')->country_id; // @todo This might be wrong
$uo->parent_id = Auth::id() ?: 1; // @todo This should be configured to a default user $uo->parent_id = Auth::id() ?: 1; // @todo This should be configured to a default user
$uo->language_id = config('site')->language_id; // @todo This might be wrong
$uo->active = 1; $uo->active = 1;
$uo->save(); $uo->save();
} }
@ -83,10 +86,8 @@ class OrderController extends Controller
$ao = new Account; $ao = new Account;
//$ao->id = Account::NextId(); //$ao->id = Account::NextId();
// @todo Make this automatic // @todo Make this automatic
$ao->site_id = config('SITE')->site_id; $ao->site_id = config('site')->site_id;
$ao->country_id = config('SITE')->country_id; // @todo This might be wrong $ao->country_id = config('site')->country_id; // @todo This might be wrong
$ao->language_id = config('SITE')->language_id; // @todo This might be wrong
$ao->currency_id = config('SITE')->currency_id; // @todo This might be wrong
$ao->active = 1; $ao->active = 1;
$uo->accounts()->save($ao); $uo->accounts()->save($ao);
@ -97,28 +98,32 @@ class OrderController extends Controller
$so = new Service; $so = new Service;
// @todo Make this automatic // @todo Make this automatic
$so->site_id = config('SITE')->site_id; $so->site_id = config('site')->site_id;
$so->product_id = $request->input('product_id'); $so->product_id = $po->id;
$so->order_status = 'ORDER-SUBMIT'; $so->order_status = 'ORDER-SUBMIT';
$so->orderby_id = Auth::id(); $so->ordered_by = Auth::id();
$so->model = get_class($options); $so->active = FALSE;
$so->model = $order ? get_class($order) : NULL;
$so->recur_schedule = $po->billing_interval;
if ($options->order_info) { if ($order && $order->order_info) {
$so->order_info = $options->order_info; $so->order_info = $order->order_info;
unset($options->order_info); unset($order->order_info);
} }
$so = $ao->services()->save($so); $so = $ao->services()->save($so);
if ($options instanceOf Model) { if ($order instanceOf Model) {
$options->service_id = $so->id; $order->service_id = $so->id;
$options->save(); $order->save();
} }
// @todo Move this email to a config item
Mail::to('help@graytech.net.au') Mail::to('help@graytech.net.au')
->queue((new OrderRequest($so,$request->input('options.notes')))->onQueue('email')); //@todo Get email from DB. ->queue((new OrderRequest($so,$request->input('options.notes') ?: ''))->onQueue('email')); //@todo Get email from DB.
return view('order_received',['o'=>$so]); return view('theme.backend.adminlte.order_received')
->with('o',$so);
} }
} }

View File

@ -200,6 +200,7 @@ class PaypalController extends Controller
foreach ($response->result->purchase_units as $pu) { foreach ($response->result->purchase_units as $pu) {
foreach ($pu->payments->captures as $cap) { foreach ($pu->payments->captures as $cap) {
$po = new Payment; $po = new Payment;
$po->active = TRUE;
switch ($cap->status) { switch ($cap->status) {
case 'PENDING': case 'PENDING':
@ -217,7 +218,7 @@ class PaypalController extends Controller
break; break;
} }
$po->date_payment = Carbon::parse($cap->create_time); $po->paid_at = Carbon::parse($cap->create_time);
$po->checkout_id = $this->o->id; $po->checkout_id = $this->o->id;
$po->checkout_data = $cap->id; $po->checkout_data = $cap->id;
@ -229,7 +230,7 @@ class PaypalController extends Controller
$pio = new PaymentItem; $pio = new PaymentItem;
$pio->site_id = 1; // @todo To implement $pio->site_id = 1; // @todo To implement
$pio->invoice_id = $cap->invoice_id; $pio->invoice_id = $cap->invoice_id;
$pio->alloc_amt = $cap->amount->value-$po->fees_amt; $pio->amount = $cap->amount->value-$po->fees_amt;
$po->items->push($pio); $po->items->push($pio);

View File

@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use App\Http\Requests\ProductAddEdit;
use App\Models\{Product,ProductTranslate};
class ProductController extends Controller
{
/**
* Get a list of products that meet a type
*
* @param Request $request
* @return Collection
* @throws \Exception
*/
public function api_supplier_products(Request $request): Collection
{
switch ($request->type) {
case Product\Broadband::class:
return Product\Broadband::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
case Product\Domain::class:
return Product\Domain::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
case Product\Email::class:
return Product\Email::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
case Product\Generic::class:
return Product\Generic::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
case Product\Host::class:
return Product\Host::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
case Product\Phone::class:
return Product\Phone::select(['id','supplier_item_id'])
->with(['supplied.supplier_detail.supplier'])
->get()
->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier->name,$item->supplied->name)]; })
->sortBy('name')
->values();
default:
throw new \Exception('Unknown type: '.$request->type);
}
}
/**
* Update a suppliers details
*
* @param Request $request
* @param Product $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function details(Request $request,Product $o)
{
if (! $o->exists && $request->name)
$o = Product::where('name',$request->name)->firstOrNew();
return view('theme.backend.adminlte.product.details')
->with('breadcrumb',collect(['Products'=>url('a/product')]))
->with('o',$o);
}
public function details_addedit(ProductAddEdit $request,Product $o)
{
foreach ($request->except(['_token','submit','translate','accounting']) as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->active;
// Trim down the pricing array, remove null values
$o->pricing = $o->pricing->map(function($item) {
foreach ($item as $k=>$v) {
if (is_array($v)) {
$v = array_filter($v);
$item[$k] = $v;
}
}
return $item;
});
try {
$o->save();
} catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput();
}
$o->load(['translate']);
$oo = $o->translate ?: new ProductTranslate;
foreach ($request->get('translate',[]) as $key => $item)
$oo->{$key} = $item;
$o->translate()->save($oo);
if ($request->accounting)
foreach ($request->accounting as $k=>$v)
$o->providers()->syncWithoutDetaching([
$k => [
'ref' => $v,
'site_id'=>$o->site_id,
],
]);
return redirect()
->back()
->with('success','Product saved');
}
/**
* Manage products for a site
*
* @note This method is protected by the routes
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function home()
{
return view('theme.backend.adminlte.product.home');
}
}

View File

@ -2,27 +2,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\Account;
class ResellerServicesController extends Controller class ResellerServicesController extends Controller
{ {
public function accounts() public function services(Request $request,Account $o)
{ {
return ['data'=>Auth::user()->all_accounts()->values()]; return $o->services
} ->filter(function($item) use ($request) {
return $item->active || ($item->id == $request->include);
public function agents() });
{
return ['data'=>Auth::user()->all_agents()->values()];
}
public function clients()
{
return ['data'=>Auth::user()->all_clients()->values()];
}
public function service_inactive()
{
return ['data'=>Auth::user()->all_client_service_inactive()->values()];
} }
} }

View File

@ -3,10 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use App\Models\{Account,Invoice,Service,Service\Adsl,User}; use App\Models\{Account,Invoice,Payment,Service,Supplier,User};
class SearchController extends Controller class SearchController extends Controller
{ {
@ -14,73 +15,86 @@ class SearchController extends Controller
* Search from the Application Dashboard. * Search from the Application Dashboard.
* *
* @param Request $request * @param Request $request
* @return Response * @return Collection
*/ */
public function search(Request $request) public function search(Request $request): Collection
{ {
$result = collect();
// If the user isnt logged in
if (! Auth::user())
abort(401,'Need to login');
// If there isnt a term value, return null // If there isnt a term value, return null
if (! $request->input('term')) if (! $request->input('term'))
return []; return $result;
$result = collect(); $account_ids = ($x=Auth::user()->accounts)->pluck('id');
$accounts = ($x=Auth::user()->all_accounts())->pluck('id'); $user_ids = $x->transform(function($item) { return $item->user;})->pluck('id');
$users = $x->transform(function($item) { return $item->user;});
# Look for User # Look for User
foreach (User::Search($request->input('term')) foreach (User::Search($request->input('term'))
->whereIN('id',$users->pluck('id')) ->whereIN('id',$user_ids)
->orderBy('lastname') ->orderBy('lastname')
->orderBy('firstname') ->orderBy('firstname')
->limit(10)->get() as $o) ->limit(10)->get() as $o)
{ {
$result->push(['name'=>sprintf('%s %s',$o->aid,$o->name),'value'=>'/u/home/'.$o->id,'category'=>'Users']); $result->push(['name'=>sprintf('%s (%s) - %s',$o->name,$o->lid,$o->email),'value'=>'/u/home/'.$o->id,'category'=>'Users']);
}
# Look for User by Supplier
if (is_numeric($request->input('term')))
foreach (User::select(['users.*','suppliers.name AS supplier_name','supplier_user.id AS pivot_id'])
->join('supplier_user',['supplier_user.user_id'=>'users.id'])
->join('suppliers',['suppliers.id'=>'supplier_user.supplier_id'])
->whereIN('user_id',$user_ids)
->where('supplier_user.id','like','%'.$request->input('term').'%')
->orderBy('lastname')
->orderBy('firstname')
->limit(10)->get() as $o)
{
$result->push(['name'=>sprintf('%s (%s:%s)',$o->name,$o->supplier_name,$o->pivot_id),'value'=>'/u/home/'.$o->id,'category'=>'Suppliers']);
} }
# Look for Account # Look for Account
foreach (Account::Search($request->input('term')) foreach (Account::Search($request->input('term'))
->whereIN('user_id',$users->pluck('id')) ->whereIN('user_id',$user_ids)
->orderBy('company') ->orderBy('company')
->limit(10)->get() as $o) ->limit(10)->get() as $o)
{ {
$result->push(['name'=>sprintf('%s %s',$o->aid,$o->company),'value'=>'/u/home/'.$o->user_id,'category'=>'Accounts']); $result->push(['name'=>sprintf('%s (%s)',$o->company,$o->lid),'value'=>'/u/home/'.$o->user_id,'category'=>'Accounts']);
} }
# Look for a Service # Look for a Service
foreach (Service::Search($request->input('term')) foreach (Service::Search($request->input('term'))
->whereIN('account_id',$accounts) ->whereIN('account_id',$account_ids)
->orderBy('id') ->orderBy('id')
->limit(10)->get() as $o) ->limit(20)->get() as $o)
{ {
$result->push(['name'=>sprintf('%s (%s)',$o->name,$o->sid),'value'=>'/u/service/'.$o->id,'category'=>'Services']); $result->push(['name'=>sprintf('%s (%s) %s',$o->name,$o->lid,$o->active ? '' : '<small>INACT</small>'),'value'=>'/u/service/'.$o->id,'category'=>$o->category_name]);
} }
# Look for an Invoice # Look for an Invoice
foreach (Invoice::Search($request->input('term')) foreach (Invoice::Search($request->input('term'))
->whereIN('account_id',$accounts) ->whereIN('account_id',$account_ids)
->orderBy('id') ->orderBy('id')
->limit(10)->get() as $o) ->limit(10)->get() as $o)
{ {
$result->push(['name'=>sprintf('%s: %s',$o->sid,$o->account->name),'value'=>'/u/invoice/'.$o->id,'category'=>'Invoices']); $result->push(['name'=>sprintf('%s: %s',$o->lid,$o->account->name),'value'=>'/u/invoice/'.$o->id,'category'=>'Invoices']);
} }
# Look for an ADSL/NBN Service if (Gate::any(['wholesaler'],new Payment)) {
foreach (Adsl::Search($request->input('term')) # Look for Payments
->whereIN('account_id',$accounts) foreach (Payment::Search($request->input('term'))
->orderBy('service_number') ->whereIN('account_id',$account_ids)
->limit(10)->get() as $o) ->limit(10)->get() as $o)
{ {
$result->push(['name'=>sprintf('%s (%s)',$o->name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Broadband']); $result->push(['name'=>sprintf('%s: %s $%s',$o->lid,$o->account->name,number_format($o->total,2)),'value'=>'/a/payment/addedit/'.$o->id,'category'=>'Payments']);
}
} }
# Look for Domain Name return $result
foreach (Service\Domain::Search($request->input('term')) ->sortBy(function($item) { return $item['category'].$item['name']; })
->whereIN('account_id',$accounts) ->values();
->orderBy('domain_name')
->limit(10)->get() as $o)
{
$result->push(['name'=>sprintf('%s (%s)',$o->service_name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Domains']);
}
return $result;
} }
} }

View File

@ -2,146 +2,463 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View; use Illuminate\View\View;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Models\Service; use App\Http\Requests\ServiceChangeRequest;
use App\Mail\{CancelRequest,ChangeRequest};
use App\Models\{Charge,Product,Service};
class ServiceController extends Controller class ServiceController extends Controller
{ {
/* SERVICE WORKFLOW METHODS */
/** /**
* Edit a domain service details * Cancel a request to cancel a service
*
* @param Service $o
* @return bool
*/
private function action_cancel_cancel(Service $o): bool
{
if (! $o->order_info)
$o->order_info = collect();
$o->order_info->put('cancel_cancel',Carbon::now()->format('Y-m-d H:i:s'));
$o->order_status = 'ACTIVE';
return $o->save();
}
private function action_cancel_pending_enter(Service $o): bool
{
$o->order_status = 'CANCEL-PENDING';
return $o->save();
}
private function action_cancelled(Service $o): bool
{
$o->order_status = 'CANCELLED';
$o->active = FALSE;
return $o->save();
}
/**
* Cancel a request to change a service
*
* @param Service $o
* @return bool
*/
private function action_change_cancel(Service $o): bool
{
if (! $o->order_info)
$o->order_info = collect();
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
$np->pivot->active = FALSE;
$np->pivot->save();
$o->order_status = 'ACTIVE';
return $o->save();
}
/**
* Action to change a service order_status to another stage
* This is a generic function that can redirect the user to a page that is required to completed to enter
* the new stage
*
* @param Service $o
* @param string $stage
* @return \Illuminate\Contracts\Foundation\Application|RedirectResponse|\Illuminate\Routing\Redirector
*/
private function action_request_enter_redirect(Service $o,string $stage)
{
return redirect(sprintf('u/service/%d/%s',$o->id,strtolower($stage)));
}
/* OTHER METHODS */
public function change_pending(ServiceChangeRequest $request,Service $o)
{
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
if ($request->post()) {
foreach ($this->service_change_charges($request,$o) as $co)
$co->save();
$o->product_id = Arr::get($request->broadband,'product_id');
$o->price = Arr::get($request->broadband,'price');
$o->order_status = 'ACTIVE';
$o->save();
$np->pivot->complete = TRUE;
$np->pivot->effective_at = Carbon::now();
$np->pivot->save();
return redirect()->to(url('u/service',[$o->id]));
}
return view('theme.backend.adminlte.service.change_pending')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o)
->with('np',$np);
}
/**
* Process a request to cancel a service
* *
* @param Request $request * @param Request $request
* @param Service $o * @param Service $o
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|RedirectResponse|\Illuminate\Routing\Redirector
*/ */
public function domain_edit(Request $request,Service $o) public function cancel_request(Request $request,Service $o)
{ {
session()->flash('service_update',TRUE); if ($request->post()) {
$request->validate([
$validation = $request->validate([ 'stop_at'=>'required|date',
'service.domain_name' => sprintf('required|unique:%s,domain_name,%d',$o->type->getTable(),$o->type->id),
'service.domain_expire' => 'required|date',
'service.domain_tld_id' => 'required|exists:ab_domain_tld,id',
'service.domain_registrar_id' => 'required|exists:ab_domain_registrar,id',
'service.registrar_account' => 'required',
'service.registrar_username' => 'required|string|min:5',
'service.registrar_ns' => 'required|string|min:5',
]); ]);
$o->type->forceFill($validation['service'])->save(); if (! $o->order_info)
$o->order_info = collect();
return redirect()->back()->with('success','Record updated.'); $o->stop_at = $request->stop_at;
$o->order_info->put('cancel_note',$request->notes);
$o->order_status = 'CANCEL-REQUEST';
$o->save();
//@todo Get email from DB.
Mail::to('help@graytech.net.au')
->queue((new CancelRequest($o,$request->notes))->onQueue('email'));
return redirect('u/service/'.$o->id)->with('success','Cancellation lodged');
}
return view('theme.backend.adminlte.service.cancel_request')
->with('o',$o);
}
/**
* Change the status of a service
* @todo This needs to be optimized
*
* @note This route is protected by middleware @see ServicePolicy::progress()
* It is assumed that the next stage is valid for the services current stage - validated in ServicePolicy::progress()
* @param Service $o
* @param string $stage
* @return RedirectResponse
*/
public function change(Service $o,string $stage): RedirectResponse
{
// While stage has a string value, that indicates the next stage we want to go to
// If stage is NULL, the current stage hasnt been completed
// If stage is FALSE, then the current stage failed, and may optionally be directed to another stage.
while ($stage) {
// Check that stage is a valid next action for the user currently performing it
//$current = $this->getStageParameters($this->order_status);
$next = $o->getStageParameters($stage);
// If valid, call the method to confirm that the current stage is complete
if ($x=$next->get('enter_method')) {
if (! method_exists($this,$x))
abort(500,sprintf('ENTER_METHOD [%s]defined doesnt exist',$x));
Log::debug(sprintf('Running ENTER_METHOD [%s] on Service [%d] to go to stage [%s]',$x,$o->id,$stage));
// @todo Should call exit_method of the current stage first, to be sure we can change
try {
$result = $this->{$x}($o,$stage);
// If we have a form to complete, we need to return with a URL, so we can catch that with an Exception
} catch (HttpException $e) {
if ($e->getStatusCode() == 301)
return ($e->getMessage());
}
// An Error Condition
if (is_null($result))
return redirect()->to('u/service/'.$o->id);
elseif ($result instanceof RedirectResponse)
return $result;
// The service cannot enter the next stage
elseif (! $result)
abort(500,'Current Method FAILED: '.$result);
} else {
$o->order_status = $stage;
if ($stage == 'ACTIVE')
$o->active = TRUE;
$o->save();
}
// If valid, call the method to start the next stage
$stage = ''; // @todo this is temporary, we havent written the code to automatically jump to the next stage if wecan
}
return redirect()->to('u/service/'.$o->id);
}
/**
* Process a request to cancel a service
*
* @param Request $request
* @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|RedirectResponse|\Illuminate\Routing\Redirector
*/
public function change_request(Request $request,Service $o)
{
if ($request->post()) {
$request->validate([
'product_id'=>'required|exists:products,id',
'change_date'=>'required|date',
'notes'=>'nullable|min:10',
]);
$o->changes()->attach([$o->id=>[
'site_id'=> $o->site_id,
'ordered_by' => Auth::id(),
'ordered_at' => Carbon::now(),
'effective_at' => $request->change_date,
'product_id' => $request->product_id,
'notes' => $request->notes,
'active' => TRUE,
'complete' => FALSE,
]]);
$o->order_status = 'CHANGE-REQUEST';
$o->save();
//@todo Get email from DB.
Mail::to('help@graytech.net.au')
->queue((new ChangeRequest($o,$request->notes))->onQueue('email'));
return redirect('u/service/'.$o->id)->with('success','Upgrade requested');
}
switch (get_class($o->type)) {
default:
return view('theme.backend.adminlte.service.change_request')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
} }
/** /**
* List all the domains managed by the user * List all the domains managed by the user
* *
* @return View * @return View
* @todo revalidate
*/ */
public function domain_list(): View public function domain_list(): View
{ {
$o = Service\Domain::serviceActive() $o = Service\Domain::serviceActive()
->serviceUserAuthorised(Auth::user()) ->serviceUserAuthorised(Auth::user())
->select('service_domains.*') ->select('service_domain.*')
->join('ab_service',['ab_service.id'=>'service_domains.service_id']) ->join('services',['services.id'=>'service_domain.service_id'])
->with(['service.account','registrar']) ->with(['service.account','registrar'])
->get(); ->get();
return view('r.service.domain.list') return view('theme.backend.adminlte.service.domain.list')
->with('o',$o);
}
public function email_list(): View
{
// @todo Need to add the with path when calculating next_billed and price
$o = Service\Email::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_email.*')
->join('services',['services.id'=>'service_email.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.email.list')
->with('o',$o); ->with('o',$o);
} }
/** /**
* Update a service * Return details on the users service
*
* @param Service $o
* @return View
*/
public function home(Service $o): View
{
return view('theme.backend.adminlte.service.home')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
public function hosting_list(): View
{
// @todo Need to add the with path when calculating next_billed and price
$o = Service\Host::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_host.*')
->join('services',['services.id'=>'service_host.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.host.list')
->with('o',$o);
}
private function service_change_charges(Request $request,Service $o): Collection
{
$charges = collect();
$po = Product::findOrFail(Arr::get($request->broadband,'product_id'));
$start_at = Carbon::create(Arr::get($request->broadband,'start_at'));
// Get the invoiced items covering the start_at date
foreach ($o->invoice_items->filter(function($item) use ($start_at) {
return ($item->start_at < $start_at) && ($item->stop_at > $start_at) && ($item->item_type === 0);
}) as $iio)
{
// Reverse the original charge
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $iio->product_id;
$co->description = 'Plan Upgrade Adjustment';
$co->user_id = Auth::id();
$co->type = $iio->item_type;
$co->start_at = $start_at;
$co->stop_at = $iio->stop_at;
$co->amount = $iio->price_base;
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = -1*$start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co);
// Add the new charge
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $po->id;
$co->description = 'Plan Upgrade Adjustment';
$co->user_id = Auth::id();
$co->type = $iio->item_type;
$co->start_at = $start_at;
$co->stop_at = $iio->stop_at;
$co->amount = Arr::get($request->broadband,'price') ?: $po->base_charge;
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = $start_at->diff($iio->stop_at)->days/$iio->start_at->diff($iio->stop_at)->days;
$charges->push($co);
}
// Add any fee
if (Arr::get($request->broadband,'change_fee')) {
$co = new Charge;
$co->active = TRUE;
$co->service_id = $o->id;
$co->account_id = $o->account_id;
$co->sweep_type = 6;
$co->product_id = $po->id;
$co->description = 'Plan Upgrade Fee';
$co->user_id = Auth::id();
$co->type = 3;
$co->start_at = $start_at;
$co->stop_at = $start_at;
$co->amount = Arr::get($request->broadband,'change_fee');
$co->taxable = TRUE; // @todo this should be determined
$co->quantity = 1;
$charges->push($co);
}
return $charges;
}
/**
* This is an API method, that works with service change - to return the new charges as a result of changing a service
* *
* @note: Route Middleware protects this path
* @param Request $request * @param Request $request
* @param Service $o * @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function service_change_charges_display(Request $request,Service $o)
{
return view('theme.backend.adminlte.a.charge.service_change')
->with('charges',$this->service_change_charges($request,$o));
}
/**
* Update details about a service
*
* @param Request $request
* @param Service $o
* @return RedirectResponse
* @throws ValidationException
*/ */
public function update(Request $request,Service $o) public function update(Request $request,Service $o)
{ {
switch ($o->order_status) { if ($o->type->validation()) {
case 'CANCEL-REQUEST': Session::put('service_update',true);
if ($request->post()) { $validator = Validator::make($x=$request->post($o->category),$o->type->validation());
if (! $request->post('date_end'))
return redirect()->back()->withErrors('Cancellation Date not provided');
$o->date_end = $request->post('date_end'); if ($validator->fails()) {
return redirect()
foreach (['cancel_notes'] as $key) { ->back()
if ($request->post($key)) ->withErrors($validator)
$o->setOrderInfo($key,$request->post($key)); ->withInput();
} }
$o->order_status='CANCEL-PENDING'; $o->type->forceFill($validator->validated());
$o->save();
return redirect()->to(url('u/service',$o->id))->with('updated','Service cancellation submitted.'); } elseif ($request->post($o->product->category)) {
} $o->type->forceFill($request->post($o->product->category));
return $this->update_request_cancel($o);
case 'ORDER-SENT':
if ($request->post()) {
foreach (['reference','notes'] as $key) {
$o->setOrderInfo($key,$request->post($key));
}
$o->save();
foreach ($request->post($o->stype) as $k=>$v) {
$o->type->{$k} = $v;
} }
$o->type->save(); $o->type->save();
return redirect()->to(url('u/service',$o->id))->with('updated','Order sent notes updated.'); if ($request->post('invoice_next_at'))
$o->invoice_next_at = $request->invoice_next_at;
if ($request->post('recur_schedule'))
$o->recur_schedule = $request->recur_schedule;
$o->suspend_billing = ($request->suspend_billing == 'on');
$o->external_billing = ($request->external_billing == 'on');
$o->price = $request->price ?: NULL;
// Also update our service start_at date.
// @todo We may want to make start_at/stop_at dynamic values calculated by the type records
if ($request->post('start_at'))
$o->start_at = $request->start_at;
else {
// For broadband, start_at is connect_at in the type record
switch ($o->category) {
case 'broadband':
$o->start_at = $o->type->connect_at;
break;
} }
return $this->update_order_status($o);
case 'PROVISION-PLANNED':
if ($request->post()) {
foreach (['provision_notes'] as $key) {
$o->setOrderInfo($key,$request->post($key));
} }
$o->date_start = $request->post('date_start');
$o->save(); $o->save();
foreach ($request->post($o->stype) as $k=>$v) { return redirect()->back()->with('success','Record Updated');
$o->type->{$k} = $v;
}
$o->type->save();
return redirect()->to(url('u/service',$o->id))->with('updated','Order sent notes updated.');
}
return $this->update_provision_planned($o);
default:
abort(499,'Not yet implemented');
}
}
private function update_order_status(Service $o)
{
return View('r.service.order.sent',['o'=>$o]);
}
private function update_request_cancel(Service $o)
{
return View('u.service.order.cancel',['o'=>$o]);
}
private function update_provision_planned(Service $o)
{
return View('r.service.order.provision_plan',['o'=>$o]);
} }
} }

View File

@ -0,0 +1,223 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Requests\{SupplierAddEdit,SupplierProductAddEdit};
use App\Models\{Cost,Supplier,SupplierDetail};
use App\Jobs\ImportCosts;
class SupplierController extends Controller
{
/**
* Update a suppliers details
*
* @param SupplierAddEdit $request
* @param Supplier $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function addedit(SupplierAddEdit $request,Supplier $o)
{
$this->middleware(['auth','wholesaler']);
foreach ($request->except(['_token','supplier_details','api_key','api_secret','submit']) as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->active;
try {
$o->save();
} catch (\Exception $e) {
return redirect()->back()->withErrors($e->getMessage())->withInput();
}
$o->load(['detail']);
$oo = $o->detail ?: new SupplierDetail;
foreach ($request->get('supplier_details',[]) as $key => $item)
$oo->{$key} = $item;
$oo->connections = $oo->connections->merge([
'api_key'=>$request->get('api_key'),
'api_secret'=>$request->get('api_secret'),
])->filter();
$o->detail()->save($oo);
return redirect()
->back()
->with('success','Supplier Saved');
}
/**
* Site up site wide suppliers, or a site's supplier details
*
* @note This method is protected by the routes
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function admin_home()
{
$this->middleware(['auth','wholesaler']);
return view('theme.backend.adminlte.supplier.home');
}
/**
* Show the suppliers invoice
*
* @param Cost $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function cost(Cost $o)
{
// @todo Need to add the services that are active that are not on the bill for the supplier.
return view('theme.backend.adminlte.supplier.cost.view')
->with('o',$o);
}
public function cost_add(Supplier $o)
{
return view('theme.backend.adminlte.supplier.cost.add')
->with('o',$o);
}
public function cost_submit(Request $request,Supplier $o)
{
$request->validate([
'file' => 'required|filled',
'billed_at' => 'required|date',
]);
$filename = $request->file('file')->store('cost_import');
ImportCosts::dispatch(
config('site'),
$o,
Carbon::create($request->billed_at),
$filename,
)->onQueue('low');
return redirect()->back()->with('success','File uploaded');
}
/**
* New Product from a supplier
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_add()
{
return view('theme.backend.adminlte.supplier.product.addedit')
->with('o',new Supplier)
->with('oo',NULL);
}
public function product_addedit(SupplierProductAddEdit $request,Supplier $o,int $id,string $type)
{
// Quick validation
if ($type !== $request->offering_type)
abort(500,'Type and offering type do not match');
if ($o->exists && ($o->detail->id !== (int)$request->supplier_detail_id))
abort(500,sprintf('Supplier [%d] and supplier_detail_id [%d] do not match',$o->detail->id,$request->supplier_detail_id));
switch ($request->offering_type) {
case 'broadband':
$oo = Supplier\Broadband::findOrNew($id);
// @todo these are broadband requirements - get them from the broadband class.
foreach ($request->only([
'supplier_detail_id',
'product_id'.
'product_desc',
'base_cost',
'setup_cost',
'contract_term',
'metric',
'speed',
'technology',
'offpeak_start',
'offpeak_end',
'base_down_peak',
'base_up_peak',
'base_down_offpeak',
'base_up_offpeak',
'extra_down_peak',
'extra_up_peak',
'extra_down_offpeak',
'extra_up_offpeak',
]) as $key => $value)
$oo->$key = $value;
// Our boolean values
foreach ($request->only(['active','extra_shaped','extra_charged']) as $key => $value)
$oo->$key = ($value == 'on' ? 1 : 0);
break;
default:
throw new \Exception('Unknown offering type:'.$request->offering_type);
}
$oo->save();
return redirect()->back()
->with('success','Saved');
}
/**
* Edit a supplier product
*
* @param Supplier $o
* @param int $id
* @param string $type
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_view(Supplier $o,int $id,string $type)
{
$oo = $o->detail->find($type,$id);
$oo->load(['products.product.services.product.type']);
return view('theme.backend.adminlte.supplier.product.addedit')
->with('o',$o)
->with('oo',$oo);
}
/**
* Return the form for a specific product type
*
* @param Request $request
* @param string $type
* @param int|null $id
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_view_type(Request $request,string $type,int $id=NULL)
{
$o = $id ? Supplier::offeringTypeClass($type)->findOrFail($id) : NULL;
if ($request->old)
$request->session()->flashInput($request->old);
if ($o)
$o->load(['products.product.services']);
return view('theme.backend.adminlte.supplier.product.widget.'.$type)
->with('o',$id ? $o : NULL)
->withErrors($request->errors);
}
/**
* View a supplier.
*
* @param Supplier|null $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function view(?Supplier $o)
{
$this->middleware(['auth','wholesaler']);
return view('theme.backend.adminlte.supplier.details')
->with('o',$o);
}
}

View File

@ -1,102 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Supplier;
use Illuminate\Http\Request;
class SuppliersController extends Controller
{
public function __construct()
{
$this->middleware('auth ');
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('r/supplier/index');
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('r/supplier/create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// @todo Insure site index is included.
$o = new Supplier;
$o->name = $request->input('name');
$o->active = 1;
$o->account_mgr = '';
$o->account_email = '';
$o->email_provision = '';
$o->email_support = '';
$o->phone_provision = '';
$o->phone_support = '';
$o->save();
echo 'REDIRECT TO <a href="'.url('r/supplier/index').'">here</a>';
}
/**
* Display the specified resource.
*
* @param \App\suppliers $suppliers
* @return \Illuminate\Http\Response
*/
public function show(suppliers $suppliers)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\suppliers $suppliers
* @return \Illuminate\Http\Response
*/
public function edit(suppliers $suppliers)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\suppliers $suppliers
* @return \Illuminate\Http\Response
*/
public function update(Request $request, suppliers $suppliers)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\suppliers $suppliers
* @return \Illuminate\Http\Response
*/
public function destroy(suppliers $suppliers)
{
//
}
}

View File

@ -24,13 +24,13 @@ class AccountController extends Controller
}); });
// Get our invoice due date for this invoice // Get our invoice due date for this invoice
$io->due_date = $s->min(function($item) { return $item->invoice_next; }); $io->due_at = $s->min(function($item) { return $item->invoice_next; });
// @todo The days in advance is an application parameter // @todo The days in advance is an application parameter
$io->date_orig = $io->due_date->subDays(30); $io->created_at = $io->due_at->subDays(30);
// Work out items to add to this invoice, plus any in the next additional days // Work out items to add to this invoice, plus any in the next additional days
$days = now()->diffInDays($io->due_date)+1+7; $days = now()->diffInDays($io->due_at)+1+7;
foreach ($s as $so) foreach ($s as $so)
{ {
if ($so->isInvoiceDueSoon($days)) if ($so->isInvoiceDueSoon($days))
@ -38,6 +38,7 @@ class AccountController extends Controller
$io->items->push($o); $io->items->push($o);
} }
return View('u.invoice.home',['o'=>$io]); return view('theme.backend.adminlte.u.invoice.home')
->with('o',$io);
} }
} }

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rule;
use App\Models\{Supplier,User};
class UserController extends Controller
{
/**
* Add a supplier to a user's profile
*
* @param Request $request
* @param User $o
* @return \Illuminate\Http\RedirectResponse
*/
public function supplier_addedit(Request $request,User $o)
{
Session::put('supplier_update',true);
$validated = $request->validate([
'id'=> ['required','string',Rule::unique('supplier_user')->where(fn ($query) => $query->where('supplier_id',$request->supplier_id)->where('user_id','<>',$o->id))],
'supplier_id'=>'required|exists:suppliers,id',
]);
$o->suppliers()->attach([
$validated['supplier_id'] => [
'id'=>$validated['id'],
'site_id'=>$o->site_id,
'created_at'=>Carbon::now(),
]
]);
return redirect()
->back()
->with('success','Supplier Added');
}
/**
* Remove a supplier from a user's profile
*
* @param User $o
* @param Supplier $so
* @return \Illuminate\Http\RedirectResponse
*/
public function supplier_delete(User $o,Supplier $so)
{
Session::put('supplier_update',true);
$o->suppliers()->detach([$so->id]);
return redirect()
->back()
->with('success','Supplier Deleted');
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers;
class WelcomeController extends Controller
{
public function __construct()
{
$this->middleware('demoMode');
}
public function home() {
return view('welcome.home');
}
public function under_construction() {
abort(499,'Under Construction');
}
}

View File

@ -6,8 +6,18 @@ use App\Http\Controllers\Controller;
class ReportController extends Controller class ReportController extends Controller
{ {
public function accounts()
{
return view('account/report');
}
public function products() public function products()
{ {
return view('a/product/report'); return view('product/report');
}
public function services()
{
return view('service/report');
} }
} }

View File

@ -1,86 +0,0 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\SetSite::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \App\Http\Middleware\SetSite::class,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'demoMode' => \Spatie\DemoMode\DemoMode::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'role' => \App\Http\Middleware\Role::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'theme' => \Igaster\LaravelTheme\Middleware\setTheme::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
'toggleState',
];
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}

View File

@ -2,15 +2,19 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Closure; use Closure;
/**
* Enable us to use the role during middleware authorisation
*/
class Role class Role
{ {
public function handle($request, Closure $next, $role) public function handle(Request $request, Closure $next, string $role)
{ {
if ($role AND ! Auth::user()) if ($role AND ! Auth::user())
return abort(303,'Not Authenticated'); abort(403,'Not Authenticated');
switch ($role) { switch ($role) {
case 'wholesaler': case 'wholesaler':

View File

@ -2,6 +2,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
@ -18,13 +19,11 @@ use App\Models\Site;
class SetSite class SetSite
{ {
/** /**
* Handle an incoming request. * @param Request $request
* * @param Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request,Closure $next)
{ {
$so = new Site; $so = new Site;
@ -43,7 +42,7 @@ class SetSite
} }
// Set who we are in SETUP. // Set who we are in SETUP.
Config::set('SITE',$so); Config::set('site',$so);
if (! $request->ajax()) if (! $request->ajax())
View::share('site',$so); View::share('site',$so);

View File

@ -1,18 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
/**
* Editing Suppliers
*/
class CheckoutAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->isWholesaler();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => 'required|string|min:2|max:255',
'active' => 'sometimes|accepted',
'description' => 'nullable|string|min:2|max:255',
];
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
/**
* Editing Suppliers
*/
class ProductAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->isWholesaler();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'translate.name_short' => 'required|string|min:2|max:100',
'translate.name_detail' => 'required|string|min:2|max:100',
'translate.description' => 'required|string|min:2|max:65535',
'active' => 'sometimes|accepted',
'model' => 'sometimes|string', // @todo Check that it is a valid model type
'model_id' => 'sometimes|int', // @todo Check that it is a valid model type
'accounting' => 'nullable|array', // @todo Validate that the value is in the accounting system
'pricing' => 'required|array', // @todo Validate the elements in the pricing
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ServiceChangeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->route('o')->serviceUserAuthorised(Auth::user());
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
* @todo This is specific to broadband - this needs to be more generic.
*/
public function rules(Request $request)
{
if (! $request->isMethod('post'))
return [];
return [
'broadband.product_id' => 'required|exists:products,id',
'broadband.change_fee' => 'nullable|numeric',
'broadband.price' => 'nullable|numeric',
'broadband.start_at' => 'required|date', // @todo Check that it is not more than 1 billing cycle ago, and not future.
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
/**
* Editing Suppliers
*/
class SupplierAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->isWholesaler();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => 'required|string|min:2|max:255',
'active' => 'sometimes|accepted',
'address1' => 'nullable|string|min:2|max:255',
'address2' => 'nullable|string|min:2|max:255',
'city' => 'nullable|string|min:2|max:64',
'state' => 'nullable|string|min:2|max:32',
'postcode' => 'nullable|string|min:2|max:8',
'supplier_details.notes' => 'nullable|string|min:3',
'supplier_details.accounts' => 'nullable|email',
'supplier_details.support' => 'nullable|email',
'supplier_details.payments' => 'nullable|string|min:3',
'api_key' => 'nullable|string|min:3',
'api_secret' => 'nullable|string|min:3',
];
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use App\Models\Supplier;
class SupplierProductAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->isWholesaler();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(Request $request)
{
// @todo these are broadband requirements - perhaps move them to the broadband class.
// @todo Enhance the validation so that extra_* values are not accepted if base_* values are not included.
return [
'id' => 'required|nullable',
'offering_type' => ['required',Rule::in(Supplier::offeringTypeKeys()->toArray())],
'supplier_detail_id' => 'required|exists:supplier_details,id',
'active' => 'sometimes|accepted',
'extra_shaped' => 'sometimes|accepted',
'extra_charged' => 'sometimes|accepted',
'product_id' => 'required|string|min:2',
'product_desc' => 'required|string|min:2',
'base_cost' => 'required|numeric|min:.01',
'setup_cost' => 'nullable|numeric',
'contract_term' => 'nullable|numeric|min:1',
'metric' => 'nullable|numeric|min:1',
'speed' => 'nullable|string|max:64',
'technology' => 'nullable|string|max:255',
'offpeak_start' => 'nullable|date_format:H:i',
'offpeak_end' => 'nullable|date_format:H:i',
'base_down_peak' => 'nullable|numeric',
'base_up_peak' => 'nullable|numeric',
'base_down_offpeak' => 'nullable|numeric',
'base_up_offpeak' => 'nullable|numeric',
'extra_down_peak' => 'nullable|numeric',
'extra_up_peak' => 'nullable|numeric',
'extra_down_offpeak' => 'nullable|numeric',
'extra_up_offpeak' => 'nullable|numeric',
];
}
}

View File

@ -9,12 +9,12 @@ interface IDs
* *
* @return mixed * @return mixed
*/ */
public function getLIDattribute(): string; public function getLIDAttribute(): string;
/** /**
* Return the system ID of the item * Return the system ID of the item
* *
* @return mixed * @return mixed
*/ */
public function getSIDattribute(): string; public function getSIDAttribute(): string;
} }

View File

@ -0,0 +1,29 @@
<?php
namespace App\Interfaces;
use Illuminate\Support\Collection;
interface ProductItem
{
/**
* Return the traffic inclusion with the service
*
* @return mixed
*/
public function allowance(): Collection;
/**
* Render the traffic inclusion as a string
*
* @return mixed
*/
public function allowance_string(): string;
/**
* Does this offering capture usage information
*
* @return bool
*/
public function hasUsage(): bool;
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Interfaces;
use Illuminate\Support\Collection;
interface ProductSupplier
{
/**
* Return the traffic inclusion with the service
*
* @return mixed
*/
public function allowance(): Collection;
/**
* Render the traffic inclusion as a string
*
* @return mixed
*/
public function allowance_string(): string;
/**
* Return the product cost
*
* @return float
*/
public function getCostAttribute(): float;
/**
* Return the supplier class
* If there is a model relationship return:
* return $this->getRelationValue('supplier');
* otherwise return a stdClass with name
*
* @return mixed
*/
public function getSupplierAttribute();
}

View File

@ -6,6 +6,13 @@ use Carbon\Carbon;
interface ServiceItem interface ServiceItem
{ {
/**
* Months the service is contracted for.
*
* @return int
*/
public function getContractTermAttribute(): int;
/** /**
* Return the Service Description. * Return the Service Description.
* *
@ -16,7 +23,7 @@ interface ServiceItem
/** /**
* Date the service expires * Date the service expires
*/ */
public function getServiceExpireAttribute(): Carbon; public function getServiceExpireAttribute(): ?Carbon;
/** /**
* Return the Service Name. * Return the Service Name.
@ -25,6 +32,13 @@ interface ServiceItem
*/ */
public function getServiceNameAttribute(): string; public function getServiceNameAttribute(): string;
/**
* Has this service expired
*
* @return bool
*/
public function hasExpired(): bool;
/** /**
* Is this service in a contract * Is this service in a contract
* *

View File

@ -6,6 +6,13 @@ use Illuminate\Support\Collection;
interface ServiceUsage interface ServiceUsage
{ {
/**
* Our model that holds traffic information
*
* @return mixed
*/
public function traffic();
/** /**
* This service provides usage information * This service provides usage information
* *

View File

@ -0,0 +1,107 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Intuit\Jobs\AccountingCustomerUpdate;
use App\Models\{Account,ProviderToken};
/**
* Synchronise customers with our accounts.
*
* This will:
* + Create the account in the accounting system
* + Update the account in the accounting system with our data (we are master)
*/
class AccountingAccountSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JAS';
private ProviderToken $to;
/**
* Create a new job instance.
*
* @param ProviderToken $to
*/
public function __construct(ProviderToken $to)
{
$this->to = $to;
}
/**
* Execute the job.
*
* @return void
* @throws \Exception
*/
public function handle()
{
$api = $this->to->API();
$ref = Account::select('id','site_id','company','user_id')->with(['user'])->get();
foreach ($api->getCustomers() as $acc) {
$o = NULL;
// See if we are already linked
if (($x=$this->to->provider->accounts->where('pivot.ref',$acc->id))->count() === 1) {
$o = $x->pop();
// If not, see if our reference matches
} elseif (($x=$ref->filter(function($item) use ($acc) { return $item->sid == $acc->ref; }))->count() === 1) {
$o = $x->pop();
// Look based on Name
} elseif (($x=$ref->filter(function($item) use ($acc) { return $item->company == $acc->companyname || $item->name == $acc->fullname || $item->user->email == $acc->email; }))->count() === 1) {
$o = $x->pop();
} else {
// Log not found
Log::alert(sprintf('%s:Customer not found [%s:%s]',self::LOGKEY,$acc->id,$acc->DisplayName));
continue;
}
$o->providers()->syncWithoutDetaching([
$this->to->provider->id => [
'ref' => $acc->id,
'synctoken' => $acc->synctoken,
'created_at'=>Carbon::create($acc->created_at),
'updated_at'=>Carbon::create($acc->updated_at),
'site_id'=>$this->to->site_id,
],
]);
Log::alert(sprintf('%s:Customer updated [%s:%s]',self::LOGKEY,$o->id,$acc->id));
// Check if QB is out of Sync and update it.
$acc->syncOriginal();
$acc->PrimaryEmailAddr = (object)['Address'=>$o->user->email];
$acc->ResaleNum = $o->sid;
$acc->GivenName = $o->user->firstname;
$acc->FamilyName = $o->user->lastname;
$acc->CompanyName = $o->name;
$acc->DisplayName = $o->name;
$acc->FullyQualifiedName = $o->name;
//$acc->Active = (bool)$o->active; // @todo implement in-activity, but only if all invoices are paid and services cancelled
if ($acc->getDirty()) {
Log::info(sprintf('%s:Customer [%s] (%s:%s) has changed',self::LOGKEY,$o->sid,$acc->id,$acc->DisplayName),['dirty'=>$acc->getDirty()]);
$acc->sparse = 'true';
AccountingCustomerUpdate::dispatch($this->to,$acc);
}
// @todo Identify accounts in our DB that are not in accounting
}
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\{Account,Invoice,Payment,PaymentItem,ProviderToken,Site};
use Intuit\Models\Payment as PaymentModel;
/**
* Synchronise payments with our payments.
*
* This will:
* + Create/Update the payment in our system
* + Associate the payment to the same invoice in our system
*/
class AccountingPaymentSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JPS';
private PaymentModel $pmi;
private ProviderToken $to;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ProviderToken $to,PaymentModel $pmi)
{
$this->pmi = $pmi;
$this->to = $to;
}
/**
* Execute the job.
*
* @return void
* @throws \Exception
*/
public function handle()
{
// @todo Can this be automatically determined?
$site = Site::findOrFail($this->to->site_id);
Config::set('site',$site);
// See if we are already linked
if (($x=$this->to->provider->payments->where('pivot.ref',$this->pmi->id))->count() === 1) {
$o = $x->pop();
} else {
// Find the account
$ao = Account::select('accounts.*')
->join('account__provider',['account__provider.account_id'=>'accounts.id'])
->where('provider_oauth_id',$this->to->provider_oauth_id)
->where('ref',$this->pmi->account_ref)
->single();
if (! $ao) {
Log::alert(sprintf('%s:Account not found for payment [%s:%d]',self::LOGKEY,$this->pmi->id,$this->pmi->account_ref));
return;
}
// Create the payment
$o = new Payment;
$o->account_id = $ao->id;
$o->site_id = $ao->site_id; // @todo Automatically determine
}
// Update the payment details
$o->paid_at = $this->pmi->date_paid;
$o->active = TRUE;
$o->checkout_id = 2; // @todo
$o->total_amt = $this->pmi->total_amt;
$o->notes = 'Imported from Intuit';
$o->save();
Log::info(sprintf('%s:Recording payment [%s:%3.2f]',self::LOGKEY,$this->pmi->id,$this->pmi->total_amt));
$o->providers()->syncWithoutDetaching([
$this->to->provider->id => [
'ref' => $this->pmi->id,
'synctoken' => $this->pmi->synctoken,
'created_at'=>Carbon::create($this->pmi->created_at),
'updated_at'=>Carbon::create($this->pmi->updated_at),
'site_id'=>$this->to->site_id,
],
]);
// Load the invoice that this payment pays
$invoices = collect();
foreach ($this->pmi->lines() as $item => $amount) {
$invoice = Invoice::select('invoices.*')
->join('invoice__provider',['invoice__provider.invoice_id'=>'invoices.id'])
->where('provider_oauth_id',$this->to->provider_oauth_id)
->where('ref',$item)
->single();
$invoices->put($item,$invoice);
}
// Delete existing paid invoices that are no longer paid
foreach ($o->items as $pio)
if ($invoices->pluck('id')->search($pio->invoice_id) === FALSE)
$pio->delete();
// Update payment items
foreach ($this->pmi->lines() as $item => $amount) {
if (! $invoices->has($item)) {
Log::alert(sprintf('%s:Invoice [%s] not recorded, payment not assigned',self::LOGKEY,$item));
continue;
}
$io = $invoices->get($item);
// If the payment item already exists
if (($x=$o->items->where('invoice_id',$io->id))->count()) {
$pio = $x->pop();
} else {
$pio = new PaymentItem;
$pio->invoice_id = $io->id;
$pio->site_id = $io->site_id;
}
$pio->amount = $amount;
$o->items()->save($pio);
}
Log::alert(sprintf('%s:Payment updated [%s:%s]',self::LOGKEY,$o->id,$this->pmi->id));
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Models\{Tax,ProviderToken};
/**
* Synchronise TAX ids with our taxes.
*
* This will only update our records, it wont create new records in the account system, nor in our DB
*/
class AccountingTaxSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JTS';
private ProviderToken $to;
/**
* Create a new job instance.
*
* @param ProviderToken $to
*/
public function __construct(ProviderToken $to)
{
$this->to = $to;
}
/**
* Execute the job.
*
* @return void
* @throws \Exception
*/
public function handle()
{
$api = $this->to->API();
$ref = Tax::select(['id','description'])->get();
foreach ($api->getTaxCodes() as $acc) {
$o = NULL;
// See if we are already linked
if (($x=$this->to->provider->taxes->where('pivot.ref',$acc->id))->count() === 1) {
$o = $x->pop();
/*
// If not, see if our reference matches
} elseif (($x=$ref->filter(function($item) use ($acc) { return $item->sid == $acc->ref; }))->count() === 1) {
$o = $x->pop();
*/
// Look based on Name
} elseif (($x=$ref->filter(function($item) use ($acc) { return $item->description === $acc->name; }))->count() === 1) {
$o = $x->pop();
} else {
// Log not found
Log::alert(sprintf('%s:Tax not found [%s:%s]',self::LOGKEY,$acc->id,$acc->name));
continue;
}
$o->providers()->syncWithoutDetaching([
$this->to->provider->id => [
'ref' => $acc->id,
'synctoken' => $acc->synctoken,
'created_at'=>Carbon::create($acc->created_at),
'updated_at'=>Carbon::create($acc->updated_at),
'site_id'=>$this->to->site_id,
],
]);
Log::alert(sprintf('%s:Tax updated [%s:%s]',self::LOGKEY,$o->id,$acc->id));
}
}
}

View File

@ -5,37 +5,41 @@ namespace App\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Mail\TrafficMismatch;
use App\Models\Service\Adsl;
use App\Models\Service\AdslTraffic;
use App\Models\AdslSupplier;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Classes\External\Supplier as ExternalSupplier;
use App\Mail\TrafficMismatch;
use App\Models\Supplier;
use App\Models\Service\Broadband as ServiceBroadband;
use App\Models\Usage\Broadband as UsageBroadband;
/** /**
* Class BroadbandTraffic * Class BroadbandTraffic
* Read and update the traffic for an Broadband supplier * Read and update the traffic for an Broadband supplier
* *
* @package App\Jobs * @package App\Jobs
*/ */
class BroadbandTraffic implements ShouldQueue final class BroadbandTraffic implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JBT'; private const LOGKEY = 'JBT';
protected $aso = NULL; // The supplier we are updating from protected int $sid; // The supplier we are updating from
private $class_prefix = 'App\Classes\External\Supplier\\'; private const class_prefix = 'App\Classes\External\Supplier\\';
public function __construct(AdslSupplier $o) private const traffic = 'broadband';
public function __construct(int $sid)
{ {
$this->aso = $o; $this->sid = $sid;
} }
/** /**
@ -43,35 +47,41 @@ class BroadbandTraffic implements ShouldQueue
* *
* @return void * @return void
* @throws \Exception * @throws \Exception
* @todo The column stats_lastupdate is actually the "next" date that stats should be retrieved. Rename it.
*/ */
public function handle() public function handle()
{ {
Log::info(sprintf('%s:Importing Broadband Traffic from [%s]',self::LOGKEY,$this->aso->name),['m'=>__METHOD__]); $so = Supplier::findOrFail($this->sid);
Log::info(sprintf('%s:Importing Broadband Traffic from [%s]',self::LOGKEY,$so->name));
if ((! $connection=$so->detail->connections->get('broadband')) || (count(array_intersect(array_keys($connection),ExternalSupplier::traffic_connection_keys)) !== 3))
throw new \Exception('No or missing connection details for:'.self::traffic);
$u = 0; $u = 0;
// Load our class for this supplier // Load our class for this supplier
$class = $this->class_prefix.$this->aso->name; $class = self::class_prefix.$so->name;
if (class_exists($class)) { if (class_exists($class)) {
$o = new $class($this->aso); $o = new $class($so);
} else { } else {
Log::error(sprintf('%s: Class doesnt exist: %d',get_class($this),$class)); Log::error(sprintf('%s: Class doesnt exist: %s',self::LOGKEY,$class));
exit(1); exit(1);
} }
$last_update = Carbon::create(Arr::get($connection,'last'))->addDay();
Arr::set($connection,'last',$last_update->format('Y-m-d'));
// Repeat pull traffic data until yesterday // Repeat pull traffic data until yesterday
while ($this->aso->stats_lastupdate < Carbon::now()->subDay()) { while ($last_update < Carbon::now()->subDay()) {
Log::notice(sprintf('%s:Next update is [%s]',self::LOGKEY,$this->aso->stats_lastupdate->format('Y-m-d')),['m'=>__METHOD__]); Log::notice(sprintf('%s:Next update is [%s]',self::LOGKEY,$last_update->format('Y-m-d')));
// Delete traffic, since we'll refresh it. // Delete traffic, since we'll refresh it.
AdslTraffic::where('supplier_id',$this->aso->id) UsageBroadband::where('supplier_id',$so->id)
->where('date',$this->aso->stats_lastupdate) ->where('date',$last_update->format('Y-m-d'))
->delete(); ->delete();
$c = 0; $c = 0;
foreach ($o->fetch() as $line) { foreach ($o->fetch($connection,self::traffic) as $line) {
// The first row is our header // The first row is our header
if (! $c++) { if (! $c++) {
$fields = $o->getColumns(preg_replace('/,\s+/',',',$line),collect($o->header())); $fields = $o->getColumns(preg_replace('/,\s+/',',',$line),collect($o->header()));
@ -79,29 +89,26 @@ class BroadbandTraffic implements ShouldQueue
} }
if (! $fields->count()) if (! $fields->count())
abort(500,'? No fields in data exportupda'); abort(500,'? No fields in data export');
$row = str_getcsv(trim($line)); $row = str_getcsv(trim($line));
try { try {
// @todo Put the date format in the DB.
$date = Carbon::createFromFormat('Y-m-d',$row[$o->getColumnKey('Date')]); $date = Carbon::createFromFormat('Y-m-d',$row[$o->getColumnKey('Date')]);
// Find the right service dependent on the dates we supplied the service
// Find the right service dependant on the dates we supplied the service $oo = ServiceBroadband::where('service_username',$row[$o->getColumnKey('Login')])
$oo = Adsl::where('service_username',$row[$o->getColumnKey('Login')]) ->select(DB::raw('service_broadband.*'))
->select(DB::raw('ab_service__adsl.*')) ->join('services','services.id','=','service_id')
->join('ab_service','ab_service.id','=','service_id') ->where('services.start_at','<=',$date)
->where('ab_service.date_start','<=',$date->format('U'))
->where(function($query) use ($date) { ->where(function($query) use ($date) {
$query->whereNULL('ab_service.date_end') $query->whereNULL('services.stop_at')
->orWhere('ab_service.date_end','<=',$date->format('U')); ->orWhere('services.stop_at','<=',$date);
}) })
->get(); ->single();
$to = new AdslTraffic; $to = new UsageBroadband;
$to->site_id = 1; // @todo TO ADDRESS $to->date = $last_update;
$to->date = $this->aso->stats_lastupdate; $to->supplier_id = $so->id;
$to->supplier_id = $this->aso->id;
$to->up_peak = $row[$o->getColumnKey('Peak upload')]; $to->up_peak = $row[$o->getColumnKey('Peak upload')];
$to->up_offpeak = $row[$o->getColumnKey('Off peak upload')]; $to->up_offpeak = $row[$o->getColumnKey('Off peak upload')];
$to->down_peak = $row[$o->getColumnKey('Peak download')]; $to->down_peak = $row[$o->getColumnKey('Peak download')];
@ -111,33 +118,39 @@ class BroadbandTraffic implements ShouldQueue
$to->time = '24:00'; // @todo $to->time = '24:00'; // @todo
// If we have no records // If we have no records
if ($oo->count() != 1) { if (! $oo) {
Log::error(sprintf('%s:Too many services return for [%s]',self::LOGKEY,$row[$o->getColumnKey('Login')]),['m'=>__METHOD__,'date'=>$date,'count'=>$oo->count()]); Log::error(sprintf('%s:None or too many services return for [%s]',self::LOGKEY,$row[$o->getColumnKey('Login')]),['date'=>$date]);
$to->service = $row[$o->getColumnKey('Login')]; $to->service = $row[$o->getColumnKey('Login')];
$to->save(); $to->site_id = 1; // @todo This needs to be worked out a better way
} else { } else {
$oo->first()->traffic()->save($to); $to->site_id = $oo->site_id;
$to->service_item_id = $oo->id;
} }
if ($to->save())
$u++; $u++;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error(sprintf('%s:Exception occurred when storing traffic record for [%s].',self::LOGKEY,$row[$o->getColumnKey('Login')]),['m'=>__METHOD__,'row'=>$row,'line'=>$line]); Log::error(sprintf('%s:Exception occurred when storing traffic record for [%s].',self::LOGKEY,$row[$o->getColumnKey('Login')]),['row'=>$row,'line'=>$line]);
throw new \Exception('Error while storing traffic date'); throw new \Exception('Error while storing traffic data: '.$e->getMessage());
} }
} }
Log::info(sprintf('%s: Records Imported [%d] for [%s]',self::LOGKEY,$u,$last_update->format('Y-m-d')));
Log::info(sprintf('%s: Records Imported [%d] for [%s]',self::LOGKEY,$u,$this->aso->stats_lastupdate->format('Y-m-d')),['m'=>__METHOD__]); // Save our current progress.
$so->detail->connections = $so->detail->connections->put(self::traffic,array_merge($connection,['last'=>$last_update->format('Y-m-d')]));
$so->detail->save();
// Update our details for the next iteration.
$last_update = $last_update->addDay();
Arr::set($connection,'last',$last_update->format('Y-m-d'));
if ($u) { if ($u) {
$this->aso->stats_lastupdate = $this->aso->stats_lastupdate->addDay(); if ($so->trafficMismatch($date)->count())
$this->aso->save();
if ($this->aso->trafficMismatch($date)->count())
Mail::to('deon@graytech.net.au') // @todo To change Mail::to('deon@graytech.net.au') // @todo To change
->send(new TrafficMismatch($this->aso,$date)); ->send(new TrafficMismatch($so,$date));
} }
} }
} }

232
app/Jobs/ImportCosts.php Normal file
View File

@ -0,0 +1,232 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\{Cost,Service,Site,Supplier};
use App\Traits\Import;
class ImportCosts implements ShouldQueue
{
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels,Import;
private const LOGKEY = 'JIC';
private Cost $co;
private Site $site;
private string $file;
protected Collection $columns;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Site $site,Supplier $so,Carbon $invoice_date,string $file)
{
$this->file = $file;
$this->site = $site;
$this->co = Cost::where('site_id',$site->site_id)
->where('supplier_id',$so->id)
->where('billed_at',$invoice_date)
->firstOrNew();
$this->co->active = TRUE;
$this->co->site_id = $site->site_id;
$this->co->billed_at = $invoice_date;
$this->co->supplier_id = $so->id;
$this->co->save();
Cost\Broadband::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
Cost\Phone::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
Cost\Generic::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
// @todo to be stored in supplier config
$headers = [
'INVOICEID'=>'Item ID',
'REF'=>'Reference No',
'IDTAG'=>'ID Tag',
'CATEGORY'=>'Category',
'DESC'=>'Item Description',
'QUANTITY'=>'Quantity',
'PRICEUNIT'=>'Unit Price (inc-GST)',
'PRICETOTAL'=>'Total (inc-GST)'
];
$this->columns = collect($headers)->transform(function($item) { return strtolower($item); });
}
/**
* Execute the job.
*
* @return void
* @throws \Exception
*/
public function handle()
{
Config::set('site',$this->site);
$skip = 7; // @todo to be stored in supplier config
$file = fopen('storage/app/'.$this->file,'r');
$haveHeader = FALSE;
$c = 0;
while (! feof($file)) {
$line = stream_get_line($file,0,"\r\n");
if (str_starts_with($line,'#'))
continue;
// Remove any embedded CR and BOM
$line = str_replace("\r",'',$line);
$line = preg_replace('/^\x{feff}/u','',$line);
if (($c++ < $skip) || (! $line))
continue;
// The first line is a header.
if (! $haveHeader) {
Log::debug(sprintf('%s: Input File: %s',get_class($this),$this->file));
Log::debug(sprintf('%s: Processing columns: %s',get_class($this),join('|',$this->setColumns($line)->toArray())));
$haveHeader = TRUE;
continue;
}
// If the line has a , between two (), then convert the comma to a space.
$x = [];
if (preg_match('#\(.+,.+\)#i',$line,$x)) {
$replace = str_replace(',','_',$x[0]);
$line = str_replace($x[0],$replace,$line);
//dd($line,$x);
}
$fields = str_getcsv(trim($line));
if (is_null($x=$this->getColumnKey('DESC')) OR empty($fields[$x]))
continue;
// The first part of our item description is the service number.
// This should go to a "supplier" function, since all suppliers may show different values in description.
$m = [];
$desc = $fields[$x];
// m[1] = Service, m[2] = Desc, m[3] = From Date, m[4] = To Date
preg_match('#^([0-9]{10})\s+-\s+(.*)\(([0-9]+\s+[JFMASOND].*\s+[0-9]+)+\s+-\s+([0-9]+\s+[JFMASOND].*\s+[0-9]+)+\)$#',$fields[$x],$m);
if (count($m) !== 5) {
dump(sprintf('ERROR: Description didnt parse [%s] on line [%d]',$fields[$x],$c));
continue;
}
$cost = ($x=$this->getColumnKey('PRICETOTAL')) ? str_replace([',','$'],'',$fields[$x]) : NULL;
$start_at = Carbon::createFromFormat('d M Y',$m[3]);
$stop_at = Carbon::createFromFormat('d M Y',$m[4]);
$so = Service::search($m[1])->active()->with(['type','product.type.supplied'])->single();
if ($so) {
// r[1] = Monthly Charge or Extra Charge,r[2] = "On Plan", r[3] = Plan Info
$r = [];
switch ($so->category) {
case 'broadband':
$to = Cost\Broadband::where('site_id',$this->co->site_id)
->where('cost_id',$this->co->id)
->where('service_broadband_id',$so->type->id)
->where('start_at',$start_at)
->where('end_at',$stop_at)
->firstOrNew();
$to->service_broadband_id = $so->type->id;
preg_match('#^(Monthly Internet Charge|Plan Change Fee|Change billing date refund for Monthly Internet Charge On Plan|First 12 Month VISP broadband plan discount|.*)\s?(On Plan)?\s?(.*)#',$m[2],$r);
switch ($r[1]) {
case 'Monthly Internet Charge':
case 'First 12 Month VISP broadband plan discount':
case 'Change billing date refund for Monthly Internet Charge On Plan':
$to->base =+ $cost;
break;
case 'Plan Change Fee':
$to->excess =+ $cost;
break;
default:
dump(['extra charge'=>$r]);
$to->excess =+ $cost;
}
break;
case 'phone':
$to = Cost\Phone::where('site_id',$this->co->site_id)
->where('cost_id',$this->co->id)
->where('service_phone_id',$so->type->id)
->where('start_at',$start_at)
->where('end_at',$stop_at)
->firstOrNew();
$to->service_phone_id = $so->type->id;
preg_match('#^(Residential VOIP Plan Excess Usage|Virtual FAX Number Monthly Rental|Corporate VOIP Plan Monthly Rental|Residential VOIP Plan Monthly Rental|.*)\s?(.*)#',$m[2],$r);
switch ($r[1]) {
case 'Residential VOIP Plan Monthly Rental':
case 'Virtual FAX Number Monthly Rental':
case 'Corporate VOIP Plan Monthly Rental':
$to->base =+ $cost;
break;
case 'Residential VOIP Plan Excess Usage':
$to->excess =+ $cost;
$to->notes = $r[2];
break;
default:
dump(['extra charge'=>$r]);
$to->excess =+ $cost;
}
break;
default:
dump(['so'=>$so,'category'=>$so->category,'line'=>$line,'m'=>$m,'r'=>$r]);
throw new \Exception(sprintf('ERROR: Service type not handled for service [%s] (%s) on line [%d]',$m[1],$so->category,$c));
}
} else {
dump(['line'=>$line,'sql'=>Service::search($m[1])->active()->with(['type','product.type.supplied'])->toSql()]);
$to = Cost\Generic::where('site_id',$this->co->site_id)
->where('cost_id',$this->co->id)
->where('notes',sprintf('%s:%s',$m[1],$m[2]))
->where('start_at',$start_at)
->where('end_at',$stop_at)
->firstOrNew();
$to->excess =+ $cost;
$to->notes = $line;
}
$to->site_id = $this->co->site_id;
$to->cost_id = $this->co->id;
$to->active = TRUE;
$to->start_at = $start_at;
$to->end_at = $stop_at;
// Work out supplier product number
Log::warning(sprintf('%s:Supplier product ID not matched',self::LOGKEY),['r'=>$r]);
//dd($m[2],$cost,$so->product->type->supplied);
// Work out if this base charge, or extra charge
//dd(['M'=>__METHOD__,'fields'=>$fields,'DESC'=>$this->getColumnKey('DESC'),'desc'=>$desc,'m'=>$m,'sql'=>$so->toSql(),'bindings'=>$so->getBindings(),'so'=>$so]);
$to->save();
}
fclose($file);
}
}

104
app/Jobs/PaymentsImport.php Normal file
View File

@ -0,0 +1,104 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Classes\External\Payments;
use App\Models\{Account,Checkout,Payment};
/**
* Import payments from payment providers
*
* @package App\Jobs
*/
final class PaymentsImport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JPI';
protected Payments $o; // The payment provider we are updating from
private $class_prefix = 'App\Classes\External\Payments\\';
public function __construct(Payments $o)
{
$this->o = $o;
}
public function handle()
{
Log::info(sprintf('%s:Importing Payment Date from [%s]',self::LOGKEY,get_class($this->o)));
// Get our checkout IDs for this plugin
$cos = Checkout::where('plugin',config('services.ezypay.plugin'))->pluck('id');
foreach ($this->o->getCustomers() as $c) {
if ($c->BillingStatus == 'Inactive') {
Log::debug(sprintf('%s:Ignoring INACTIVE: [%s] %s %s',self::LOGKEY,$c->EzypayReferenceNumber,$c->Firstname,$c->Surname));
continue;
}
// Load Account Details from ReferenceId
$ao = Account::where('site_id',(int)substr($c->ReferenceId,0,2))
->where('id',(int)substr($c->ReferenceId,2,4))
->first();
if (! $ao) {
Log::error(sprintf('%s:Missing: [%s] %s %s (%s)',self::LOGKEY,$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId));
continue;
}
// Find the last payment logged
$last = Carbon::create(Payment::whereIN('checkout_id',$cos)->where('account_id',$ao->id)->max('paid_at'));
$o = $this->o->getDebits([
'customerId'=>$c->Id,
'dateFrom'=>$last->format('Y-m-d'),
'dateTo'=>$last->addQuarter()->format('Y-m-d'),
'pageSize'=>100,
]);
Log::info(sprintf('%s:Loaded [%d] payments for account: [%s]',self::LOGKEY,$o->count(),$ao->id));
// Load the payments
if ($o->count()) {
foreach ($o->reverse() as $p) {
$pd = Carbon::createFromTimeString($p->Date);
// If not success, ignore it.
if ($p->Status != 'Success') {
Log::alert(sprintf('%s:Payment not successful: [%s] %s %s (%s) [%s]',self::LOGKEY,$pd->format('Y-m-d'),$ao->id,$p->Id,$p->Amount,$p->Status));
continue;
}
$lp = $ao->payments->last();
if ($lp AND (($pd == $lp->paid_at) OR ($p->Id == $lp->checkout_data))) {
Log::alert(sprintf('%s:Payment Already Recorded: [%s] %s %s (%s)',self::LOGKEY,$pd->format('Y-m-d'),$ao->id,$p->Id,$p->Amount));
continue;
}
// New Payment
$po = new Payment;
$po->active = TRUE;
$po->site_id = 1; // @todo
$po->paid_at = $pd;
$po->checkout_id = '999'; // @todo
$po->checkout_data = $p->Id;
$po->total_amt = $p->Amount;
$ao->payments()->save($po);
Log::info(sprintf('%s:Recorded: Payment for [%s] %s %s (%s) on %s',self::LOGKEY,$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$po->id,$pd));
}
}
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\ProviderToken;
class ProviderTokenRefresh implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private ProviderToken $to;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(ProviderToken $to)
{
$this->to = $to;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->to->refreshToken();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\{Site,Supplier,TLD};
class SupplierDomainSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JSD';
protected Site $site;
protected Supplier $supplier;
protected bool $forceprod;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Site $site,Supplier $supplier,bool $forceprod=FALSE)
{
$this->site = $site;
$this->supplier = $supplier;
$this->forceprod = $forceprod;
Config::set('site',$site);
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$registrar_id = ($x=$this->supplier->registrar()) ? $x->id : NULL;
foreach ($this->supplier->API($this->forceprod)->getDomains(['fetchall'=>true]) as $domain) {
// @todo See if we can find this domain by its ID
// Find this domain by it's name
if (! $to=TLD::domaintld($domain->domain_name)) {
Log::alert(sprintf('%s:Domain [%s] from (%s) is not in a TLD that we manage',self::LOGKEY,$this->supplier->name,$domain->domain_name));
} elseif (($domainpart=strtolower($to->domain_part($domain->domain_name))) && (($x=$to->domains->where('domain_name',$domainpart))->count() === 1)) {
$o = $x->pop();
$o->registrar_auth_password = $domain->auth_key;
$o->expire_at = Carbon::create($domain->expiry_date);
$o->registrar_account = $domain->account;
$o->registrar_username = '';
$o->registrar_ns = Supplier\Domain::nameserver_name($domain->nameservers());
if ($registrar_id)
$o->domain_registrar_id = $registrar_id;
if ($o->getDirty()) {
Log::info(sprintf('%s:Updating Domain [%s] from (%s)',self::LOGKEY,$domain->domain_name,$this->supplier->name));
$o->save();
} else {
Log::info(sprintf('%s:No Change to Domain [%s] from (%s)',self::LOGKEY,$domain->domain_name,$this->supplier->name));
}
// Alert an unmanaged name.
} else {
Log::alert(sprintf('%s:Domain [%s] from (%s) is not one managed in OSB',self::LOGKEY,$this->supplier->name,$domain->domain_name));
}
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class LogSentMessage
{
private const LOGKEY = 'LSM';
/**
* Handle the event.
*
* @param MessageSent $event
* @return void
*/
public function handle(MessageSent $event)
{
Log::debug(
sprintf('%s:Email to [%s] with subject [%s] sent [%s]',
self::LOGKEY,
collect($event->data['message']->getTo())->transform(function($item) { return $item->getAddress(); })->join(','),
$event->data['message']->getSubject(),
$event->sent->getMessageId(),
),
['debug'=>$event->sent->getDebug()]);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Events\ProviderPaymentCreated as Event;
use App\Jobs\AccountingPaymentSync as Job;
use App\Models\{ProviderOauth,Site,User};
class ProviderPaymentCreated
{
private const LOGKEY = 'LPC';
/**
* Handle the event.
*
* @param Event $event
* @return void
*/
public function handle(Event $event)
{
$site = Site::findOrFail(1); // @todo This shouldnt be hard coded
Config::set('site',$site);
$uo = User::findOrFail(1); // @todo This shouldnt be hard coded
$so = ProviderOauth::where('name',$event->provider)->singleOrFail();
if (! ($to=$so->token($uo)))
abort(500,sprintf('Unknown Tokens for [%s]',$uo->email));
$api = $to->API();
$acc = $api->getPayment($event->paymentData['id']);
Job::dispatch($to,$acc);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
class CancelRequest extends Mailable
{
use Queueable, SerializesModels;
public Service $service;
public string $notes;
/**
* Create a new message instance.
*
* @param Service $o
* @param string $notes
*/
public function __construct(Service $o,string $notes='')
{
$this->service = $o;
$this->notes = $notes;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
Config::set('site',$this->service->site);
switch (get_class($this->service->type)) {
case Service\Broadband::class:
$subject = sprintf('Cancel BROADBAND: %s',$this->service->type->service_address);
break;
case Service\Phone::class:
$subject = sprintf('Cancel PHONE: %s',$this->service->type->service_number);
break;
default:
$subject = 'Cancel Service Request';
}
return $this
->markdown('email.admin.service.cancel')
->subject($subject)
->with(['site'=>$this->service->site]);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
class ChangeRequest extends Mailable
{
use Queueable, SerializesModels;
public Service $service;
public string $notes;
/**
* Create a new message instance.
*
* @param Service $o
* @param string $notes
*/
public function __construct(Service $o,string $notes='')
{
$this->service = $o;
$this->notes = $notes ?? '';
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
Config::set('site',$this->service->site);
switch (get_class($this->service->type)) {
case Service\Broadband::class:
$subject = sprintf('Change BROADBAND: %s',$this->service->type->service_address);
break;
case Service\Phone::class:
$subject = sprintf('Change PHONE: %s',$this->service->type->service_number);
break;
default:
$subject = 'Change Service Request';
}
return $this
->markdown('email.admin.service.change')
->subject($subject)
->with(['site'=>$this->service->site]);
}
}

View File

@ -2,10 +2,12 @@
namespace App\Mail; namespace App\Mail;
use App\Models\Site;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Config;
use App\Models\Invoice; use App\Models\Invoice;
@ -14,12 +16,12 @@ class InvoiceEmail extends Mailable
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $invoice; public $invoice;
public $site;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @param Invoice $o * @param Invoice $o
* @param string $notes
*/ */
public function __construct(Invoice $o) public function __construct(Invoice $o)
{ {
@ -33,12 +35,15 @@ class InvoiceEmail extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',Site::findOrFail($this->invoice->site_id));
$this->site = config('site');
return $this return $this
->markdown('email.user.invoice') ->markdown('email.user.invoice',['site'=>config('site')])
->subject(sprintf( 'Invoice: %s - Total: $%s - Due: %s', ->subject(sprintf( 'Invoice: %s - Total: $%s - Due: %s',
$this->invoice->id, $this->invoice->id,
number_format($this->invoice->total,2), number_format($this->invoice->total,2),
$this->invoice->date_due)) $this->invoice->due_at->format('Y-m-d')))
->with([ ->with([
'user'=>$this->invoice->account->user, 'user'=>$this->invoice->account->user,
'site'=>$this->invoice->account->user->site, 'site'=>$this->invoice->account->user->site,

View File

@ -3,9 +3,10 @@
namespace App\Mail; namespace App\Mail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Config;
use App\Models\Service; use App\Models\Service;
@ -13,8 +14,8 @@ class OrderRequest extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $service; public Service $service;
public $notes; public string $notes;
/** /**
* Create a new message instance. * Create a new message instance.
@ -22,7 +23,7 @@ class OrderRequest extends Mailable
* @param Service $o * @param Service $o
* @param string $notes * @param string $notes
*/ */
public function __construct(Service $o,$notes='') public function __construct(Service $o,string $notes='')
{ {
$this->service = $o; $this->service = $o;
$this->notes = $notes; $this->notes = $notes;
@ -35,14 +36,15 @@ class OrderRequest extends Mailable
*/ */
public function build() public function build()
{ {
switch (get_class($this->service->type)) Config::set('site',$this->service->site);
{
case 'App\Models\Service\Adsl': switch (get_class($this->service->type)) {
$subject = sprintf('NBN: %s',$this->service->type->service_address); case Service\Broadband::class:
$subject = sprintf('Order BROADBAND: %s',$this->service->type->service_address);
break; break;
case 'App\Models\Service\Voip': case Service\Phone::class:
$subject = sprintf('VOIP: %s',$this->service->type->service_number); $subject = sprintf('Order PHONE: %s',$this->service->type->service_number);
break; break;
default: default:

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use App\Models\Service; use App\Models\Service;
@ -13,8 +14,8 @@ class OrderRequestApprove extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $service; public Service $service;
public $notes; public string $notes;
/** /**
* Create a new message instance. * Create a new message instance.
@ -22,7 +23,7 @@ class OrderRequestApprove extends Mailable
* @param Service $o * @param Service $o
* @param string $notes * @param string $notes
*/ */
public function __construct(Service $o,$notes='') public function __construct(Service $o,string $notes='')
{ {
$this->service = $o; $this->service = $o;
$this->notes = $notes; $this->notes = $notes;
@ -35,12 +36,14 @@ class OrderRequestApprove extends Mailable
*/ */
public function build() public function build()
{ {
switch ($this->service->category) Config::set('site',$this->service->site);
{
case 'ADSL': $subject = sprintf('%s: %s',$this->service->category,$this->service->service_adsl->service_address); // @todo This is not consistent with Cancel/Change Request
switch ($this->service->category) {
case 'broadband': $subject = sprintf('%s: %s',$this->service->category,$this->service->type->service_address);
break; break;
case 'VOIP': $subject = sprintf('%s: %s',$this->service->category,$this->service->service_voip->service_number); case 'phone': $subject = sprintf('%s: %s',$this->service->category,$this->service->type->service_number);
break; break;
default: default:

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use App\Models\Service; use App\Models\Service;
@ -13,15 +14,15 @@ class OrderRequestReject extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $service; public Service $service;
public $reason; public string $reason;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @return void * @return void
*/ */
public function __construct(Service $o,$reason) public function __construct(Service $o,string $reason)
{ {
$this->service = $o; $this->service = $o;
$this->reason = $reason; $this->reason = $reason;
@ -34,6 +35,8 @@ class OrderRequestReject extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',$this->service->site);
return $this return $this
->markdown('email.admin.order.reject') ->markdown('email.admin.order.reject')
->subject(sprintf('Your order: #%s was rejected',$this->service->id)) ->subject(sprintf('Your order: #%s was rejected',$this->service->id))

View File

@ -5,26 +5,28 @@ namespace App\Mail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\{AccountOauth,User}; use App\Models\{Site,User,UserOauth};
class SocialLink extends Mailable class SocialLink extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public $token; public string $token;
public $user; public Site $site;
public ?User $user;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @param User $o * @param UserOauth $o
* @param string $token
*/ */
public function __construct(AccountOauth $o) public function __construct(UserOauth $o)
{ {
$this->site = $o->site;
$this->token = $o->link_token; $this->token = $o->link_token;
$this->user = $o->account->user; $this->user = $o->user;
} }
/** /**
@ -34,11 +36,13 @@ class SocialLink extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',$this->site);
return $this return $this
->markdown('email.system.social_link') ->markdown('email.system.social_link')
->subject('Link your Account') ->subject('Link your Account')
->with([ ->with([
'site'=>$this->user->site, 'site'=>$this->site,
]); ]);
} }
} }

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\User; use App\Models\User;
@ -13,6 +14,8 @@ class TestEmail extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public User $user;
/** /**
* Create a new message instance. * Create a new message instance.
* *
@ -30,6 +33,8 @@ class TestEmail extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',$this->user->site);
return $this return $this
->markdown('email.system.test_email') ->markdown('email.system.test_email')
->subject('Just a test...') ->subject('Just a test...')

View File

@ -2,25 +2,28 @@
namespace App\Mail; namespace App\Mail;
use App\Models\Site;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\AdslSupplier; use App\Models\{Supplier,Site};
class TrafficMismatch extends Mailable class TrafficMismatch extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public Supplier $aso;
public Carbon $date;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @return void * @return void
*/ */
public function __construct(AdslSupplier $o,Carbon $date) public function __construct(Supplier $o,Carbon $date)
{ {
$this->aso = $o; $this->aso = $o;
$this->date = $date; $this->date = $date;
@ -33,11 +36,13 @@ class TrafficMismatch extends Mailable
*/ */
public function build() public function build()
{ {
Config::set('site',$x=Site::find(1)); // @todo To auto determine;
return $this return $this
->markdown('email.system.broadband_traffic_mismatch') ->markdown('email.system.broadband_traffic_mismatch')
->subject('Traffic Mismatch for '.$this->date) ->subject('Traffic Mismatch for '.$this->date)
->with([ ->with([
'site'=>Site::find(1), // @todo To auto determine 'site'=>$x,
'date'=>$this->date, 'date'=>$this->date,
'aso'=>$this->aso, 'aso'=>$this->aso,
]); ]);

View File

@ -2,54 +2,71 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Leenooks\Traits\ScopeActive; use Leenooks\Traits\ScopeActive;
use App\Interfaces\IDs; use App\Interfaces\IDs;
use App\Traits\NextKey;
/** /**
* Class Account * Class Account
* Service Accounts * Service Accounts
* *
* Attributes for accounts: * Attributes for accounts:
* + lid: : Local ID for account * + lid : Local ID for account
* + sid: : System ID for account * + sid : System ID for account
* * + name : Account Name
* @package App\Models * + taxes : Taxes Applicable to this account
*/ */
class Account extends Model implements IDs class Account extends Model implements IDs
{ {
use HasFactory,NextKey,ScopeActive; use HasFactory,ScopeActive;
const RECORD_ID = 'account'; /* STATIC */
public $incrementing = FALSE;
const CREATED_AT = 'date_orig'; public static function InvoicesCredit(Collection $invoices=NULL): Collection
const UPDATED_AT = 'date_last'; {
return (new self)
->invoiceSummaryCredit($invoices,TRUE)
->get();
}
protected $appends = [ public static function InvoicesDue(Collection $invoices=NULL): Collection
'active_display', {
'name', return (new self)
'services_count_html', ->invoiceSummaryDue($invoices,TRUE)
'switch_url', ->get();
]; }
public $dateFormat = 'U'; /* INTERFACES */
protected $visible = [ public function getLIDAttribute(): string
'id', {
'active_display', return sprintf('%04s',$this->id);
'name', }
'services_count_html',
'switch_url', public function getSIDAttribute(): string
]; {
return sprintf('%02s-%s',$this->site_id,$this->getLIDAttribute());
}
/* RELATIONS */ /* RELATIONS */
/** /**
* Return the country the user belongs to * Charges assigned to this account
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function charges()
{
return $this->hasMany(Charge::class);
}
/**
* Country this account belongs to
*/ */
public function country() public function country()
{ {
@ -61,28 +78,87 @@ class Account extends Model implements IDs
return $this->belongsToMany(External\Integrations::class,'external_account',NULL,'external_integration_id'); return $this->belongsToMany(External\Integrations::class,'external_account',NULL,'external_integration_id');
} }
public function group()
{
return $this->hasOneThrough(Group::class,AccountGroup::class,'account_id','id','id','group_id');
}
/**
* Invoices created for this account
*
* @todo This needs to be optimised, to only return outstanding invoices and invoices for a specific age (eg: 2 years worth)
*/
public function invoices() public function invoices()
{ {
return $this->hasMany(Invoice::class); return $this->hasMany(Invoice::class)
->with(['items.taxes','paymentitems.payment']);
} }
public function language() /**
* Relation to only return active invoices
*
* @todo Only return active invoice_items
*/
public function invoices_active()
{ {
return $this->belongsTo(Language::class); return $this->invoices()
->active();
} }
/**
* Payments received and assigned to this account
*/
public function payments() public function payments()
{ {
return $this->hasMany(Payment::class); return $this->hasMany(Payment::class)
->with(['items']);
} }
public function services($active=FALSE) /**
* Relation to only return active payments
*
* @todo Only return active payment_items
*/
public function payments_active()
{ {
$query = $this->hasMany(Service::class); return $this->payments()
->active();
return $active ? $query->active() : $query;
} }
public function providers()
{
return $this->belongsToMany(ProviderOauth::class,'account__provider')
->where('account__provider.site_id',$this->site_id)
->withPivot('ref','synctoken','created_at','updated_at');
}
/**
* Services assigned to this account
*/
public function services()
{
return $this->hasMany(Service::class)
->with(['product.translate','product.type.supplied']);
}
/**
* Relation to only return active services
*/
public function services_active()
{
return $this->services()
->active();
}
public function taxes()
{
return $this->hasMany(Tax::class,'country_id','country_id')
->select(['id','zone','rate','country_id']);
}
/**
* User that owns this account
*/
public function user() public function user()
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
@ -95,7 +171,7 @@ class Account extends Model implements IDs
* *
* @param $query * @param $query
* @param string $term * @param string $term
* @return * @return mixed
*/ */
public function scopeSearch($query,string $term) public function scopeSearch($query,string $term)
{ {
@ -115,117 +191,54 @@ class Account extends Model implements IDs
/* ATTRIBUTES */ /* ATTRIBUTES */
public function getActiveDisplayAttribute($value)
{
return sprintf('<span class="btn-sm btn-block btn-%s text-center">%s</span>',$this->active ? 'success' : 'danger',$this->active ? 'Active' : 'Inactive');
}
/**
* @deprecated use getAIDAttribute()
*/
public function getAccountIdAttribute()
{
return $this->getAIDAttribute();
}
/**
* @deprecated use getUrlAdminAttribute()
*/
public function getAccountIdUrlAttribute()
{
return $this->getUrlAdminAttribute();
}
/** /**
* Get the address for the account * Get the address for the account
* *
* @return array * @return array
* @todo Change this to return a collection
*/ */
public function getAddressAttribute(): array public function getAddressAttribute(): array
{ {
return [ return collect([
$this->address1, 'address1' => $this->address1,
$this->address2, 'address2' => $this->address2,
sprintf('%s %s %s',$this->city.(($this->state OR $this->zip) ? ',' : ''),$this->state,$this->zip) 'location' => sprintf('%s %s %s',
]; $this->city.(($this->state || $this->zip) ? ',' : ''),
$this->state,
$this->zip)
])
->filter()
->values()
->toArray();
} }
/** /**
* Return the Account Unique Identifier * Return the account name
* @return string
* @deprecated use getSIDAttribute()
*/
public function getAIDAttribute()
{
return $this->getSIDAttribute();
}
/**
* Account Local ID
* *
* @return string * @return string
*/ */
public function getLIDAttribute(): string public function getNameAttribute(): string
{ {
return sprintf('%04s',$this->id); return $this->company ?: ($this->user_id ? $this->user->getSurFirstNameAttribute() : 'LID:'.$this->id);
}
public function getNameAttribute()
{
return $this->company ?: ($this->user_id ? $this->user->SurFirstName : 'AID:'.$this->id);
}
public function getServicesCountHtmlAttribute()
{
return sprintf('%s <small>/%s</small>',$this->services()->noEagerLoads()->where('active',TRUE)->count(),$this->services()->noEagerLoads()->count());
} }
/** /**
* Account System ID * Return the type of account this is - if it has a company name, then its a business account.
* *
* @return string * @return string
*/ */
public function getSIDAttribute(): string public function getTypeAttribute(): string
{
return sprintf('%02s-%s',$this->site_id,$this->getLIDAttribute());
}
public function getSwitchUrlAttribute()
{
return sprintf('<a href="/r/switch/start/%s"><i class="fas fa-external-link-alt"></i></a>',$this->user_id);
}
public function getTypeAttribute()
{ {
return $this->company ? 'Business' : 'Private'; return $this->company ? 'Business' : 'Private';
} }
/** /* METHODS */
* Return the Admin URL to manage the account
*
* @return string
*/
public function getUrlAdminAttribute(): string
{
return sprintf('<a href="/r/account/view/%s">%s</a>',$this->id,$this->account_id);
}
/**
* Return the User URL to manage the account
*
* @return string
*/
public function getUrlUserAttribute(): string
{
return sprintf('<a href="/u/account/view/%s">%s</a>',$this->id,$this->account_id);
}
/* GENERAL METHODS */
/** /**
* Get the due invoices on an account * Get the due invoices on an account
* *
* @return mixed * @return mixed
* @deprecated use invoiceSummary->filter(_balance > 0)
*/ */
public function dueInvoices() public function dueInvoices()
{ {
@ -235,17 +248,97 @@ class Account extends Model implements IDs
} }
/** /**
* Get the external account ID for a specific integration * List of invoices (summary) for this account
* *
* @param External\Integrations $o * @param Collection|NULL $invoices
* @return mixed * @return Collection
*/ */
public function ExternalAccounting(External\Integrations $o) public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder
{ {
return $this return (new Invoice)
->external() ->select([
->where('id','=',$o->id) 'invoices.account_id',
->where('site_id','=',$this->site_id) 'invoices.id as id',
->first(); DB::raw('SUM(item) AS _item'),
DB::raw('SUM(tax) AS _tax'),
DB::raw('SUM(payments) AS _payment'),
DB::raw('SUM(discount)+COALESCE(invoices.discount_amt,0) AS _discount'),
DB::raw('SUM(item_total) AS _item_total'),
DB::raw('SUM(payment_fees) AS _payment_fee'),
DB::raw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0) AS NUMERIC),2) AS _total'),
DB::raw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) AS _balance'),
'invoices.due_at',
'invoices.created_at',
])
->from(
(new Payment)
->select([
'invoice_id',
DB::raw('0 as item'),
DB::raw('0 as tax'),
DB::raw('0 as discount'),
DB::raw('0 as item_total'),
DB::raw('SUM(amount) AS payments'),
DB::raw('SUM(fees_amt) AS payment_fees'),
])
->leftjoin('payment_items',['payment_items.payment_id'=>'payments.id'])
->where('payments.active',TRUE)
->where('payment_items.active',TRUE)
->groupBy(['payment_items.invoice_id'])
->union(
(new InvoiceItem)
->select([
'invoice_id',
DB::raw('ROUND(CAST(SUM(quantity*price_base) AS NUMERIC),2) AS item'),
DB::raw('ROUND(CAST(SUM(amount) AS NUMERIC),2) AS tax'),
DB::raw('ROUND(CAST(SUM(COALESCE(invoice_items.discount_amt,0)) AS NUMERIC),2) AS discount'),
DB::raw('ROUND(CAST(SUM(ROUND(CAST(quantity*price_base AS NUMERIC),2))+SUM(ROUND(CAST(amount AS NUMERIC),2))-SUM(ROUND(CAST(COALESCE(invoice_items.discount_amt,0) AS NUMERIC),2)) AS NUMERIC),2) AS item_total'),
DB::raw('0 as payments'),
DB::raw('0 as payment_fees'),
])
->leftjoin('invoice_item_taxes',['invoice_item_taxes.invoice_item_id'=>'invoice_items.id'])
->rightjoin('invoices',['invoices.id'=>'invoice_items.invoice_id'])
->where('invoice_items.active',TRUE)
->where('invoice_item_taxes.active',TRUE)
->where('invoices.active',TRUE)
->groupBy(['invoice_items.invoice_id']),
),'p')
->join('invoices',['invoices.id'=>'invoice_id'])
->when(($all === FALSE),fn($query)=>$query->where('invoices.account_id',$this->id))
->orderBy('due_at')
->groupBy(['invoices.account_id','invoices.id','invoices.created_at','invoices.due_at','invoices.discount_amt'])
->with(['account']);
}
public function invoiceSummaryDue(Collection $invoices=NULL,bool $all=FALSE): Builder
{
return $this->invoiceSummary($invoices,$all)
->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) > 0');
}
public function invoiceSummaryCredit(Collection $invoices=NULL,bool $all=FALSE): Builder
{
return $this->invoiceSummary($invoices,$all)
->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) < 0');
}
public function invoiceSummaryPast(Collection $invoices=NULL,bool $all=FALSE): Builder
{
return $this->invoiceSummary($invoices,$all)
->join('payment_items',['payment_items.invoice_id'=>'invoices.id'])
->join('payments',['payments.id'=>'payment_items.payment_id'])
->addSelect(DB::raw('max(paid_at) as _paid_at'))
->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) <= 0');
}
/**
* Return the taxed value of a value
*
* @param float $value
* @return float
*/
public function taxed(float $value): float
{
return Tax::calc($value,$this->taxes);
} }
} }

View File

@ -4,7 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class HostServer extends Model class AccountGroup extends Model
{ {
protected $table = 'ab_host_server'; protected $table = 'account_group';
} }

View File

@ -1,46 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey;
class AccountOauth extends Model
{
use NextKey;
const RECORD_ID = 'account_oauth';
public $incrementing = FALSE;
protected $table = 'ab_account_oauth';
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
public $dateFormat = 'U';
protected $casts = [
'oauth_data'=>'array',
];
public function account()
{
return $this->belongsTo(Account::class);
}
public function site()
{
return $this->belongsTo(Site::class);
}
public function User()
{
return $this->belongsTo(User::class);
}
/**
* Get a link token to use when validating account.
*/
public function getLinkTokenAttribute()
{
return strtoupper(substr(md5($this->id.$this->date_last),0,8));
}
}

Some files were not shown because too many files have changed in this diff Show More