Compare commits

...

171 Commits
test ... master

Author SHA1 Message Date
Deon George 7c91082ca8 Fix user creation in order - language_id doesnt have a default value 2024-02-02 10:58:29 +11:00
Deon George 27720ee882 Telephone is now phone 2023-06-16 15:10:36 +10:00
Deon George 8f283f83f2 Fix exception in AccountingInvoiceAdd 2023-05-14 21:39:54 +10:00
Deon George c1bb20dec0 Add event to process webhook payments 2023-05-13 23:51:27 +10:00
Deon George 12b63a506f Minor bug fixes for payment update, internal product link and order billing interval 2023-05-13 22:19:22 +10:00
Deon George a195e4b55b Update to get an individual payment from intuit 2023-05-13 22:01:43 +10:00
Deon George 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
Deon George a32e8e9d05 Added webhook to capture incoming webhooks 2023-05-06 21:48:46 +10:00
Deon George 013bb632d3 Reimplmement service changes going to the service__change table 2023-05-06 17:21:56 +10:00
Deon George 691180b3f0 More product cleanup 2023-05-06 13:53:50 +10:00
Deon George dc74a064ba Normalise usage of Model into Model::class strings 2023-05-05 16:29:57 +10:00
Deon George 820ff2be00 More Product Model optimisation 2023-05-05 16:29:57 +10:00
Deon George 96f799f535 Product Model optimisation 2023-05-05 10:51:28 +10:00
Deon George 0f91ce4940 Updates to Product Model, product updates, enable pricing update, improved formating of product services 2023-05-04 22:21:14 +10:00
Deon George 95bb55aad8 Optimize Groups 2023-05-04 11:50:54 +10:00
Deon George 0ac35c3d43 Product class optimisation 2023-05-04 10:02:25 +10:00
Deon George a5238bfbdc No longer need to test for type, it should exist 2023-05-03 23:41:48 +10:00
Deon George 25dab73a83 model/model_id is now required on products 2023-05-03 23:02:52 +10:00
Deon George 72648ea14d Framework update, and moved markup() helper to new helpers.php 2023-05-03 18:24:14 +10:00
Deon George 4f19da5987 Fix broadband plan change update 2023-05-03 18:09:29 +10:00
Deon George fd110f5c6f Minor bug fixes from live site 2023-05-01 17:18:12 +10:00
Deon George 8fb888a395 Update upstream urls and framework update 2023-03-15 16:21:53 +11:00
Deon George 10931bd156 Updated docker base image and synchronise consistent docker build/test 2023-03-15 16:21:53 +11:00
Deon George 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
520 changed files with 22807 additions and 15552 deletions

View File

@ -19,14 +19,14 @@ BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
QUEUE_DRIVER=database
QUEUE_CONNECTION=database
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=MAIL
MAIL_HOST=smtp
MAIL_PORT=25
MAIL_USERNAME=null
MAIL_PASSWORD=null
@ -43,13 +43,13 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
EZYPAY_TOKEN=
EZYPAY_GUID=
QUICKBOOKS_CLIENT_ID=
QUICKBOOKS_CLIENT_SECRET=
QUICKBOOKS_API_URL=Production
AUTH_GOOGLE_CLIENT_ID=
AUTH_GOOGLE_SECRET=
AUTH_INTUIT_CLIENT_ID=
AUTH_INTUIT_SECRET_KEY=
INTUIT_VERIFYTOKEN=
PAYPAL_MODE=sandbox
PAYPAL_SANDBOX_CLIENT_ID=
PAYPAL_SANDBOX_SECRET=

View File

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

View File

@ -1,33 +1,27 @@
docker:
image: docker:latest
variables:
VERSION: latest
DOCKER_HOST: tcp://docker:2375
stage: build
image: docker:latest
services:
- docker:dind
variables:
VERSION: latest
CACHETAG: build-${VERSION}
DOCKER_HOST: tcp://docker:2375
tags:
- docker
- x86_64
only:
- master
- docker:dind
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
- 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}
- if [ -f init ]; then chmod 500 init; fi
- echo -n ${CI_COMMIT_SHORT_SHA} > VERSION
- rm -rf vendor/ database/schema database/seeders database/factories/*
- docker build -t ${CI_REGISTRY_IMAGE}:${VERSION} .
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
tags:
- docker
- x86_64
only:
- master

View File

@ -1,12 +1,12 @@
test:
image: registry.leenooks.net/leenooks/php:8.0-fpm-ext-test
image: ${CI_REGISTRY}/leenooks/php:8.1-fpm-alpine-mysql-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
- mariadb:10.5
variables:
MYSQL_DATABASE: testing
@ -15,31 +15,34 @@ test:
MYSQL_PASSWORD: test
tags:
- php
- php
only:
- master
- test
- master
- test
before_script:
- mv .env.testing .env
- 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
# Install Composer and project dependencies.
- mkdir -p ${COMPOSER_HOME}
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > ${COMPOSER_HOME}/auth.json; fi
- composer install
# 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
# Add mysql client for schema pre-load
- apt update -o Acquire::ForceIPv4=true && apt install -o Acquire::ForceIPv4=true -y 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 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
# 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 +1,12 @@
FROM registry.leenooks.net/leenooks/php:8.0-fpm-image
FROM registry.dege.au/leenooks/php:8.1-fpm-alpine-mysql
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 \
RUN mkdir -p ${COMPOSER_HOME} \
&& ([ -r auth.json ] && mv auth.json ${COMPOSER_HOME}) || 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
&& rm -rf ${COMPOSER_HOME}/* .git* composer.lock

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_reset = Arr::get($result->getHeader('X-RateLimit-Reset'),0);
if ($api_remain == 0) {
Log::error('API Throttle.',['m'=>__METHOD__]);
if ($api_remain === 0) {
Log::error('API Throttle.',['m'=>__METHOD__,'api_reset'=>$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
{
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
{
return Cache::remember(__METHOD__.http_build_query($opt),86400,function() use ($opt) {
@ -65,4 +138,4 @@ class Ezypay extends Payments
return Collect($this->connect('settlements/'.config('services.ezypay.guid').($opt ? '?'.http_build_query($opt) : '')));
});
}
}
}

View File

@ -17,6 +17,8 @@ abstract class Supplier
protected $o = NULL;
protected $_columns = [];
public const traffic_connection_keys = ['user','pass','url'];
public function __construct(Model $o)
{
$this->o = $o;
@ -26,24 +28,29 @@ abstract class Supplier
/**
* Connect and pull down traffic data
*
* @param array $connection
* @param string $type
* @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()) {
Log::notice(sprintf('%s:API Throttle, waiting [%s]...',self::LOGKEY,$x),['m'=>__METHOD__]);
sleep($x);
}
Log::debug(sprintf('%s:Supplier [%d], fetch data for [%s]...',self::LOGKEY,$this->o->id,$this->o->stats_lastupdate),['m'=>__METHOD__]);
$key = 'Supplier:'.$this->o->id.$this->o->stats_lastupdate;
$result = Cache::remember($key,86400,function() {
$client = $this->getClient();
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.Arr::get($connection,'last');
$response = Http::get($this->o->stats_url,[
$this->login_user_field => $this->o->stats_username,
$this->login_pass_field => $this->o->stats_password,
$this->date_field => $this->o->stats_lastupdate->format('Y-m-d'),
$result = Cache::remember($key,86400,function() use ($connection) {
$response = Http::get(Arr::get($connection,'url'),[
$this->login_user_field => Arr::get($connection,'user'),
$this->login_pass_field => Arr::get($connection,'pass'),
$this->date_field => Arr::get($connection,'last'),
]);
// @todo These API rate limiting is untested.
@ -55,7 +62,6 @@ abstract class Supplier
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
if (preg_match('#^text/html;#',$x=$response->header('Content-Type'))) {
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 App\Jobs\BroadbandTraffic as Job;
use App\Models\AdslSupplier;
use App\Models\Supplier;
class BroadbandTraffic extends Command
{
@ -14,7 +14,8 @@ class BroadbandTraffic extends Command
*
* @var string
*/
protected $signature = 'broadband:traffic:import';
protected $signature = 'broadband:traffic:import'.
' {--s|supplier= : Supplier Name}';
/**
* The console command description.
@ -30,7 +31,14 @@ class BroadbandTraffic extends Command
*/
public function handle()
{
foreach (AdslSupplier::active()->get() as $o)
Job::dispatch($o);
if ($this->option('supplier')) {
$o = Supplier::where('name','like',$this->option('supplier'))->singleOrFail();
Job::dispatchSync($o);
return;
}
foreach (Supplier::active()->get() as $o)
Job::dispatchSync($o);
}
}

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

View File

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

View File

@ -2,11 +2,10 @@
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay;
use App\Models\{Account,Checkout,Payment};
use App\Jobs\PaymentsImport as Job;
class PaymentsEzypayImport extends Command
{
@ -24,16 +23,6 @@ class PaymentsEzypayImport extends Command
*/
protected $description = 'Retrieve payments from Ezypay';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@ -41,67 +30,6 @@ class PaymentsEzypayImport extends Command
*/
public function handle()
{
$poo = 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));
}
}
}
Job::dispatchSync(new Ezypay);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
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';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@ -40,12 +31,10 @@ class PaymentsEzypayNext extends Command
*/
public function handle()
{
$poo = new Ezypay();
$poo = new Ezypay;
foreach ($poo->getCustomers() as $c)
{
if ($c->BillingStatus == 'Inactive')
{
foreach ($poo->getCustomers() as $c) {
if ($c->BillingStatus == 'Inactive') {
$this->info(sprintf('Ignoring INACTIVE: [%s] %s %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname));
continue;
}
@ -53,10 +42,9 @@ class PaymentsEzypayNext extends Command
// 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();
->single();
if (! $ao)
{
if (! $ao) {
$this->warn(sprintf('Missing: [%s] %s %s (%s)',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId));
continue;
}
@ -70,12 +58,33 @@ class PaymentsEzypayNext extends Command
'dateTo'=>now()->addQuarter()->format('Y-m-d'),
])->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)
$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)
$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
$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;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
use App\Models\{Service,Site};
class ServiceList extends Command
{
@ -15,10 +14,10 @@ class ServiceList extends Command
*
* @var string
*/
protected $signature = 'service:list '.
'{--a|active : Active Only}'.
'{--category= : Category}'.
'{--f|fix : Fix start_date}';
protected $signature = 'service:list'.
' {--i|inactive : Include Inactive}'.
' {--t|type= : Type}'.
' {--f|fix : Fix start_date}';
/**
* The console command description.
@ -27,16 +26,6 @@ class ServiceList extends Command
*/
protected $description = 'List all services';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@ -44,50 +33,50 @@ class ServiceList extends Command
*/
public function handle()
{
DB::listen(function($query) {
Log::debug('- SQL',['sql'=>$query->sql,'binding'=>$query->bindings]);
});
$header = '|%13s|%-14s|%-35s|%-40s|%8s|%17s|%12s|%12s|%12s|%12s|%14s|';
$this->warn(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|',
$this->warn(sprintf($header,
'ID',
'CAT',
'Type',
'Product',
'Name',
'active',
'status',
'invoice next',
'start date',
'stop date',
'connect date',
'first invoice'
));
'Active',
'Status',
'Next Invoice',
'Start Date',
'Stop Date',
'Connect Date',
'First Invoice'
));
foreach (Service::all() as $o) {
if ($this->option('active') AND ! $o->isActive())
foreach (Service::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->with(['site'])->cursor() as $o) {
if ((! $this->option('inactive')) AND ! $o->isActive())
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;
$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')) {
$o->date_start = $o->type->service_connect_date;
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->start_at = $o->type->connect_at;
$o->save();
}
$this->info(sprintf('|%10s|%-6s|%-20s|%-50s|%8s|%14s|%10s|%10s|%10s|%10s|%10s|',
$this->info(sprintf($header,
$o->sid,
$o->product->category,
$o->product_name,
$o->name_short,
$o->product->getCategoryNameAttribute(),
substr($o->product->getNameAttribute(),0,35),
substr($o->name_short,0,40),
$o->active ? 'active' : 'inactive',
$o->status,
$o->invoice_next ? $o->invoice_next->format('Y-m-d') : NULL,
$o->date_start ? $o->date_start->format('Y-m-d') : NULL,
$o->date_end ? $o->date_end->format('Y-m-d') : NULL,
($o->type AND $o->type->service_connect_date) ? $o->type->service_connect_date->format('Y-m-d') : NULL,
$c ? $c->date_start->format('Y-m-d') : NULL,
$o->invoice_next?->format('Y-m-d'),
$o->start_at?->format('Y-m-d'),
$o->stop_at?->format('Y-m-d'),
($o->type AND $o->type->connect_at) ? $o->type->connect_at->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;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use App\Mail\TestEmail as MailTest;
use App\Models\User;
use App\Models\{Site,User};
class TestEmail extends Command
{
@ -15,7 +16,7 @@ class TestEmail extends Command
*
* @var string
*/
protected $signature = 'test:email {id}';
protected $signature = 'test:email {site : Site ID} {id : User ID} {email? : Alternative Email}';
/**
* The console command description.
@ -41,9 +42,11 @@ class TestEmail extends Command
*/
public function handle()
{
Config::set('site',Site::findOrFail($this->argument('site')));
$uo = User::find($this->argument('id'));
Mail::to($uo->email)
Mail::to($this->argument('email') ?? $uo->email)
->send(new MailTest($uo));
}
}

View File

@ -5,8 +5,8 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Models\AdslSupplier;
use App\Jobs\BroadbandTraffic;
use App\Models\Supplier;
class Kernel extends ConsoleKernel
{
@ -29,7 +29,7 @@ class Kernel extends ConsoleKernel
{
// @todo This needs to be more generic and dynamic
// Exetel Traffic
$schedule->job(new BroadbandTraffic(AdslSupplier::find(1)))->timezone('Australia/Melbourne')->dailyAt('10:00');
$schedule->job(new BroadbandTraffic(Supplier::find(1)))->timezone('Australia/Melbourne')->dailyAt('10:00');
}
/**

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
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Collection;
use App\Models\ProviderOauth;
use App\Models\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,16 +4,83 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
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,
Supplier,
SupplierDetail};
/**
* 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
{
// @todo Move to reseller
public function service(Service $o)
{
return View('a.service',['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('a.charge.addedit')
->with('o',$o);
}
// @todo Move to reseller
public function charge_pending_account(Request $request,Account $o)
{
return view('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('a.charge.unprocessed');
}
/**
* Record payments on an account.
*
@ -21,56 +88,97 @@ class AdminController extends Controller
* @param Payment $o
* @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()) {
$validation = $request->validate([
'account_id' => 'required|exists:ab_account,id',
'date_payment' => 'required|date',
'checkout_id' => 'required|exists:ab_checkout,id',
'account_id' => 'required|exists:accounts,id',
'paid_at' => 'required|date',
'checkout_id' => 'required|exists:checkouts,id',
'total_amt' => 'required|numeric|min:0.01',
'fees_amt' => 'nullable|numeric|lt:total_amt',
'source_id' => 'nullable|exists:ab_account,id',
'source_id' => 'nullable|exists:accounts,id',
'pending' => 'nullable|boolean',
'notes' => 'nullable|string',
'ip' => 'nullable|ip',
'invoices' => ['nullable','array',function ($attribute,$value,$fail) use ($request) {
if (collect($value)->sum() > $request->post('total_amt'))
'invoices' => ['required','array',function ($attribute,$value,$fail) use ($request) {
if (collect($value)->sum('id') > $request->post('total_amt'))
$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;
$oo->forceFill($request->only(['account_id','date_payment','checkout_id','checkout_id','total_amt','fees_amt','source_id','pending','notes','ip']));
$oo->site_id = config('SITE')->site_id;
$oo->save();
if (! $o->exists) {
$o->site_id = config('site')->site_id;
$o->active = TRUE;
}
$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) {
$ooo = new PaymentItem;
$ooo->invoice_id = $id;
$ooo->alloc_amt = $amount;
$ooo->site_id = config('SITE')->site_id;
$oo->items()->save($ooo);
// See if we already have a payment item that we need to update
$items = $o->items->filter(function($item) use ($id) { return $item->invoice_id == $id; });
if ($items->count() == 1) {
$oo = $items->pop();
if ($amount['id'] == 0) {
$oo->delete();
continue;
}
} else {
$oo = new PaymentItem;
$oo->invoice_id = $id;
}
$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');
->with('success','Payment recorded: '.$o->id);
}
return view('a.payment.add')
return view('a.payment.addedit')
->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('a.payment.unapplied');
}
/**
* Show a list of invoices to apply payments to
*
* @param Request $request
* @param Account $o
* @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')
->with('pid',$request->pid)
->with('o',$o);
}
@ -85,13 +193,13 @@ class AdminController extends Controller
{
if ($request->post()) {
$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_address1' => 'required|string|max:255',
'site_address2' => 'nullable|string|max:255',
'site_city' => 'required|string|max:64',
'site_state' => 'required|string|max:32',
'site_postcode' => 'required|string|max:8',
'site_address1' => 'required|string|min:2|max:255',
'site_address2' => 'nullable|string|min:2|max:255',
'site_city' => 'required|string|min:2|max:64',
'site_state' => 'required|string|min:2|max:32',
'site_postcode' => 'required|string|min:2|max:8',
'site_description' => 'nullable|string|min:5',
'site_phone' => 'nullable|regex:/[0-9 ]+/|min:6|max:12',
'site_fax' => 'nullable|regex:/[0-9 ]+/|min:6|max:12',
@ -102,7 +210,7 @@ class AdminController extends Controller
'email_logo' => 'nullable|image',
]);
$site = config('SITE');
$site = config('site');
// @todo - not currently rendered in the home page
$validated['social'] = [];

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
@ -10,9 +11,7 @@ use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller;
use App\Mail\SocialLink;
use App\Models\Oauth;
use App\Models\AccountOauth;
use App\Models\User;
use App\Models\{ProviderOauth,ProviderToken,User,UserOauth};
use App\Providers\RouteServiceProvider;
class SocialLoginController extends Controller
@ -26,10 +25,13 @@ class SocialLoginController extends Controller
{
$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
$aoo = $oo->accounts->where('userid',$openiduser->id);
$aoo = $oo->users->where('userid',$openiduser->id);
if ($aoo->count() == 1) {
$aoo = $aoo->first();
@ -59,10 +61,10 @@ class SocialLoginController extends Controller
// See if their is an account with this email address
if ($uo->count() == 1) {
$aoo = new AccountOauth;
$aoo = new UserOauth;
$aoo->userid = $openiduser->id;
$aoo->oauth_data = $openiduser->user;
$oo->accounts()->save($aoo);
$oo->users()->save($aoo);
return $this->link($provider,$aoo,$uo->first());
@ -78,16 +80,43 @@ class SocialLoginController extends Controller
return redirect()->intended(RouteServiceProvider::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(RouteServiceProvider::HOME)
->with('success','Token refreshed.');
}
/**
* We have identified the user and oauth, just need them to confirm the link
*
* @param $provider
* @param $provider
* @param UserOauth $ao
* @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')
->with('oauthid',$ao->id)
@ -97,7 +126,7 @@ class SocialLoginController extends Controller
public function linkcomplete(Request $request,$provider)
{
// Load our oauth id
$aoo = AccountOauth::findOrFail($request->post('oauthid'));
$aoo = UserOauth::findOrFail($request->post('oauthid'));
// Check our email matches
if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email'))

View File

@ -2,13 +2,41 @@
namespace App\Http\Controllers;
use App\Models\Invoice;
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
{
/**
* 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)
{
if ($o) {
@ -27,8 +55,29 @@ class CheckoutController extends Controller
return $o->fee($request->post('total',0));
}
/**
* Render a specific invoice for the user
*
* @return View
*/
public function home(): View
{
return View('payment.home');
}
public function pay(Request $request,Checkout $o)
{
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('payment.view',['o'=>$o]);
}
}

View File

@ -2,10 +2,10 @@
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\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{

View File

@ -4,11 +4,9 @@ namespace App\Http\Controllers;
use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode};
use Clarkeash\Doorman\Facades\Doorman;
use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
use App\Models\{Invoice,Service,User};
@ -22,60 +20,18 @@ use App\Models\{Invoice,Service,User};
*/
class HomeController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Logged in users home page
*
* @return Factory|View
* @param User $o
* @return 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) {
$o->load(['accounts','services']);
return View('u.home',['o'=>$o]);
}
if (! $o->exists)
$o = Auth::user();
// If User was null, then test and see what type of logged on user we have
$o = Auth::user();
switch (Auth::user()->role()) {
case 'customer':
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));
return View('home',['o'=>$o]);
}
/**
@ -101,18 +57,7 @@ class HomeController extends Controller
abort(404);
}
return $this->invoice_pdf($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]);
return $this->invoice($o);
}
/**
@ -122,6 +67,7 @@ class HomeController extends Controller
* @param Service $o
* @param string $status
* @return \Illuminate\Http\RedirectResponse
* @deprecated
*/
public function service_progress(Service $o,string $status)
{

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers;
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('u.invoice.home',['o'=>$o])->stream(sprintf('%s.pdf',$o->sid));
}
/**
* Render a specific invoice for the user
*
* @param Invoice $o
* @return View
*/
public function view(Invoice $o): View
{
return View('invoice.view',['o'=>$o]);
}
}

View File

@ -14,33 +14,38 @@ use App\Models\{Account,Product,Service,User};
class OrderController extends Controller
{
// @todo To check
public function __construct()
{
$this->middleware('auth');
}
// @todo To check
public function index()
{
return view('order');
return view('order.home');
}
// @todo To check
public function product_order(Product $o)
{
Theme::set('metronic-fe');
return view('widgets.product_order',['o'=>$o]);
return view('order.widget.order',['o'=>$o]);
}
// @todo To check
public function product_info(Product $o)
{
Theme::set('metronic-fe');
return view('widgets.product_description',['o'=>$o]);
return view('order.widget.info',['o'=>$o]);
}
// @todo To check
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
->sometimes('account_id','required|email',function($input) use ($request) {
return is_null($input->account_id) AND is_null($input->order_email_manual);
@ -59,7 +64,7 @@ class OrderController extends Controller
$po = Product::findOrFail($request->input('product_id'));
// Check we have the custom attributes for the product
$options = $po->orderValidation($request);
$order = $po->orderValidation($request);
if ($request->input('order_email_manual')) {
$uo = User::firstOrNew(['email'=>$request->input('order_email_manual')]);
@ -67,12 +72,13 @@ class OrderController extends Controller
// If this is a new client
if (! $uo->exists) {
// @todo Make this automatic
$uo->site_id = config('SITE')->site_id;
$uo->site_id = config('site')->site_id;
$uo->active = FALSE;
$uo->firstname = '';
$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->language_id = config('site')->language_id; // @todo This might be wrong
$uo->active = 1;
$uo->save();
}
@ -83,10 +89,8 @@ class OrderController extends Controller
$ao = new Account;
//$ao->id = Account::NextId();
// @todo Make this automatic
$ao->site_id = config('SITE')->site_id;
$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->site_id = config('site')->site_id;
$ao->country_id = config('site')->country_id; // @todo This might be wrong
$ao->active = 1;
$uo->accounts()->save($ao);
@ -97,28 +101,30 @@ class OrderController extends Controller
$so = new Service;
// @todo Make this automatic
$so->site_id = config('SITE')->site_id;
$so->product_id = $request->input('product_id');
$so->site_id = config('site')->site_id;
$so->product_id = $po->id;
$so->order_status = 'ORDER-SUBMIT';
$so->orderby_id = Auth::id();
$so->model = get_class($options);
$so->ordered_by = Auth::id();
$so->active = FALSE;
$so->model = $order ? get_class($order) : NULL;
$so->recur_schedule = $po->billing_interval;
if ($options->order_info) {
$so->order_info = $options->order_info;
if ($order && $order->order_info) {
$so->order_info = $order->order_info;
unset($options->order_info);
unset($order->order_info);
}
$so = $ao->services()->save($so);
if ($options instanceOf Model) {
$options->service_id = $so->id;
$options->save();
if ($order instanceOf Model) {
$order->service_id = $so->id;
$order->save();
}
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]);
}
}
}

View File

@ -200,6 +200,7 @@ class PaypalController extends Controller
foreach ($response->result->purchase_units as $pu) {
foreach ($pu->payments->captures as $cap) {
$po = new Payment;
$po->active = TRUE;
switch ($cap->status) {
case 'PENDING':
@ -217,7 +218,7 @@ class PaypalController extends Controller
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_data = $cap->id;
@ -229,7 +230,7 @@ class PaypalController extends Controller
$pio = new PaymentItem;
$pio->site_id = 1; // @todo To implement
$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);

View File

@ -0,0 +1,148 @@
<?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('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('product.home');
}
}

View File

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

View File

@ -3,10 +3,11 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
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
{
@ -14,73 +15,84 @@ class SearchController extends Controller
* Search from the Application Dashboard.
*
* @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 (! $request->input('term'))
return [];
return $result;
$result = collect();
$accounts = ($x=Auth::user()->all_accounts())->pluck('id');
$users = $x->transform(function($item) { return $item->user;});
$account_ids = ($x=Auth::user()->accounts)->pluck('id');
$user_ids = $x->transform(function($item) { return $item->user;})->pluck('id');
# Look for User
foreach (User::Search($request->input('term'))
->whereIN('id',$users->pluck('id'))
->whereIN('id',$user_ids)
->orderBy('lastname')
->orderBy('firstname')
->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
foreach (Account::Search($request->input('term'))
->whereIN('user_id',$users->pluck('id'))
->orderBy('company')
->limit(10)->get() as $o)
->whereIN('user_id',$user_ids)
->orderBy('company')
->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
foreach (Service::Search($request->input('term'))
->whereIN('account_id',$accounts)
->orderBy('id')
->limit(10)->get() as $o)
->whereIN('account_id',$account_ids)
->orderBy('id')
->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
foreach (Invoice::Search($request->input('term'))
->whereIN('account_id',$accounts)
->orderBy('id')
->limit(10)->get() as $o)
->whereIN('account_id',$account_ids)
->orderBy('id')
->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
foreach (Adsl::Search($request->input('term'))
->whereIN('account_id',$accounts)
->orderBy('service_number')
->limit(10)->get() as $o)
{
$result->push(['name'=>sprintf('%s (%s)',$o->name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Broadband']);
if (Gate::any(['wholesaler'],new Payment)) {
# Look for Payments
foreach (Payment::Search($request->input('term'))
->whereIN('account_id',$account_ids)
->limit(10)->get() as $o)
{
$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
foreach (Service\Domain::Search($request->input('term'))
->whereIN('account_id',$accounts)
->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;
return $result->sortBy(function($item) { return $item['category'].$item['name']; })->values();
}
}

View File

@ -2,146 +2,463 @@
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
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 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
{
/* 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('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 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([
'stop_at'=>'required|date',
]);
$validation = $request->validate([
'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',
]);
if (! $o->order_info)
$o->order_info = collect();
$o->type->forceFill($validation['service'])->save();
$o->stop_at = $request->stop_at;
$o->order_info->put('cancel_note',$request->notes);
$o->order_status = 'CANCEL-REQUEST';
$o->save();
return redirect()->back()->with('success','Record updated.');
//@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('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('service.change_request')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
}
/**
* List all the domains managed by the user
*
* @return View
* @todo revalidate
*/
public function domain_list(): View
{
$o = Service\Domain::serviceActive()
->serviceUserAuthorised(Auth::user())
->select('service_domains.*')
->join('ab_service',['ab_service.id'=>'service_domains.service_id'])
->select('service_domain.*')
->join('services',['services.id'=>'service_domain.service_id'])
->with(['service.account','registrar'])
->get();
return view('r.service.domain.list')
return view('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('service.email.list')
->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('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('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 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('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)
{
switch ($o->order_status) {
case 'CANCEL-REQUEST':
if ($request->post()) {
if (! $request->post('date_end'))
return redirect()->back()->withErrors('Cancellation Date not provided');
if ($o->type->validation()) {
Session::put('service_update',true);
$validator = Validator::make($x=$request->post($o->category),$o->type->validation());
$o->date_end = $request->post('date_end');
if ($validator->fails()) {
return redirect()
->back()
->withErrors($validator)
->withInput();
}
foreach (['cancel_notes'] as $key) {
if ($request->post($key))
$o->setOrderInfo($key,$request->post($key));
}
$o->type->forceFill($validator->validated());
$o->order_status='CANCEL-PENDING';
$o->save();
return redirect()->to(url('u/service',$o->id))->with('updated','Service cancellation submitted.');
}
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();
return redirect()->to(url('u/service',$o->id))->with('updated','Order sent notes updated.');
}
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();
foreach ($request->post($o->stype) as $k=>$v) {
$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');
} elseif ($request->post($o->product->category)) {
$o->type->forceFill($request->post($o->product->category));
}
}
private function update_order_status(Service $o)
{
return View('r.service.order.sent',['o'=>$o]);
}
$o->type->save();
private function update_request_cancel(Service $o)
{
return View('u.service.order.cancel',['o'=>$o]);
}
if ($request->post('invoice_next_at'))
$o->invoice_next_at = $request->invoice_next_at;
private function update_provision_planned(Service $o)
{
return View('r.service.order.provision_plan',['o'=>$o]);
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;
}
}
$o->save();
return redirect()->back()->with('success','Record Updated');
}
}

View File

@ -0,0 +1,220 @@
<?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('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('supplier.cost.view',['o'=>$o]);
}
public function cost_add(Supplier $o)
{
return view('supplier.cost.add',['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('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('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('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('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
$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
$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
$days = now()->diffInDays($io->due_date)+1+7;
$days = now()->diffInDays($io->due_at)+1+7;
foreach ($s as $so)
{
if ($so->isInvoiceDueSoon($days))

View File

@ -0,0 +1,56 @@
<?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

@ -2,18 +2,12 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
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
{
public function accounts()
{
return view('account/report');
}
public function products()
{
return view('a/product/report');
return view('product/report');
}
public function services()
{
return view('service/report');
}
}

View File

@ -36,7 +36,6 @@ class Kernel extends HttpKernel
\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,
],
@ -59,7 +58,6 @@ class Kernel extends HttpKernel
'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,

View File

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

View File

@ -43,7 +43,7 @@ class SetSite
}
// Set who we are in SETUP.
Config::set('SITE',$so);
Config::set('site',$so);
if (! $request->ajax())
View::share('site',$so);

View File

@ -3,7 +3,7 @@
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
@ -19,5 +19,10 @@ class TrustProxies extends Middleware
*
* @var int
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

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
*/
public function getLIDattribute(): string;
public function getLIDAttribute(): string;
/**
* Return the system ID of the item
*
* @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
{
/**
* Months the service is contracted for.
*
* @return int
*/
public function getContractTermAttribute(): int;
/**
* Return the Service Description.
*
@ -16,7 +23,7 @@ interface ServiceItem
/**
* Date the service expires
*/
public function getServiceExpireAttribute(): Carbon;
public function getServiceExpireAttribute(): ?Carbon;
/**
* Return the Service Name.
@ -25,6 +32,13 @@ interface ServiceItem
*/
public function getServiceNameAttribute(): string;
/**
* Has this service expired
*
* @return bool
*/
public function hasExpired(): bool;
/**
* Is this service in a contract
*

View File

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

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

View File

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

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
@ -13,8 +14,8 @@ class OrderRequestApprove extends Mailable
{
use Queueable, SerializesModels;
public $service;
public $notes;
public Service $service;
public string $notes;
/**
* Create a new message instance.
@ -22,7 +23,7 @@ class OrderRequestApprove extends Mailable
* @param Service $o
* @param string $notes
*/
public function __construct(Service $o,$notes='')
public function __construct(Service $o,string $notes='')
{
$this->service = $o;
$this->notes = $notes;
@ -35,12 +36,14 @@ class OrderRequestApprove extends Mailable
*/
public function build()
{
switch ($this->service->category)
{
case 'ADSL': $subject = sprintf('%s: %s',$this->service->category,$this->service->service_adsl->service_address);
Config::set('site',$this->service->site);
// @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;
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;
default:

View File

@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use App\Models\Service;
@ -13,15 +14,15 @@ class OrderRequestReject extends Mailable
{
use Queueable, SerializesModels;
public $service;
public $reason;
public Service $service;
public string $reason;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(Service $o,$reason)
public function __construct(Service $o,string $reason)
{
$this->service = $o;
$this->reason = $reason;
@ -34,6 +35,8 @@ class OrderRequestReject extends Mailable
*/
public function build()
{
Config::set('site',$this->service->site);
return $this
->markdown('email.admin.order.reject')
->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\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use App\Models\{AccountOauth,User};
use App\Models\{Site,User,UserOauth};
class SocialLink extends Mailable
{
use Queueable, SerializesModels;
public $token;
public $user;
public string $token;
public Site $site;
public ?User $user;
/**
* Create a new message instance.
*
* @param User $o
* @param string $token
* @param UserOauth $o
*/
public function __construct(AccountOauth $o)
public function __construct(UserOauth $o)
{
$this->site = $o->site;
$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()
{
Config::set('site',$this->site);
return $this
->markdown('email.system.social_link')
->subject('Link your Account')
->with([
'site'=>$this->user->site,
]);
'site'=>$this->site,
]);
}
}

View File

@ -6,36 +6,41 @@ 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\User;
class TestEmail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $o)
{
$this->user = $o;
}
public User $user;
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->markdown('email.system.test_email')
->subject('Just a test...')
->with([
'site'=>$this->user->site,
'user'=>$this->user,
]);
}
}
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $o)
{
$this->user = $o;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
Config::set('site',$this->user->site);
return $this
->markdown('email.system.test_email')
->subject('Just a test...')
->with([
'site'=>$this->user->site,
'user'=>$this->user,
]);
}
}

View File

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

View File

@ -2,52 +2,50 @@
namespace App\Models;
use Awobaz\Compoships\Compoships;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
use App\Models\Scopes\SiteScope;
use App\Interfaces\IDs;
use App\Traits\NextKey;
use App\Traits\SiteID;
/**
* Class Account
* Service Accounts
*
* Attributes for accounts:
* + lid: : Local ID for account
* + sid: : System ID for account
* + lid : Local ID for account
* + sid : System ID for account
* + name : Account Name
* + taxes : Taxes Applicable to this account
*
* @package App\Models
*/
class Account extends Model implements IDs
{
use HasFactory,NextKey,ScopeActive;
use Compoships,HasFactory,ScopeActive,SiteID;
const RECORD_ID = 'account';
public $incrementing = FALSE;
/* INTERFACES */
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
public function getLIDAttribute(): string
{
return sprintf('%04s',$this->id);
}
protected $appends = [
'active_display',
'name',
'services_count_html',
'switch_url',
];
public $dateFormat = 'U';
protected $visible = [
'id',
'active_display',
'name',
'services_count_html',
'switch_url',
];
public function getSIDAttribute(): string
{
return sprintf('%02s-%s',$this->site_id,$this->getLIDAttribute());
}
/* RELATIONS */
public function charges()
{
return $this->hasMany(Charge::class);
}
/**
* Return the country the user belongs to
*/
@ -61,9 +59,20 @@ class Account extends Model implements IDs
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');
}
/**
* @return mixed
* @todo This needs to be optimised, to only return outstanding invoices and invoices for a specific age (eg: 2 years worth)
*/
public function invoices()
{
return $this->hasMany(Invoice::class);
return $this->hasMany(Invoice::class)
->active()
->with(['items.taxes','paymentitems.payment']);
}
public function language()
@ -73,16 +82,37 @@ class Account extends Model implements IDs
public function payments()
{
return $this->hasMany(Payment::class);
return $this->hasMany(Payment::class)
->active()
->with(['items']);
}
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');
}
public function services($active=FALSE)
{
$query = $this->hasMany(Service::class);
$query = $this->hasMany(Service::class,['account_id','site_id'],['id','site_id'])
->withoutGlobalScope(SiteScope::class)
->with(['product.translate','invoice_items']);
return $active ? $query->active() : $query;
}
public function site()
{
return $this->belongsTo(Site::class);
}
public function taxes()
{
return $this->hasMany(Tax::class,'country_id','country_id');
}
public function user()
{
return $this->belongsTo(User::class);
@ -95,7 +125,7 @@ class Account extends Model implements IDs
*
* @param $query
* @param string $term
* @return
* @return mixed
*/
public function scopeSearch($query,string $term)
{
@ -115,27 +145,6 @@ class Account extends Model implements IDs
/* 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
*
@ -151,76 +160,36 @@ class Account extends Model implements IDs
}
/**
* Return the Account Unique Identifier
* @return string
* @deprecated use getSIDAttribute()
* Account breadcrumb to render on pages
*
* @return array
*/
public function getAIDAttribute()
public function getBreadcrumbAttribute(): array
{
return $this->getSIDAttribute();
return [$this->name => url('u/home',$this->user_id)];
}
/**
* Account Local ID
* Return the account name
*
* @return string
* @return mixed|string
*/
public function getLIDAttribute(): string
public function getNameAttribute(): string
{
return sprintf('%04s',$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());
return $this->company ?: ($this->user_id ? $this->user->getSurFirstNameAttribute() : 'LID:'.$this->id);
}
/**
* Account System ID
* Return the type of account this is - if it has a company name, then its a business account.
*
* @return string
*/
public function getSIDAttribute(): 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 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 */
/* METHODS */
/**
* Get the due invoices on an account
@ -235,17 +204,13 @@ class Account extends Model implements IDs
}
/**
* Get the external account ID for a specific integration
* Return the taxed value of a value
*
* @param External\Integrations $o
* @return mixed
* @param float $value
* @return float
*/
public function ExternalAccounting(External\Integrations $o)
public function taxed(float $value): float
{
return $this
->external()
->where('id','=',$o->id)
->where('site_id','=',$this->site_id)
->first();
return Tax::calc($value,$this->taxes);
}
}

View File

@ -4,7 +4,7 @@ namespace App\Models;
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));
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\OrderServiceOptions;
class AdslPlan extends Model
{
use OrderServiceOptions;
protected $table = 'ab_adsl_plan';
protected $order_attributes = [
'options.address'=>[
'request'=>'options.address',
'key'=>'service_address',
'validation'=>'required|string:10',
'validation_message'=>'Address is a required field.',
],
'options.notes'=>[
'request'=>'options.notes',
'key'=>'order_info.notes',
'validation'=>'present',
'validation_message'=>'Special Instructions here.',
],
];
protected $order_model = Service\Adsl::class;
public function product()
{
return $this->hasOne(AdslSupplierPlan::class,'id','adsl_supplier_plan_id');
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Models;
use App\Models\Service\AdslTraffic;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class AdslSupplier extends Model
{
protected $table = 'ab_adsl_supplier';
protected $dates = [
'stats_lastupdate',
];
public $timestamps = FALSE;
/** SCOPES */
/**
* Only query active categories
*/
public function scopeActive($query)
{
return $query->where('active',TRUE);
}
/** METHODS **/
/**
* Return the traffic records, that were not matched to a service.
*
* @param Carbon $date
* @return Collection
*/
public function trafficMismatch(Carbon $date): Collection
{
return AdslTraffic::where('date',$date->format('Y-m-d'))
->where('supplier_id',$this->id)
->whereNULL('ab_service_adsl_id')
->get();
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Models\Base;
use Illuminate\Database\Eloquent\Model;
use App\Models\Product;
//@todo column prod_plugin_file should no longer be required
abstract class ProductType extends Model
{
public $timestamps = FALSE;
public $dateFormat = 'U';
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Models\Base;
use Illuminate\Database\Eloquent\Model;
use App\Models\Service;
abstract class ServiceType extends Model
{
public $timestamps = FALSE;
public $dateFormat = 'U';
/**
* @NOTE: The service_id column could be discarded, if the id column=service_id
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function service()
{
return $this->morphOne(Service::class,'type','model','id','service_id');
}
/** SCOPES */
/**
* Search for a record
*
* @param $query
* @param string $term
* @return
*/
public function scopeSearch($query,string $term)
{
return $query
->with(['service'])
->join('ab_service','ab_service.id','=',$this->getTable().'.service_id')
->Where('ab_service.id','like','%'.$term.'%');
}
/** ATTRIBUTES **/
public function getTypeAttribute()
{
return strtolower((new \ReflectionClass($this))->getShortName());
}
}

View File

@ -2,16 +2,83 @@
namespace App\Models;
use Awobaz\Compoships\Compoships;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use App\Traits\SiteID;
/**
* CLEANUP NOTES:
* + Charge Date should not be null
* + Attributes should be a collection array
* + type should not be null
* + It would be useful, given an array of Charges to call a function that renders them into invoice format. This may provide consistence and be the single view of how charges do look on an invoice.
*/
class Charge extends Model
{
protected $table = 'ab_charge';
protected $dates = ['date_charge'];
public $dateFormat = 'U';
use Compoships,SiteID;
protected $casts = [
'attributes' => 'json',
];
protected $dates = [
'start_at',
'stop_at',
'charge_at', // The date the charge applies - since it can be different to created_at
];
public const sweep = [
// 0 => 'Daily',
// 1 => 'Weekly',
// 2 => 'Monthly',
// 3 => 'Quarterly',
// 4 => 'Semi-Annually',
// 5 => 'Annually',
6 => 'Service Rebill',
];
/* RELATIONS */
public function account()
{
return $this->belongsTo(Account::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
public function service()
{
return $this->belongsTo(Service::class);
}
/* SCOPES */
public function scopeUnprocessed($query)
{
return $query
->where('active',TRUE)
->whereNotNull('charge_at')
->whereNotNull('type')
->where(function($q) {
return $q->where('processed',FALSE)
->orWhereNull('processed');
});
}
/* ATTRIBUTES */
public function getNameAttribute()
{
return sprintf('%s %s',$this->description,join('|',unserialize($this->getAttribute('attributes'))));
return sprintf('%s %s',$this->description,$this->getAttribute('attributes') ? join('|',unserialize($this->getAttribute('attributes'))) : '');
}
public function getTypeNameAttribute(): string
{
return Arr::get(InvoiceItem::type,$this->type);
}
}

View File

@ -2,33 +2,43 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Leenooks\Traits\ScopeActive;
class Checkout extends Model
{
protected $table = 'ab_checkout';
public $timestamps = FALSE;
use ScopeActive;
protected $casts = [
'plugin_data'=>'json',
];
/* RELATIONS */
public function payments()
{
return $this->hasMany(Payment::class);
}
/** SCOPES **/
/* STATIC METHODS */
/**
* Search for a record
*
* @param $query
* @param string $term
* @return
*/
public function scopeActive($query)
public static function available(): Collection
{
return $query->where('active',TRUE);
return self::active()->get();
}
/** FUNCTIONS **/
/* ATTRIBUTES */
public function getIconAttribute(): string
{
switch(strtolower($this->name)) {
case 'paypal': return 'fab fa-cc-paypal';
default: return 'fas fa-money-bill-alt';
}
}
/* METHODS */
public function fee(float $amt,int $items=1): float
{

66
app/Models/Cost.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Cost\{Broadband,Generic,Phone};
class Cost extends Model
{
use HasFactory;
protected $dates = [
'billed_at',
];
protected $with = [
'broadbands',
'generics',
'phones',
];
/* RELATIONS */
public function broadbands()
{
return $this->hasMany(Broadband::class)
->where('active',TRUE);
}
public function generics()
{
return $this->hasMany(Generic::class)
->where('active',TRUE);
}
public function phones()
{
return $this->hasMany(Phone::class)
->where('active',TRUE);
}
/* ATTRIBUTES */
public function getTotalBroadbandAttribute(): float
{
return $this->broadbands->sum('base')+$this->broadbands->sum('excess');
}
public function getTotalGenericAttribute(): float
{
return $this->generics->sum('base')+$this->generics->sum('excess');
}
public function getTotalPhoneAttribute(): float
{
return $this->phones->sum('base')+$this->phones->sum('excess');
}
public function getTotalAttribute(): float
{
return $this->getTotalBroadbandAttribute()+$this->getTotalGenericAttribute()+$this->getTotalPhoneAttribute();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models\Cost;
use App\Models\Service;
use App\Models\Service\Broadband as BroadbandService;
class Broadband extends Type
{
protected $table = 'cost_broadband';
/* RELATIONS */
public function service()
{
return $this->hasOneThrough(Service::class,BroadbandService::class,'id','id','service_broadband_id','service_id');
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models\Cost;
use App\Models\Service;
use App\Models\Service\Generic as GenericService;
class Generic extends Type
{
protected $table = 'cost_generic';
/* RELATIONS */
public function service()
{
return $this->hasOneThrough(Service::class,GenericService::class,'id','id','service_generic_id','service_id');
}
}

18
app/Models/Cost/Phone.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models\Cost;
use App\Models\Service;
use App\Models\Service\Phone as PhoneService;
class Phone extends Type
{
protected $table = 'cost_phone';
/* RELATIONS */
public function service()
{
return $this->hasOneThrough(Service::class,PhoneService::class,'id','id','service_phone_id','service_id');
}
}

26
app/Models/Cost/Type.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace App\Models\Cost;
use Illuminate\Database\Eloquent\Model;
abstract class Type extends Model
{
public $timestamps = FALSE;
protected $dates = [
'start_at',
'end_at',
];
/* RELATIONS */
abstract public function service();
/* ATTRIBUTES */
public function getCostAttribute(): float
{
return $this->base+$this->excess;
}
}

View File

@ -6,9 +6,10 @@ use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
protected $table = 'ab_country';
public $timestamps = FALSE;
/* RELATIONS */
/**
* The currency this country belongs to
*/
@ -21,12 +22,4 @@ class Country extends Model
{
return $this->hasMany(Tax::class);
}
/**
* The accounts in this country
*/
public function users()
{
return $this->hasMany(User::class);
}
}

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