Compare commits

..

258 Commits
test ... master

Author SHA1 Message Date
3fc676fa26 BCC invoice emails to the admin
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2024-09-19 18:45:46 +10:00
a65c81871b Fix adding the product_id to account based charges (when services are not active)
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 38s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-09-16 11:36:55 +10:00
7ec28218fa Remove more old references to @js datatables
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2024-08-24 18:21:49 +10:00
2627cea3b5 Update supplier/cost with components 2024-08-24 17:37:55 +10:00
b877a2b673 Put back google social login
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-23 21:51:29 +10:00
89fb347806 More intuit optimisations in Console/Commands 2024-08-23 15:27:34 +10:00
e39dde05d8 Fixes for intuit:invoice:add, TaxSync and InvoiceSync
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-22 21:43:06 +10:00
2c3665650c Use map() instead of transform(), use fn() instead of function(), consistent coding for form.select
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Failing after 28s
Create Docker Image / Final Docker Image Manifest (push) Has been skipped
2024-08-18 14:11:35 +10:00
5139b26a05 Change supplier resources to use components 2024-08-18 14:11:35 +10:00
283ae06a5c Fixes for service change, validation added for date and product_id
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-17 15:15:15 +10:00
23f23dfe40 Fix showing all traffic usage for broadband
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-17 14:39:52 +10:00
7e784c3e81 Show cancel date on service information widget
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-17 13:17:07 +10:00
a41a69676e Get helpdesk email address from configuration
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-17 10:47:33 +10:00
d5b5de3086 Framework updates
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 39s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-17 10:38:08 +10:00
6ac1b11864 Add validation to service cancellation, and displaying cancellation costs if any 2024-08-17 10:33:56 +10:00
7a41dd803f Only show estimated invoicing if we have a next invoice to show
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-16 17:51:38 +10:00
3b40e92c48 Improvements for service_change and service_cancel
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-08-16 08:20:58 +10:00
5f66987a3e Fixes for ordering, the themes are in frontend.metronic, not backend.adminlte
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-15 21:30:34 +10:00
b4c7c3ad20 Order wasnt showing all accounts for the logged in user
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-15 20:19:39 +10:00
8179ad60e1 Put back weblog logging, lost when updating
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-15 08:34:34 +10:00
e7f1ab638f Added TxnTaxDetail to InvoiceAdd 2024-08-15 08:34:34 +10:00
33ba7903f2 Framework updates 2024-08-14 23:37:20 +10:00
f1031beff6 Rework products with components 2024-08-14 23:37:20 +10:00
1b581e9feb Intuit commands updates 2024-08-12 23:31:59 +10:00
bab1f45234 Moved some basic intuit commands to leenooks/intuit 2024-08-12 20:58:35 +10:00
5abc07b712 Renamed leenooks dreamscape/intuit back to leenooks namespace 2024-08-11 21:17:36 +10:00
4c273364c7 Fix rendering of invoice items, and links to payments and add payment dates to invoice
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-11 00:36:10 +10:00
ef0d4dc773 Change ScopeServiceUserAuthorised to ScopeAccountUserAuthorised. Scope payments to AccountUserAuthorised, and added PaymentPolicy
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 55s
Create Docker Image / Final Docker Image Manifest (push) Successful in 12s
2024-08-10 23:53:13 +10:00
f60727f5fb Framework updates
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-10 22:26:41 +10:00
222fd5092e Move setup out of resources/a 2024-08-10 22:21:38 +10:00
f1dd68a737 Fixes for cart and payment/paypal processing 2024-08-10 22:17:21 +10:00
efbb3d091f Separated Checkout and Payment controllers, updates to checkout and payments 2024-08-10 10:14:47 +10:00
06f25d5d4d Resize and compress web homepage images 2024-08-09 19:44:27 +10:00
c54b0fdc79 Remove redundant service controller method and view 2024-08-03 21:45:47 +10:00
2e9f87550c Move traffic mismatch mail tempaltes to admin/service
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-08-03 11:49:34 +10:00
78a8f63ac9 Remove social link items and update test email 2024-08-03 11:40:37 +10:00
df3f7e31be Update password reset email 2024-08-03 11:33:23 +10:00
0469d64577 Move email/ resources to mail/, added invoice generated email to admin, updated email template 2024-08-03 11:33:23 +10:00
f8453ae391 More work on moving service updates to use components, move 'host' to 'hosting', move some redundant views 2024-08-01 17:34:31 +10:00
078dc6ab39 Update framework
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2024-08-01 10:03:34 +10:00
7e383511ab Update phone service update form to use form components 2024-08-01 10:03:34 +10:00
725b6f317d Provide a link to the account in reseller account list 2024-08-01 10:03:34 +10:00
f43748e20a Add account next invoice 2024-08-01 10:03:34 +10:00
0b5bc9e012 Rework service, removed redundant code, service invoicing improvements 2024-07-31 22:37:04 +10:00
5f10175b35 Updated datatables, using @pa instead of @js/@css, using conditionalPaging in datatables
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-07-28 21:33:30 +10:00
1c4cb6f38c Fix usage_broadband, since our usage data is a float
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-26 14:46:06 +10:00
667109150d Added API stub configuration 2024-07-26 14:46:06 +10:00
9380850395 Move charge actions to ChargeController, implemented charge delete 2024-07-26 12:33:42 +10:00
756f550b43 Cosmetic code changes, no functional changes 2024-07-25 14:44:09 +10:00
9277d42196 Fix order URL
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-25 14:09:24 +10:00
743374cb17 Move charge to under service 2024-07-25 14:08:26 +10:00
ddd44b643f Service display pricing, as a result of moving to psql. Service information updates 2024-07-24 20:17:23 +10:00
79237868cb Moved service.widget.status to a component 2024-07-24 17:45:32 +10:00
14609fb377 Fix display of Billing Start Date and other minor items
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 30s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-07-24 16:34:13 +10:00
1bae121481 Fix search now that supplier_user is account_supplier 2024-07-24 14:53:18 +10:00
d6a2c70146 Update service update to use components, enhanced form handling and submission. Added pppoe to broadband and changed validation to allow for longer service number.
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2024-07-24 14:33:14 +10:00
46075745d2 Move user suppliers to account suppliers 2024-07-24 09:32:17 +10:00
b145856ce9 Update setup with new component based layout
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-23 20:25:32 +10:00
45794ff109 Put back site_id middleware 2024-07-23 20:25:32 +10:00
c91a2fa8e5 Added Passkey login, fixed password reset as a result of updating laravel 2024-07-23 20:25:32 +10:00
b486a0eac4 Moving accounting commands into an Intuit/ namespace, updates to intuit module 2024-07-23 20:25:32 +10:00
28aa1f9dc8 Home page performance optimisations 2024-07-23 20:25:32 +10:00
f561139d45 Optimise Invoice 2024-07-23 20:25:32 +10:00
29bccbf72f Update laravel jobs/failed_jobs tables
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-07 22:09:15 +10:00
2dc56d0321 Update dependant libraries
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2024-07-07 21:31:23 +10:00
09f2eb8d9d Remove binary attributes from DB, should be json columns
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-07-07 21:22:14 +10:00
76889728cd Addresses now a collection 2024-07-07 21:22:14 +10:00
0d9dbafcf1 Optimise Service model 2024-07-07 21:22:14 +10:00
b4f3db04fc Removed redundant functions in Account, Optimised User a little more, Moved Ezypay Commands to new Ezypay folder 2024-07-07 15:18:57 +10:00
70e94bf6e6 Fix search and psql like queries need to be ilike for case insensitivity 2024-07-07 10:32:26 +10:00
61fe84498a Update invoice query to allow for missing invoice_item_taxes records, discount is now removed from price base before calculating item_total
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-06 22:17:41 +10:00
3c7e2bbbc9 Move DB queries into jobs, so that the scheduler and artisan command calls doesnt evaluate them until the job is actually run
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 35s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2024-07-06 20:01:42 +10:00
844d509834 Move invoice blades around. Added invoices in credit view
Some checks failed
Create Docker Image / Build Docker Image (x86_64) (push) Failing after 28s
Create Docker Image / Final Docker Image Manifest (push) Has been skipped
2024-07-06 19:28:14 +10:00
f458bb19c5 Put back daily schedule job to collect broadband traffic 2024-07-06 11:16:18 +10:00
76d43a81c8 Temporarily fix invoice emailing 2024-07-06 11:06:07 +10:00
b3d5bf05a9 Fix handling of discounts in invoiceSummary(), added invoiceSummaryCredit() to show invoices in credit 2024-07-06 11:06:07 +10:00
f0ec35f463 Update laravel/leenooks module 2024-07-06 11:06:07 +10:00
326b1dcfc5 User optimisation and code cleanup 2024-07-06 11:06:07 +10:00
b6b036e06d Optimisations for resellers home page 2024-07-06 11:06:07 +10:00
e7ac329d24 Home page performance optimisations 2024-07-06 11:06:07 +10:00
648d941893 Code refactor work. New optimised query to get invoice status summary for an account 2024-07-06 11:06:07 +10:00
59dc825bf7 Update laravel framework from 9 to 11, removed some old packages 2024-07-06 11:06:07 +10:00
1b4504cee2 Fix dashboard SQL query error, doesnt work with postgres 2024-07-06 09:00:36 +10:00
74864e66cb Remove the error dump from a 500 message 2024-07-06 09:00:36 +10:00
c0cffc3e2c Framework update 2024-07-06 09:00:36 +10:00
4e450d4efc Updated for gitea 2024-07-06 09:00:36 +10:00
1374d38545 Redo database migrations for pgsql 2024-07-06 09:00:36 +10:00
7c91082ca8 Fix user creation in order - language_id doesnt have a default value 2024-02-02 10:58:29 +11:00
27720ee882 Telephone is now phone 2023-06-16 15:10:36 +10:00
8f283f83f2 Fix exception in AccountingInvoiceAdd 2023-05-14 21:39:54 +10:00
c1bb20dec0 Add event to process webhook payments 2023-05-13 23:51:27 +10:00
12b63a506f Minor bug fixes for payment update, internal product link and order billing interval 2023-05-13 22:19:22 +10:00
a195e4b55b Update to get an individual payment from intuit 2023-05-13 22:01:43 +10:00
8ad9e73abb Added intuit payments 2023-05-13 21:20:56 +10:00
Deon George
a518158ccf Framework update 2023-05-12 23:05:43 +10:00
Deon George
ad2f6f3a7f Inuit sync of tax, product accounting, accounts and invoices 2023-05-12 20:09:51 +10:00
Deon George
e2d8f8a096 Command to add accounts to accounting 2023-05-10 17:56:52 +09:00
Deon George
11b554daad Rename account_provider 2023-05-10 16:39:31 +09:00
Deon George
17ebbb71e8 Integration with Intuit - get accounting details for products 2023-05-10 12:59:42 +09:00
Deon George
dde11f73f5 Changed home screen to use account models instead of user model. Home screen now shows multiple accounts 2023-05-09 19:28:51 +09:00
Deon George
790ece14d1 Change product name_detail, name to name, pid 2023-05-09 17:12:07 +09:00
Deon George
45dd74aad4 Added product report, showing just active products and number of services 2023-05-09 16:50:39 +09:00
Deon George
b3539e6c7e Added Account report, renamed Product to Service List 2023-05-09 16:32:02 +09:00
Deon George
f3ecc12494 Only show intuit link to reseller users 2023-05-09 10:14:45 +09:00
Deon George
fe4bc3acef Deprecate price_overriden 2023-05-09 10:09:00 +09:00
a32e8e9d05 Added webhook to capture incoming webhooks 2023-05-06 21:48:46 +10:00
013bb632d3 Reimplmement service changes going to the service__change table 2023-05-06 17:21:56 +10:00
691180b3f0 More product cleanup 2023-05-06 13:53:50 +10:00
dc74a064ba Normalise usage of Model into Model::class strings 2023-05-05 16:29:57 +10:00
820ff2be00 More Product Model optimisation 2023-05-05 16:29:57 +10:00
96f799f535 Product Model optimisation 2023-05-05 10:51:28 +10:00
0f91ce4940 Updates to Product Model, product updates, enable pricing update, improved formating of product services 2023-05-04 22:21:14 +10:00
95bb55aad8 Optimize Groups 2023-05-04 11:50:54 +10:00
0ac35c3d43 Product class optimisation 2023-05-04 10:02:25 +10:00
a5238bfbdc No longer need to test for type, it should exist 2023-05-03 23:41:48 +10:00
25dab73a83 model/model_id is now required on products 2023-05-03 23:02:52 +10:00
72648ea14d Framework update, and moved markup() helper to new helpers.php 2023-05-03 18:24:14 +10:00
4f19da5987 Fix broadband plan change update 2023-05-03 18:09:29 +10:00
fd110f5c6f Minor bug fixes from live site 2023-05-01 17:18:12 +10:00
8fb888a395 Update upstream urls and framework update 2023-03-15 16:21:53 +11:00
10931bd156 Updated docker base image and synchronise consistent docker build/test 2023-03-15 16:21:53 +11:00
a6f01d0864 Fix display of supplier products and offerings - wasnt including all services 2023-03-15 16:21:53 +11:00
Deon George
b719efb58c Rework on product name/description and translate 2022-10-18 23:23:45 +11:00
Deon George
bfd17b0686 Fix invoices not being generated when price is null, service update rendering updates 2022-10-18 17:24:53 +11:00
Deon George
a87560ff96 Fix invoices being generated for suspended/external billing and zero price items 2022-10-18 15:58:14 +11:00
Deon George
c96d264c8f Enable creation of domains and domain service editing 2022-10-18 14:38:57 +11:00
Deon George
0b4e3a9341 Dont save payments with a zero alloc amount, delete payments with a zero alloc amount, fix javascript calculating payment balance 2022-10-18 10:23:11 +11:00
Deon George
6aa30f537b Fix invoice view when not logged in 2022-09-30 09:35:58 +10:00
Deon George
ffcab790fc Fix invoice emailing, and improved formatting 2022-09-30 08:21:16 +10:00
Deon George
b065d15f60 Service paid_to can be null if no invoices created yet 2022-09-30 08:21:15 +10:00
Deon George
319fa32754 Fix phone contract length display 2022-09-30 08:21:15 +10:00
Deon George
15a3b11d2e Enable editing of address for phone/broadband, fix billing start for phone 2022-09-30 08:21:15 +10:00
Deon George
ec99a5ff75 Add Compoships for multile key relationships, first implemented with Service::class 2022-09-30 08:21:15 +10:00
Deon George
2a19f14adb Fixes to emailing cancel requests. Changes to email jobs with no site_id 2022-09-29 12:49:38 +10:00
Deon George
1667b8c1df Fix broadband traffic import, when new services cannot be found. Fix broadband update so that start_at = connect_at 2022-09-29 11:22:16 +10:00
Deon George
5b4aa5c73e WIP: Fix invoice_next_at date on service update for broadband - must do the others, and create a service validator 2022-09-07 15:15:02 +10:00
Deon George
3cae12b256 Framework update, intuit update 2022-08-25 11:25:03 +10:00
Deon George
39db6303c2 Fix email generation and sending via CLI 2022-08-25 11:08:10 +10:00
Deon George
8955df84cd Editing product and enabled updating the accounting field 2022-08-20 23:35:41 +10:00
Deon George
8d920e1ba1 Some product rework 2022-08-20 23:01:03 +10:00
Deon George
71b252843c Show accounting link on accounts that are linked 2022-08-19 20:12:08 +10:00
Deon George
1deda523b4 Fix queue configuration, fix AccountingAccountSync description 2022-08-19 17:18:37 +10:00
Deon George
73d92f25c1 Added cost import via web 2022-08-19 15:12:56 +10:00
Deon George
798608cebd Fix site_id table references 2022-08-19 13:52:23 +10:00
Deon George
4b85e01e93 Customer account sync with Intuit 2022-08-19 13:52:23 +10:00
Deon George
c1a64e2094 Implement token refresh 2022-08-18 23:29:49 +10:00
Deon George
8fd79ce23e Initial integration with Quicken (Intuit API), rework oauth tables, update/test google login 2022-08-12 14:53:06 +10:00
Deon George
70571cb6ac Include ID in Supplier Account Sync and add supplier id integrity constraint 2022-08-11 11:04:35 +10:00
Deon George
f5d535daa7 Fix supplier_user being unique for each supplier and id, fix search looking for users with a supplier_id 2022-08-10 17:05:57 +10:00
Deon George
d358620b46 Update dreamscape api to get all domains 2022-08-10 16:10:08 +10:00
Deon George
e4c1305da5 Added Supplier Domain importing - using dreamscape API 2022-08-10 15:30:39 +10:00
Deon George
53c665787e Add SameSite attribute to cookies 2022-08-10 15:30:39 +10:00
Deon George
2722c92bcf Add supplier linking 2022-08-10 15:30:39 +10:00
Deon George
a1fd36aa6f Update supplier details to use form helpers, and added in api settings 2022-08-06 00:22:22 +10:00
Deon George
ae3f97d890 Fix supplier configuration 2022-08-05 22:21:17 +10:00
Deon George
17f07dafc0 Added leenooks/dreamscape 2022-08-05 17:08:22 +10:00
Deon George
20c91e8e31 Using search while the session has expired will generate a 401 2022-08-03 16:34:23 +10:00
Deon George
a52c20993b Fix existing cancel workflow 2022-08-03 15:47:09 +10:00
Deon George
dd76fda274 Hosting domain_name input, ensure we redirect back to the update page 2022-08-03 15:28:27 +10:00
Deon George
2bac177618 Some service host/domain updates, including schema updates 2022-08-02 22:36:35 +10:00
Deon George
06b1eca306 Updates to service updating - phone 2022-08-02 08:49:17 +10:00
Deon George
360182b6bb Fix old products report 2022-08-01 21:06:57 +10:00
Deon George
7feec266b8 Updates to service updating - broadband 2022-08-01 20:34:10 +10:00
Deon George
de3f1a534b Framework update 2022-08-01 15:00:56 +10:00
Deon George
97f5c84f23 Consolidate service resources into a top level services/ directory 2022-08-01 14:04:04 +10:00
Deon George
7f6df8d032 Update test to use standard docker image now 2022-07-29 16:31:32 +10:00
Deon George
39ded93a42 Update checkout, enable editing of checkout, show details on invoices 2022-07-29 16:31:32 +10:00
Deon George
4f7a27dd8d Framework update 2022-07-26 00:29:07 +10:00
Deon George
cc906e9b22 Fixes for viewing unprocessed charges 2022-07-25 23:59:28 +10:00
Deon George
bb44c1a216 Hack to make services active when changed after ordering 2022-07-25 23:50:46 +10:00
Deon George
3ff7bf1571 Fixes for error 500 in order and fix layout when updating changed services 2022-07-25 23:37:31 +10:00
Deon George
5297ae8a62 Enable editing of supplier products and listing services connected to them 2022-06-30 23:51:20 +10:00
Deon George
fb416306e7 Optimising Supplier Layout and source code placement 2022-06-28 23:20:56 +10:00
Deon George
464407e7ee Update leenooks/laravel and framework updates 2022-06-28 21:57:55 +10:00
Deon George
3723d644e6 Framework update, and include nunomaduro/laravel-console-summary 2022-06-28 18:11:57 +10:00
Deon George
f6f502618d Log sent emails 2022-06-25 00:18:57 +10:00
Deon George
b0a317d709 Fixes to cost display 2022-06-14 17:23:14 +10:00
Deon George
8ba6a93214 Fixes for testing to pass 2022-06-14 17:03:58 +10:00
Deon George
768744ad27 Initial support for importing supplier costs 2022-06-14 16:55:17 +10:00
Deon George
606f357839 Improved search performance 2022-06-14 16:51:18 +10:00
Deon George
8777024cd8 Enable email test 2022-06-13 20:55:39 +10:00
Deon George
642446e592 Cast Invoice::reminders to json 2022-06-13 19:58:53 +10:00
Deon George
d1fbb4b42b Count negative payment_items towards payment balance 2022-06-13 18:01:29 +10:00
Deon George
5e82f091c0 Payments need to be active to be recognised 2022-06-13 17:31:11 +10:00
Deon George
28438c6423 Fix for ezypay payment import 2022-06-13 16:44:24 +10:00
Deon George
49a4830d89 Fix payment entry 2022-06-13 15:46:38 +10:00
Deon George
9e889008bf Fix recording charges 2022-06-13 14:36:12 +10:00
Deon George
2590997b1a Bring back some services logic to still needed by charges 2022-06-13 14:21:48 +10:00
Deon George
c1080481ec Enabled changing broadband services and adjusting invoices 2022-06-12 18:32:54 +10:00
Deon George
c8162b8eb9 Fixes to CI testing 2022-06-12 18:32:19 +10:00
Deon George
849fd8d56b Framework update 2022-06-12 11:23:06 +10:00
Deon George
cc94426902 Optimising product category and category names 2022-06-12 11:21:20 +10:00
Deon George
360c1e46a1 Updates to charges table 2022-06-11 17:43:09 +10:00
Deon George
b8f85960aa Updates to charges table 2022-06-11 17:39:25 +10:00
Deon George
b9ec64fd4f Updates for new mail enviroment for laravel 9 2022-05-12 14:34:14 +10:00
Deon George
69e5a5f12c Add site() to SiteID trait, so site models can be referenced (eg: in email) 2022-05-12 12:45:18 +10:00
Deon George
2d7437fc0d Fix some missing date_* attributes for Service that have been missed 2022-05-12 09:09:49 +10:00
Deon George
03f37f33ff Fix authorisation for resellers to only see their own accounts 2022-04-22 16:35:01 +10:00
Deon George
16cc0c9f8d Optimise product tables 2022-04-22 16:01:41 +10:00
Deon George
e1a4db700f Removed redundant functions from Invoice, optimised Invoice tables 2022-04-22 16:01:41 +10:00
Deon George
a16277d9bb Cleanup usage of Leenooks\Carbon, change ServiceType to be consistent with Products/ Suppliers/ 2022-04-22 15:25:14 +10:00
Deon George
8ed9e38290 Removed redundant functions from Service::class 2022-04-22 15:25:13 +10:00
Deon George
d53643ef55 Removed many redundant functions from User::class 2022-04-22 15:25:13 +10:00
Deon George
796c72dd09 Home screen improvements, testing for role, work on user/account models 2022-04-22 14:41:53 +10:00
Deon George
40d12b906b Upgrade framework to laravel 9 2022-04-22 14:41:53 +10:00
Deon George
3fb6c0a052 Fixes identified by CI testing 2022-04-20 17:05:49 +10:00
Deon George
16b7e0b493 Update and fix broadband traffic 2022-04-20 16:28:54 +10:00
Deon George
621a132e35 Start work on updating services 2022-04-20 12:43:10 +10:00
Deon George
ebf08ea414 Fix error 500 when cancelling and ordering broadband 2022-04-04 20:29:45 +10:00
Deon George
9659621ba0 Added hosting report and enabled updating hosting details 2022-04-02 20:26:59 +11:00
Deon George
edc06e51fb Enable editing num accounts for email 2022-04-02 18:16:02 +11:00
Deon George
a4ed29b560 Added in email hosting, and other misc cosmetic fixes 2022-04-02 18:06:34 +11:00
Deon George
7775105da6 Rename supplier home screen to home 2022-04-02 09:56:30 +11:00
Deon George
d7b5d9a272 Fix VOIP services from 500 errors 2022-03-05 13:49:50 +11:00
Deon George
62f587d7e6 Fix invoice generation and other minor cosmetic items 2022-03-05 11:05:35 +11:00
Deon George
6dab5bded8 Query performance improvements for home 2022-02-02 17:12:17 +11:00
Deon George
e9ada2468e Minor fixes to get charging working 2022-02-02 16:01:12 +11:00
Deon George
db0ded44e0 Framework updates 2022-02-02 12:07:27 +11:00
Deon George
d1a7e399dc Fixes for CI/CD 2022-02-02 10:56:28 +11:00
Deon George
b9b4416737 More works on products 2022-02-02 10:43:59 +11:00
Deon George
1e9f15b40f Work on products, first completed broadband 2022-02-02 10:43:59 +11:00
Deon George
8f5293662e Comments on Middleware/Role 2022-02-02 10:43:59 +11:00
Deon George
d0a56de07e DB rework id, site_id and relations 2022-02-02 10:43:59 +11:00
Deon George
bdcfe07fb0 Created Suppliers 2022-02-01 16:45:35 +11:00
Deon George
0aa7ff3b2c Update leenooks/laravel and now using @js @css shortcuts 2022-02-01 16:45:35 +11:00
Deon George
b7b6a575bc Site related updates 2022-02-01 16:45:35 +11:00
Deon George
8d194c5523 Add link to account from service view 2021-12-20 17:24:31 +11:00
Deon George
05c5d35dbf Ensure users can only login from the right site 2021-12-20 17:24:31 +11:00
Deon George
99a62828f5 Rework Site and top level tables 2021-12-20 17:24:31 +11:00
Deon George
b4e569ccc8 Change docker base image 2021-12-17 15:02:48 +11:00
Deon George
48a76bacb6 Fix invoice creation with charges. Fix including discounts in invoices 2021-12-16 11:55:59 +11:00
Deon George
8a8e299c7b Fix when VOIP order doesnt have an address or number 2021-12-14 13:14:14 +11:00
Deon George
7908676063 Fix error when new order created and notes is blank 2021-12-14 12:38:45 +11:00
Deon George
6fa88daeb0
Minor fix to SearchController, updated composer.json based on other projects 2021-10-08 12:34:56 +11:00
Deon George
17e7b47cdc
Add product info on broadband change requests 2021-10-08 12:34:56 +11:00
Deon George
cc49692545
Missed addition of 2 new js files in 7c536920 2021-10-01 15:10:47 +10:00
Deon George
1a92d89911
More autoincrement definition to after unique key creation 2021-10-01 15:04:59 +10:00
Deon George
7c5369203c
Optimise charge table, implemented charge recording, optimised payment recording 2021-10-01 14:59:04 +10:00
Deon George
c0ad46ba65
Show service detail on invoices 2021-09-30 12:54:44 +10:00
Deon George
5be4fe6784
Redirect Invoice Download to invoice view 2021-09-29 17:36:55 +10:00
Deon George
7acb9e964b
Enable users to execute workflows 2021-09-29 17:11:46 +10:00
Deon George
4243da9c32
Fix Model Policies from matching user_id's and account_id's, and other minor cosmetic fixes 2021-09-29 16:20:22 +10:00
Deon George
f7439172b6
Service cancellation and ordering 2021-09-29 14:57:25 +10:00
Deon George
b2e45fcaee
Added unique validation for broadband orders, add more visual display that order had errors 2021-09-28 15:14:43 +10:00
Deon George
eae1b16797
Error when accepting VOIP orders 2021-09-28 12:56:29 +10:00
Deon George
4cbe990ec1
Changes to orders to stop existing services being submitted 2021-09-28 12:43:54 +10:00
Deon George
7277d7407a
Framework updates and minor cosmetic fixes 2021-09-28 10:09:56 +10:00
Deon George
ccd6a11c8a
Missed PaymentsImport in previous commit 2021-07-29 13:18:39 +10:00
Deon George
00f215b780
Minor job cleanup, import Ezypay payments update 2021-07-29 13:11:14 +10:00
Deon George
10e6c73b2b
Missed some references to ab_payments in d463239 2021-07-23 17:49:59 +10:00
Deon George
7a963c8461
Fix test installing mariadb-client 2021-07-23 17:38:57 +10:00
Deon George
d463239b17
Rework payment tables, enable payment editing 2021-07-23 17:36:53 +10:00
Deon George
fa62a47680
Add VOIP to search 2021-07-19 16:42:37 +10:00
Deon George
f22813bb5b
Remove more references to ab_account, Docker build tweaks, Fix error rendering charges 2021-07-19 16:23:38 +10:00
Deon George
8b08f79877
Fix 500 error Service::isBilled 2021-07-13 17:07:47 +10:00
Deon George
9515e67493
Re-enable google auth 2021-07-13 15:00:01 +10:00
Deon George
53fc25612b
Fix domain name uniqueness during update, taking into account tld 2021-07-13 14:56:14 +10:00
Deon George
71d2faedb1
Try new schema dump to trim migrations - remove database testing from docker image 2021-07-13 13:31:30 +10:00
528 changed files with 25558 additions and 15676 deletions

View File

@ -1,32 +1,37 @@
APP_ADMIN=
APP_DEBUG=false
APP_NAME=OSB
APP_NAME_HTML_LONG="<b>Graytech</b>Hosting"
APP_NAME_HTML_SHORT="<b>G</b>H"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_TIMEZONE=Australia/Melbourne
APP_URL=https://www.graytech.net.au
LOG_CHANNEL=stack
AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets
DB_CONNECTION=mysql
DB_HOST=database
DB_PORT=3306
DB_DATABASE=database
DB_USERNAME=homestead
DB_PASSWORD=secret
LOG_CHANNEL=daily
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=graytech
DB_USERNAME=graytech
DB_PASSWORD=
DB_SCHEMA=billing
BROADCAST_DRIVER=log
CACHE_DRIVER=file
CACHE_STORE=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_MAILER=smtp
MAIL_HOST=smtp
MAIL_PORT=25
MAIL_USERNAME=null
MAIL_PASSWORD=null
@ -43,13 +48,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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CollectionOrNull implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
{
return collect(json_decode($value, true));
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return count($value) ? json_encode($value) : NULL;
}
}

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

@ -3,9 +3,10 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Jobs\BroadbandTraffic as Job;
use App\Models\AdslSupplier;
use App\Models\Supplier;
class BroadbandTraffic extends Command
{
@ -14,7 +15,8 @@ class BroadbandTraffic extends Command
*
* @var string
*/
protected $signature = 'broadband:traffic:import';
protected $signature = 'broadband:traffic:import'.
' {supplier? : Supplier Name}';
/**
* The console command description.
@ -30,7 +32,26 @@ class BroadbandTraffic extends Command
*/
public function handle()
{
foreach (AdslSupplier::active()->get() as $o)
Job::dispatch($o);
if ($this->argument('supplier')) {
try {
$o = Supplier::active()
->where('name','ilike',$this->argument('supplier'))
->sole();
} catch (ModelNotFoundException $e) {
$this->error(sprintf('Supplier [%s] not found',$this->argument('supplier')));
return self::FAILURE;
}
Job::dispatchSync($o->name);
return self::SUCCESS;
}
foreach (Supplier::active()->get() as $o)
Job::dispatchSync($o->name);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use App\Mail\Test;
use App\Models\{Site,User};
class EmailTest extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test-email'
.' {--s|site : Site ID}'
.' {id : User ID}'
.' {email? : Alternative Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Config::set(
'site',
$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole()
);
$uo = User::find($this->argument('id'));
$result = Mail::to($this->argument('email') ?? $uo->email)
->send(new Test($uo));
$this->info($result->getMessageId());
return self::SUCCESS;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Console\Commands\Ezypay;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay;
use App\Models\Account;
class PaymentNext extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ezypay:payment:next';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Load next payments, and ensure they cover the next invoice';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$poo = new Ezypay;
foreach ($poo->getCustomers() as $c) {
if ($c->BillingStatus == 'Inactive') {
$this->comment(sprintf('Ignoring INACTIVE: [%s] %s %s',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname));
continue;
}
// Load Account Details from ReferenceId
$ao = Account::find((int)substr($c->ReferenceId,2,4));
if (! $ao) {
$this->warn(sprintf('Missing: [%s] %s %s (%s)',$c->EzypayReferenceNumber,$c->Firstname,$c->Surname,$c->ReferenceId));
continue;
}
// Get Due Invoices
$invoice_due = $ao->invoiceSummaryDue()->get();
$this->info(sprintf('Account [%s] (%s) has [%d] invoices due, totalling [%3.2f]',
$ao->lid,
$ao->name,
$invoice_due->count(),
($due=$invoice_due->sum('_balance')),
));
$next_pay = $poo->getDebits([
'customerId'=>$c->Id,
'dateFrom'=>now()->format('Y-m-d'),
'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 < $due)
$this->error(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($due,2)));
elseif ($next_pay->Amount > $due)
$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($due,2)));
else
$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($due,2)));
}
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Console\Commands\Ezypay;
use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay;
use App\Jobs\PaymentsImport as Job;
class PaymentsImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ezypay:payment:import';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Retrieve payments from Ezypay';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Job::dispatchSync(new Ezypay);
}
}

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

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Exceptions\NotTokenException;
use Intuit\Jobs\AccountingCustomerUpdate;
use Intuit\Models\Customer as AccAccount;
use Intuit\Traits\ProviderTokenTrait;
use App\Models\Account;
class AccountAdd extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:account:add'
.' {id : Account ID}'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add an account to quickbooks';
/**
* Execute the console command.
*
* @return int
* @throws NotTokenException
*/
public function handle()
{
$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(
$this->providerToken($this->argument('user')),
$acc
);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Exceptions\NotTokenException;
use Intuit\Traits\ProviderTokenTrait;
use App\Jobs\AccountingAccountSync;
/**
* Synchronise Customers with Accounts
*/
class AccountSync extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:account:sync'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise accounts with quickbooks';
/**
* Execute the console command.
*
* @return int
* @throws NotTokenException
*/
public function handle()
{
AccountingAccountSync::dispatchSync($this->providerToken($this->argument('user')));
return self::SUCCESS;
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Jobs\AccountingInvoiceUpdate;
use Intuit\Models\Invoice as AccInvoice;
use Intuit\Traits\ProviderTokenTrait;
use App\Models\Invoice;
class InvoiceAdd extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:invoice:add'
.' {id : Invoice ID}'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add an invoice to the accounting provider';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle()
{
$to = $this->providerToken($this->argument('user'));
$io = Invoice::findOrFail($this->argument('id'));
// Check the customer exists
if ($io->account->providers->where('pivot.provider_oauth_id',$to->provider->id)->count() !== 1)
throw new \Exception(sprintf('Account [%d] for Invoice [%d] not defined',$io->account_id,$io->id));
$ao = $io->account->providers->where('pivot.provider_oauth_id',$to->provider->id)->pop();
// Some validation
if (! $ao->pivot->ref) {
$this->error(sprintf('Accounting not defined for account [%d]',$io->account_id));
return self::FAILURE;
}
$acc = new AccInvoice;
$acc->CustomerRef = (object)['value'=>$ao->pivot->ref];
$acc->DocNumber = $io->lid;
$acc->TxnDate = $io->created_at->format('Y-m-d');
$acc->DueDate = $io->due_at->format('Y-m-d');
$lines = collect();
$c = 0;
$subtotal = 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 ($io->items->groupBy(
fn($item)=>
sprintf('%s.%s.%s.%s',
$item->item_type_name,
$item->price_base,
$item->product?->provider_ref($to->provider),
$item->taxes->pluck('description')->join('|'))) as $os)
{
$key = $os->first();
// Some validation
if (! ($ref=$key->product?->provider_ref($to->provider))) {
$this->error(sprintf('Accounting not defined in product [%d]',$key->product_id));
return self::FAILURE;
}
if ($key->taxes->count() !== 1) {
$this->error(sprintf('Cannot handle when there is not just 1 tax line [%d]',$key->id));
return self::FAILURE;
}
$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'=>$tcf=$key->taxes->first()->tax->provider_ref($to->provider)],
];
$line->Amount = round($os->sum('quantity')*$key->price_base,2);
$subtotal += $line->Amount;
$lines->push($line);
}
$acc->Line = $lines;
// If our subtotal doesnt match, we need to add a tax line
if ($io->subtotal !== $subtotal) {
$acc->TxnTaxDetail = (object)[
'TotalTax' => $x=$io->total-$subtotal,
'TaxLine' => [
(object) [
'Amount' => $x,
'DetailType' => 'TaxLineDetail',
'TaxLineDetail' => (object)[
// @todo It is assumed there is only 1 tax category
'TaxRateRef' => (object)['value'=>$to->API()->getTaxCodeQuery($tcf)->getTaxRateRef()->first()],
'NetAmountTaxable' => $io->subtotal,
]
]
]
];
}
return AccountingInvoiceUpdate::dispatchSync($to,$acc);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Traits\ProviderTokenTrait;
use App\Jobs\AccountingInvoiceSync as Job;
class InvoiceSync extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:invoice:sync'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise invoices with accounting system';
/**
* Execute the console command.
*
* @return int
* @throws \Intuit\Exceptions\NotTokenException
*/
public function handle()
{
Job::dispatchSync($this->providerToken($this->argument('user')));
return self::SUCCESS;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Exceptions\NotTokenException;
use Intuit\Traits\ProviderTokenTrait;
use App\Models\Product;
/**
* Return a list of products and their accounting id
*/
class ItemList extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'accounting:item:list'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise items with accounting system';
/**
* Execute the console command.
*
* @return int
* @throws NotTokenException
*/
public function handle()
{
$to = $this->providerToken($this->argument('user'));
// Current Products used by services
$products = Product::select(['products.*'])
->distinct('products.id')
->join('services',['services.product_id'=>'products.id'])
->where('services.active',TRUE)
->get();
foreach ($products as $po) {
if (! $x=$po->provider_ref($to->provider))
$this->error(sprintf('Product [%03d](%s) doesnt have accounting set',$po->id,$po->name));
else
$this->info(sprintf('Product [%03d](%s) set to accounting [%s]',$po->id,$po->name,$x));
}
return self::SUCCESS;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Traits\ProviderTokenTrait;
use App\Jobs\AccountingPaymentSync as Job;
class PaymentSync extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:payment:sync'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise payments with accounting system';
/**
* Execute the console command.
*
* @return int
* @throws \Intuit\Exceptions\NotTokenException
*/
public function handle()
{
$to = $this->providerToken($this->argument('user'));
foreach ($to->API()->getPayments() as $acc)
Job::dispatchSync($to,$acc);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands\Intuit;
use Illuminate\Console\Command;
use Intuit\Traits\ProviderTokenTrait;
use App\Jobs\AccountingTaxSync as Job;
/**
* Synchronise TAX ids with our taxes.
*/
class TaxSync extends Command
{
use ProviderTokenTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'intuit:tax:sync'
.' {user? : User Email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise taxes with accounting system';
/**
* Execute the console command.
*
* @return int
* @throws \Intuit\Exceptions\NotTokenException
*/
public function handle()
{
Job::dispatchSync($this->providerToken($this->argument('user')));
return self::SUCCESS;
}
}

View File

@ -2,10 +2,10 @@
namespace App\Console\Commands;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Config;
use Illuminate\Console\Command;
use App\Models\Invoice;
use App\Models\{Invoice,Site};
class InvoiceEmail extends Command
{
@ -14,7 +14,9 @@ class InvoiceEmail extends Command
*
* @var string
*/
protected $signature = 'invoice:email {id}';
protected $signature = 'invoice:email'
.' {--s|site : Site ID}'
.' {id?}';
/**
* The console command description.
@ -23,16 +25,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 +32,23 @@ class InvoiceEmail extends Command
*/
public function handle()
{
Config::set(
'site',
$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole()
);
$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 {
$o->print_status = TRUE;
$o->reminders = $o->reminders('send');
try {
$o->send();
$o->save();
} catch (\Exception $e) {
dd($e);
}
return self::SUCCESS;
}
}

View File

@ -2,10 +2,11 @@
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 +15,11 @@ class InvoiceGenerate extends Command
*
* @var string
*/
protected $signature = 'invoice:generate {account?} {--p|preview : Preview} {--l|list : List Items}';
protected $signature = 'invoice:generate'
.' {--l|list : List Items}'
.' {--p|preview : Preview}'
.' {--s|site : Site ID}'
.' {id?}';
/**
* The console command description.
@ -23,16 +28,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,35 +35,50 @@ class InvoiceGenerate extends Command
*/
public function handle()
{
if ($this->argument('account'))
$accounts = collect()->push(Account::find($this->argument('account')));
Config::set(
'site',
$this->option('site')
? Site::findOrFail($this->option('site'))
: Site::where('url',config('app.url'))->sole()
);
if ($this->argument('id'))
$accounts = collect()->push(Account::find($this->argument('id')));
else
$accounts = Account::active()->get();
foreach ($accounts as $o) {
$items = $o->invoice_next(Carbon::now());
if (! $items->count()) {
$this->warn(sprintf('No items for account (%s) [%d]',$o->name,$o->id));
continue;
}
$this->info(sprintf('Account: %s [%d]',$o->name,$o->lid));
$io = new Invoice;
$io->account_id = $o->id;
foreach ($o->services(TRUE)->get() as $so) {
foreach ($so->next_invoice_items(FALSE) as $ooo)
$io->items->push($ooo);
}
foreach ($items as $oo)
$io->items_active->push($oo);
// If there are no items, no reason to do anything
if (! $io->items->count() OR $io->total < 0)
if ($io->total < 0) {
$this->warn(sprintf(' - Invoice totals [%3.2f] - skipping',$io->total));
continue;
}
$io->account_id = $o->id;
if ($this->option('list')) {
$this->warn(sprintf('|%4s|%4s|%-50s|%8s|',
$this->line(sprintf('|%4s|%4s|%-50s|%8s|',
'SID',
'PID',
'Name',
'Amount',
));
foreach ($io->items as $oo) {
foreach ($io->items_active as $oo) {
$this->info(sprintf('|%4s|%4s|%-50s|%8.2f|',
$oo->service_id,
$oo->product_id,
@ -78,8 +88,9 @@ class InvoiceGenerate extends Command
}
}
//dump($io);
if ($this->option('preview')) {
$this->info(sprintf('Invoice for Account [%d] - [%d] items totalling [%3.2f]',$o->id,$io->items->count(),$io->total));
$this->info(sprintf('=> Invoice for Account [%d] - [%d] items totalling [%3.2f]',$o->id,$io->items_active->count(),$io->total));
continue;
}
@ -89,5 +100,7 @@ class InvoiceGenerate extends Command
$io->pushNew();
}
return self::SUCCESS;
}
}

View File

@ -43,8 +43,8 @@ class OrderSend extends Command
{
$so = Service::findOrFail($this->argument('service'));
// @todo TO get from DB
Mail::to('help@graytech.net.au')->sendNow(new OrderRequestApprove($so));
Mail::to(config('osb.ticket_admin'))
->sendNow(new OrderRequestApprove($so));
if (Mail::failures()) {
dump('Failure?');

View File

@ -1,107 +0,0 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay;
use App\Models\{Account,Checkout,Payment};
class PaymentsEzypayImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payments:ezypay:import';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Retrieve payments from Ezypay';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
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));
}
}
}
}
}

View File

@ -1,81 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Classes\External\Payments\Ezypay;
use App\Models\Account;
class PaymentsEzypayNext extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'payments:ezypay:next';
/**
* The console command description.
*
* @var string
*/
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.
*
* @return mixed
*/
public function handle()
{
$poo = new Ezypay();
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;
}
// Get Due Invoices
$account_due = $ao->dueInvoices()->sum('due');
$next_pay = $poo->getDebits([
'customerId'=>$c->Id,
'dateFrom'=>now()->format('Y-m-d'),
'dateTo'=>now()->addQuarter()->format('Y-m-d'),
])->reverse()->first();
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)));
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)));
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)));
}
}
}

View File

@ -0,0 +1,46 @@
<?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'
.' {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()
{
$uo = User::where('email',$this->argument('user') ?: config('osb.admin'))->singleOrFail();
$so = ProviderOauth::where('name',$this->argument('provider'))->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());
return self::SUCCESS;
}
}

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,8 +3,6 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\Service;
@ -15,10 +13,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 +25,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 +32,51 @@ class ServiceList extends Command
*/
public function handle()
{
DB::listen(function($query) {
Log::debug('- SQL',['sql'=>$query->sql,'binding'=>$query->bindings]);
});
$header = '|%5s|%-9s|%-30s|%-30s|%7s|%7s|%10s|%10s|%10s|%10s|%10s|';
$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',
'Start',
'Stop',
'Connect',
'First',
'Next',
));
foreach (Service::all() as $o) {
if ($this->option('active') AND ! $o->isActive())
foreach (Service::cursor() as $o) {
if ((! $this->option('inactive')) && (! $o->isActive()))
continue;
if ($this->option('category') AND $o->product->category !== $this->option('category'))
if ($this->option('type') && ($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->invoiced_items
->filter(fn($item)=>$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') && (! $o->start_at) && $c && $c->start_at && $o->type && $o->type->connect_at && $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|',
$o->sid,
$o->product->category,
$o->product_name,
$o->name_short,
$this->info(sprintf($header,
$o->lid,
$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->start_at?->format('Y-m-d'),
$o->stop_at?->format('Y-m-d'),
($o->type && $o->type->connect_at) ? $o->type->connect_at->format('Y-m-d') : NULL,
($c && $c->start_at) ? $c->start_at->format('Y-m-d') : NULL,
$o->invoice_next?->format('Y-m-d'),
));
}
}

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

@ -1,49 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use App\Mail\TestEmail as MailTest;
use App\Models\User;
class TestEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:email {id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a test email';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$uo = User::find($this->argument('id'));
Mail::to($uo->email)
->send(new MailTest($uo));
}
}

View File

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

View File

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

View File

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

View File

@ -2,138 +2,59 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Http\RedirectResponse;
use App\Models\{Account,Payment,PaymentItem,Service,SiteDetail};
use App\Http\Requests\SiteEdit;
use App\Models\SiteDetail;
/**
* The AdminController governs all routes that are prefixed with 'a/'.
*
* This is everything about the configuration of the application as a whole, or administration of a site.
*/
class AdminController extends Controller
{
public function service(Service $o)
{
return View('a.service',['o'=>$o]);
}
/**
* Record payments on an account.
*
* @param Request $request
* @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)
{
if ($request->post()) {
$validation = $request->validate([
'account_id' => 'required|exists:ab_account,id',
'date_payment' => 'required|date',
'checkout_id' => 'required|exists:ab_checkout,id',
'total_amt' => 'required|numeric|min:0.01',
'fees_amt' => 'nullable|numeric|lt:total_amt',
'source_id' => 'nullable|exists:ab_account,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'))
$fail('Allocation is greater than payment total.');
}],
'invoices.*.id' => 'nullable|exists:ab_invoice,id',
]);
$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();
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);
}
return redirect()->back()
->with('success','Payment recorded');
}
return view('a.payment.add')
->with('o',$o);
}
/**
* Show a list of invoices to apply payments to
*
* @param Account $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function pay_invoices(Account $o)
{
return view('a.payment.widgets.invoices')
->with('o',$o);
}
/**
* Site setup
*
* @note This method is protected by the routes
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
* @param SiteEdit $request
* @return RedirectResponse
*/
public function setup(Request $request)
public function setup(SiteEdit $request)
{
if ($request->post()) {
$validated = $request->validate([
'site_name' => 'required|string|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_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',
'site_tax' => 'required|regex:/[0-9 ]+/|size:14',
'social' => 'nullable|array',
'top_menu' => 'nullable|array',
'site_logo' => 'nullable|image',
'email_logo' => 'nullable|image',
]);
$site = config('site');
$images = ['site_logo','email_logo'];
$validated = collect($request->validated());
$site = config('SITE');
// Handle the images
foreach($images as $key)
if ($x=$request->validated($key))
$validated->put($key,$x->storeAs('site/'.$site->site_id,$x->getClientOriginalName(),'public'));
// @todo - not currently rendered in the home page
$validated['social'] = [];
$validated['top_menu'] = [];
foreach ($site->details as $oo)
if ($validated->has($oo->key)) {
// Dont set the following keys to null if they are null
if (in_array($oo->key,$images) && is_null($validated->get($oo->key)))
continue;
// Handle the images
foreach(['site_logo','email_logo'] as $key)
if (array_key_exists($key,$validated))
$validated[$key] = ($x=$validated[$key])->storeAs('site/'.$site->site_id,$x->getClientOriginalName(),'public');
$oo->value = $validated->get($oo->key) ?: '';
$oo->save();
foreach ($site->details as $oo)
if (array_key_exists($oo->key,$validated)) {
$oo->value = Arr::get($validated,$oo->key);
$oo->save();
unset($validated[$oo->key]);
}
// Left over values to be created.
foreach ($validated as $k=>$v) {
$oo = new SiteDetail;
$oo->key = $k;
$oo->value = $v ?: '';
$site->details()->save($oo);
$validated->forget($oo->key);
}
return redirect()->back()
->with('success','Settings saved');
// Left over values to be created.
foreach ($validated as $k=>$v) {
$oo = new SiteDetail;
$oo->key = $k;
$oo->value = $v ?: '';
$site->details()->save($oo);
}
return view('a.setup');
return redirect()
->back()
->with('success','Settings saved');
}
}

View File

@ -4,46 +4,29 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
use SendsPasswordResetEmails;
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\View\View
*/
public function showLinkRequestForm()
{
return view('adminlte::auth.passwords.email');
}
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// If the account is not active, or doesnt exist, we'll send a fake "sent" message.
if (! ($x=$this->broker()->getUser($this->credentials($request))) || (! $x->active))
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$this->credentials($request)
);
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}
}

View File

@ -3,8 +3,6 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -12,35 +10,36 @@ use Illuminate\Support\Facades\Schema;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth')
->only('logout');
}
public function login(Request $request)
{
@ -73,6 +72,7 @@ class LoginController extends Controller
if (file_exists('login_note.txt'))
$login_note = file_get_contents('login_note.txt');
return view('adminlte::auth.login')->with('login_note',$login_note);
return view('adminlte::auth.login')
->with('login_note',$login_note);
}
}

View File

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

View File

@ -3,46 +3,47 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
use ResetsPasswords;
/**
* Create a new controller instance.
* Where to redirect users after resetting their password.
*
* @return void
* @var string
*/
public function __construct()
{
$this->middleware('guest');
}
protected $redirectTo = '/home';
public function showResetForm(Request $request, $token = null)
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showResetForm(Request $request)
{
return view('adminlte::auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
$token = $request->route()->parameter('token');
return view('adminlte::auth.passwords.reset')
->with([
'token' => $token,
'email' => $request->email
]);
}
}
}

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\Auth;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
@ -10,86 +12,120 @@ 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\Providers\RouteServiceProvider;
use App\Models\{ProviderOauth,ProviderToken,User,UserOauth};
class SocialLoginController extends Controller
{
public function redirectToProvider($provider)
{
return Socialite::with($provider)->redirect();
return Socialite::with($provider)
->redirect();
}
public function handleProviderCallback($provider)
{
$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) {
if ($aoo->count() === 1) {
$aoo = $aoo->first();
if ((is_null($user=$aoo->user) AND (is_null($aoo->account) OR is_null($user=$aoo->account->user))) OR ! $user->active) {
if (! $user) {
if ((is_null($user=$aoo->user) && (is_null($aoo->account) || is_null($user=$aoo->account->user))) || ! $user->active) {
if (! $user)
$user = User::where('email',$openiduser->email)->first();
}
if (! $user OR ! $user->active) {
return redirect('/login')->with('error','Invalid account, or account inactive, please contact an admin.');
}
if ((! $user) || (! $user->active))
return redirect('/login')
->with('error','Invalid account, or account inactive, please contact an admin.');
return $this->link($provider,$aoo,$user);
}
// All Set to login
Auth::login($user,FALSE);
Auth::login($user);
// If there are too many users, then we have a problem
} elseif ($aoo->count() > 1) {
return redirect('/login')->with('error','Seems you have multiple oauth IDs, please contact an admin.');
return redirect('/login')
->with('error','Seems you have multiple oauth IDs, please contact an admin.');
// User is using OAUTH for the first time.
} else {
$uo = User::active()->where('email',$openiduser->email);
// See if their is an account with this email address
if ($uo->count() == 1) {
$aoo = new AccountOauth;
if ($uo->count() === 1) {
$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());
// If there are too many users, then we have a problem
} elseif ($uo->count() > 1) {
return redirect('/login')->with('error','Seems you have multiple accounts, please contact an admin.');
return redirect('/login')
->with('error','Seems you have multiple accounts, please contact an admin.');
} else {
return redirect('/login')->with('error','Seems you dont have an account with that email, please contact an admin.');
return redirect('/login')
->with('error','Seems you dont have an account with that email, please contact an admin.');
}
}
return redirect()->intended(RouteServiceProvider::HOME);
return redirect()
->intended('/home');
}
public function handleBearerTokenCallback($provider)
{
$openiduser = Socialite::with($provider)->user();
if (! $openiduser)
return redirect('/home')
->with('error','No user details obtained.');
$po = ProviderOauth::where('name',$provider)->singleOrFail();
$uoo = ProviderToken::where('user_id',Auth::id())->where('provider_oauth_id',$po->id)->firstOrNew();
$uoo->user_id = Auth::id();
$uoo->access_token = $openiduser->token;
$uoo->access_token_expires_at = Carbon::now()->addSeconds($openiduser->expiresIn);
$uoo->refresh_token = $openiduser->refreshToken;
$uoo->refresh_token_expires_at = Carbon::now()->addSeconds($openiduser->refresh_token_expires_in);
$uoo->realm_id = $openiduser->realmid;
$po->tokens()->save($uoo);
return redirect()
->intended('/home')
->with('success','Token refreshed.');
}
/**
* We have identified the user and oauth, just need them to confirm the link
*
* @param $provider
* @param $provider
* @param UserOauth $ao
* @param User $uo
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return View
*/
public function link($provider,AccountOauth $ao,User $uo)
public function link($provider,UserOauth $ao,User $uo): View
{
Mail::to($uo->email)->send(new SocialLink($ao));
// @note If this is sent now (send()), it results in the caller to be executed a second time (handleProviderCallback()).
Mail::to($uo->email)->queue(new SocialLink($ao));
return view('auth.social_link')
return view('theme.backend.adminlte.auth.social_link')
->with('oauthid',$ao->id)
->with('provider',$provider);
}
@ -97,23 +133,29 @@ 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'))
return redirect('/login')->with('error','Account details didnt match to make link.');
return redirect('/login')
->with('error','Account details didnt match to make link.');
// Check our token matches
if ($aoo->link_token !== $request->post('token'))
return redirect('/login')->with('error','Token details didnt match to make link.');
return redirect('/login')
->with('error','Token details didnt match to make link.');
// Load our email.
$uo = User::where('email',$request->post('email'))->firstOrFail();
// Incase we have an existing record with a different oauthid
UserOauth::where('user_id',$uo->id)->delete();
$aoo->user_id = $uo->id;
$aoo->save();
Auth::login($uo,FALSE);
Auth::login($uo);
return redirect()->intended(RouteServiceProvider::HOME);
return redirect()
->intended('/home');
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ChargeAdd;
use App\Models\Charge;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
use Illuminate\View\View;
class ChargeController extends Controller
{
/**
* Add a charge to a service/account
*
* @param ChargeAdd $request
* @return RedirectResponse
*/
public function addedit(ChargeAdd $request): RedirectResponse
{
$o = Charge::findOrNew(Arr::get($request->validated(),'id'));
// Dont update processed charges
if ($o->processed)
abort(403);
$o->forceFill(array_merge(Arr::except($request->validated(),['id']),['active'=>TRUE]));
$o->save();
return redirect()
->back()
->with('success',sprintf('Charge %s #%d',$o->wasRecentlyCreated ? 'Created' : 'Updated',$o->id));
}
public function delete(Charge $o): array
{
if (Gate::allows('delete',$o)) {
$o->active = FALSE;
$o->save();
return ['ok'];
} else {
abort(401,'Not Allowed');
}
}
/**
* Add a charge to a service/account
*
* @param Request $request
* @return View
*/
public function edit(Request $request): View
{
$o = Charge::where('processed',FALSE)
->where('id',$request->id)
->firstOrFail();
if (Gate::allows('update',$o)) {
return view('theme.backend.adminlte.charge.widget.addedit')
->with('o',$o)
->with('so',$o->service);
}
abort(403);
}
}

View File

@ -2,33 +2,93 @@
namespace App\Http\Controllers;
use App\Models\Invoice;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use App\Models\Checkout;
use App\Http\Requests\CheckoutAddEdit;
use App\Models\{Checkout,Invoice};
class CheckoutController extends Controller
{
public function cart_invoice(Request $request,Invoice $o=NULL)
/**
* Update a checkout details
*
* @param CheckoutAddEdit $request
* @param Checkout $o
* @return RedirectResponse
*/
public function addedit(CheckoutAddEdit $request,Checkout $o): RedirectResponse
{
if ($o) {
$request->session()->put('invoice.cart.'.$o->id,$o->id);
foreach ($request->validated() as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->validated('active',FALSE);
try {
$o->save();
} catch (\Exception $e) {
return redirect()
->back()
->withErrors($e->getMessage())->withInput();
}
if (! $request->session()->get('invoice.cart'))
return redirect()->to('u/home');
return View('u.invoice.cart')
->with('invoices',Invoice::find(array_values($request->session()->get('invoice.cart'))));
return $o->wasRecentlyCreated
? redirect()
->to('a/checkout/'.$o->id)
->with('success','Checkout added')
: redirect()
->back()
->with('success','Checkout saved');
}
public function fee(Request $request,Checkout $o): float
/**
* Add an invoice to the cart
*
* @param Request $request
* @param Invoice $o
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application
* @note The route validates that the user can see the invoice
*/
public function cart_invoice(Request $request,Invoice $o)
{
return $o->fee($request->post('total',0));
$request->session()->put('invoice.cart.'.$o->id,$o->id);
return view('theme.backend.adminlte.checkout.cart');
}
public function pay(Request $request,Checkout $o)
/**
* Remove an item from the cart
*
* @param Request $request
* @return string
*/
public function cart_remove(Request $request): string
{
return redirect('pay/paypal/authorise');
if ($id=$request->post('id')) {
$cart = $request->session()->pull('invoice.cart');
unset($cart[$id]);
$request->session()->put('invoice.cart',$cart);
}
return '';
}
public function fee(Request $request): float
{
if ((! $request->post('checkout_id') || (! $request->post('total'))))
return 0;
$co = Checkout::findOrFail($request->post('checkout_id'));
return $co->fee($request->post('total'));
}
public function pay()
{
// @todo Currently sending all payments to paypal
return redirect()
->action([PaypalController::class,'authorise']);
}
}

View File

@ -2,12 +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\Validation\ValidatesRequests;
class Controller extends BaseController
abstract class Controller extends \Illuminate\Routing\Controller
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
use AuthorizesRequests, ValidatesRequests;
}

View File

@ -4,11 +4,9 @@ namespace App\Http\Controllers;
use Clarkeash\Doorman\Exceptions\{DoormanException,ExpiredInviteCode};
use Clarkeash\Doorman\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,19 @@ 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('theme.backend.adminlte.home')
->with(['o'=>$o]);
}
/**
@ -101,18 +58,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 +68,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,71 @@
<?php
namespace App\Http\Controllers;
use Clarkeash\Doorman\Exceptions\{ExpiredInviteCode,InvalidInviteCode,NotYourInviteCode};
use Clarkeash\Doorman\Facades\Doorman;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Barryvdh\Snappy\Facades\SnappyPdf as PDF;
use App\Models\{Account,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
{
/**
* Show a list of invoices to apply payments to
*
* @param Request $request
* @return \Illuminate\Contracts\View\View
*/
public function api_account_invoices(Request $request): \Illuminate\Contracts\View\View
{
session()->flashInput($request->post('old',[]));
return view('theme.backend.adminlte.payment.widget.invoices')
->with('pid',$request->post('pid'))
->with('o',Account::findOrFail($request->post('aid')));
}
/**
* Return the invoice in PDF format, ready to download
*
* @param Invoice $o
* @return mixed
*/
public function pdf(Invoice $o)
{
return PDF::loadView('theme.backend.adminlte.u.invoice.home',['o'=>$o])
->stream(sprintf('%s.pdf',$o->sid));
}
/**
* Render a specific invoice for the user
*
* @param Invoice $o
* @param string|null $code
* @return View
*/
public function view(Invoice $o,string $code=NULL): View
{
if ($code) {
try {
Doorman::redeem($code,$o->account->user->email);
} catch (ExpiredInviteCode|InvalidInviteCode|NotYourInviteCode $e) {
abort(404);
}
}
return view('theme.backend.adminlte.invoice.view')
->with('o',$o);
}
}

View File

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

View File

@ -3,63 +3,49 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Igaster\LaravelTheme\Facades\Theme;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Database\Eloquent\Model;
use App\Mail\OrderRequest;
use App\Models\{Account,Product,Service,User};
use App\Models\{Account,Product,Rtm,Service,User};
class OrderController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
return view('order');
}
public function product_order(Product $o)
{
Theme::set('metronic-fe');
return view('widgets.product_order',['o'=>$o]);
}
public function product_info(Product $o)
{
Theme::set('metronic-fe');
return view('widgets.product_description',['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);
})
->sometimes(
'account_id',
'required|email',
fn($input)=>is_null($input->account_id) && is_null($input->order_email_manual)
)
// Un-Authed User
->sometimes('order_email_manual','required|email|unique:users,email,NULL,id',function($input) use ($request) {
return (is_null($input->order_email_manual) AND ! isset($input->account_id)) OR $input->order_email_manual;
})
->sometimes(
'order_email_manual',
'required|email|unique:users,email,NULL,id',
fn($input)=>(is_null($input->order_email_manual) && (! isset($input->account_id))) || $input->order_email_manual
)
// Authed User
->sometimes('account_id','required|email',function($input) use ($request) {
return is_null($input->account_id) AND ! isset($input->order_email_manual);
})
->sometimes(
'account_id',
'required|email',
fn($input)=>is_null($input->account_id) && (! isset($input->order_email_manual))
)
->validate();
// Check the plugin details.
$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 +53,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();
}
@ -81,12 +68,9 @@ class OrderController extends Controller
// If we have a new account.
if (is_null($request->input('account_id'))) {
$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 +81,33 @@ 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.
$ro = Rtm::where('parent_id',NULL)->sole();
return view('order_received',['o'=>$so]);
Mail::to(config('osb.ticket_admin'))
->queue((new OrderRequest($so,$request->input('options.notes') ?: ''))->onQueue('email')); //@todo Get email from DB.
return view('theme.frontend.metronic.order_received')
->with('o',$so);
}
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
use App\Http\Requests\PaymentAddEdit;
use App\Models\{Payment,PaymentItem};
class PaymentController extends Controller
{
/**
* Record payments on an account.
*
* @param PaymentAddEdit $request
* @param Payment $o
* @return RedirectResponse
*/
public function addedit(PaymentAddEdit $request,Payment $o): RedirectResponse
{
foreach (Arr::except($request->validated(),'invoices') as $k=>$v)
$o->{$k} = $v;
foreach ($request->validated('invoices',[]) as $id => $amount) {
// See if we already have a payment item that we need to update
$items = $o->items
->filter(fn($item)=>$item->invoice_id == $id);
if ($items->count() === 1) {
$oo = $items->pop();
if ($amount == 0) {
$oo->delete();
continue;
}
} else {
$oo = new PaymentItem;
$oo->invoice_id = $id;
}
$oo->amount = ($oo->invoice->due >= 0) && ($oo->invoice->due-$amount >= 0)
? $amount
: 0;
// If the amount is empty, ignore it.
if (! $oo->amount)
continue;
$oo->site_id = config('site')->site_id;
$oo->active = TRUE;
$o->items->push($oo);
}
try {
$o->pushNew();
} catch (\Exception $e) {
return redirect()
->back()
->withErrors($e->getMessage())->withInput();
}
return $o->wasRecentlyCreated
? redirect()
->to('r/payment/'.$o->id)
->with('success','Payment added')
: redirect()
->back()
->with('success','Payment saved');
}
}

View File

@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Models\PaymentItem;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use PayPalCheckoutSdk\Core\PayPalHttpClient;
@ -13,14 +13,13 @@ use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
use PayPalHttp\HttpException;
use App\Models\Checkout;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\{Checkout,Invoice,Payment,PaymentItem};
class PaypalController extends Controller
{
private $client;
private $o = NULL;
private PayPalHttpClient $client;
protected const cart_url = 'u/checkout/cart';
// Create a new instance with our paypal credentials
public function __construct()
@ -31,27 +30,30 @@ class PaypalController extends Controller
$environment = new ProductionEnvironment(config('paypal.live_client_id'),config('paypal.live_secret'));
$this->client = new PayPalHttpClient($environment);
$this->o = Checkout::where('name','paypal')->firstOrFail();
}
public function cancel(Request $request)
public function cancel()
{
return redirect()->to('u/invoice/cart');
return redirect()
->to(self::cart_url);
}
/**
* Authorize a paypal payment, and redirect the user to pay.
*
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
* @return RedirectResponse
* @throws \PayPalHttp\IOException
*/
public function authorise(Request $request)
public function authorise()
{
$co = Checkout::where('name','ilike','paypal')->firstOrFail();
$currency = 'AUD'; // @todo TO determine from DB.;
$cart = $request->session()->get('invoice.cart');
$cart = request()->session()->get('invoice.cart');
if (! $cart)
return redirect()->to('u/home');
return redirect()
->to('u/home');
$invoices = Invoice::find($cart);
@ -61,7 +63,7 @@ class PaypalController extends Controller
// Paypal Purchase Units
$items = collect();
foreach ($invoices as $io) {
$fee = $this->o->fee($io->due,count($cart));
$fee = $co->fee($io->due,count($cart));
$total = round($io->due+$fee,2);
$items->push([
@ -100,7 +102,7 @@ class PaypalController extends Controller
$data->put('application_context',[
'return_url' => url('pay/paypal/capture'),
'cancel_url' => url('u/invoice/cart'),
'cancel_url' => url(self::cart_url),
]);
$paypal->body = $data->toArray();
@ -111,12 +113,16 @@ class PaypalController extends Controller
} catch (HttpException $e) {
Log::error('Paypal Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect()->to('u/invoice/cart')->withErrors('Paypal Exception: '.$e->getCode());
return redirect()
->to(self::cart_url)
->withErrors('Paypal Exception: '.$e->getCode());
} catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$request,'response'=>$e->getMessage()]);
Log::error('HTTP Exception',['request'=>$this->client,'response'=>$e->getMessage()]);
return redirect()->to('u/invoice/cart')->withErrors('HTTP Exception: '.$e->getCode());
return redirect()
->to(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode());
}
// Get the approval link
@ -128,18 +134,21 @@ class PaypalController extends Controller
}
}
if ($redirect_url) {
return redirect()->away($redirect_url);
}
if ($redirect_url)
return redirect()
->away($redirect_url);
return redirect()->to('u/invoice/cart')->withErrors('An error occurred with Paypal?');
return redirect()
->to(self::cart_url)
->withErrors('An error occurred with Paypal?');
}
/**
* Capture a paypal payment
*
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
* @return RedirectResponse
* @throws \PayPalHttp\IOException
*/
public function capture(Request $request)
{
@ -179,27 +188,37 @@ class PaypalController extends Controller
if ($redirect_url) {
Log::error('Paypal Capture: Redirect back to Paypal.');
return redirect()->away($redirect_url);
return redirect()
->away($redirect_url);
}
return redirect()->to('u/invoice/cart')->withErrors('An error occurred with Paypal?');
return redirect()
->to(self::cart_url)
->withErrors('An error occurred with Paypal?');
} catch (\HttpException $e) {
Log::error('HTTP Exception',['request'=>$paypal,'response'=>$e->getMessage()]);
return redirect()->to('u/invoice/cart')->withErrors('HTTP Exception: '.$e->getCode());
return redirect()
->to(self::cart_url)
->withErrors('HTTP Exception: '.$e->getCode());
}
if (! $response OR ! $response->result->purchase_units) {
if ((! $response) || (! $response->result->purchase_units)) {
Log::error('Paypal Capture: No Purchase Units?');
return redirect()->to('u/invoice/cart')->withErrors('Paypal Exception: NPU');
return redirect()
->to(self::cart_url)
->withErrors('Paypal Exception: NPU');
}
$co = Checkout::where('name','ilike','paypal')->firstOrFail();
// If we got here, we got a payment
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,8 +236,8 @@ class PaypalController extends Controller
break;
}
$po->date_payment = Carbon::parse($cap->create_time);
$po->checkout_id = $this->o->id;
$po->paid_at = Carbon::parse($cap->create_time);
$po->checkout_id = $co->id;
$po->checkout_data = $cap->id;
list($account_id,$fee) = explode(':',$cap->custom_id);
@ -229,7 +248,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);
@ -245,7 +264,11 @@ class PaypalController extends Controller
}
$request->session()->forget('invoice.cart');
Log::info('Paypal Payment Recorded',['po'=>$po->id]);
return redirect()->to('u/home')->with('success','Payment recorded thank you.');
return redirect()
->to('u/home')
->with('success','Payment recorded thank you.');
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
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_supplied_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(fn($item)=>['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(fn($item)=>['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(fn($item)=>['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(fn($item)=>['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(fn($item)=>['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(fn($item)=>['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);
}
}
public function addedit(ProductAddEdit $request,Product $o)
{
foreach (Arr::except($request->validated(),['translate','accounting','pricing','active']) as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->active;
// Trim down the pricing array, remove null values
$o->pricing = collect($request->validated('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->validated('translate',[]) as $key => $item)
$oo->{$key} = $item;
$o->translate()->save($oo);
foreach ($request->validated('accounting',[]) as $k=>$v) {
$o->providers()->syncWithoutDetaching([
$k => [
'ref' => $v,
'site_id'=>$o->site_id,
],
]);
}
return redirect()
->back()
->with('success','Product saved');
}
}

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,User};
class SearchController extends Controller
{
@ -14,73 +15,87 @@ 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_all)->pluck('id');
$user_ids = $x->pluck('user_id')->unique();
# Look for User
// 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 Account
// Look for User by their Supplier ID with some suppliers
if (is_numeric($request->input('term')))
foreach (Account::select(['user_id','suppliers.name AS supplier_name','account_supplier.supplier_ref AS pivot_id'])
->join('account_supplier',['account_supplier.account_id'=>'accounts.id'])
->join('suppliers',['suppliers.id'=>'account_supplier.supplier_id'])
->whereIN('accounts.id',$account_ids)
->where('account_supplier.supplier_ref','like','%'.$request->input('term').'%')
->orderBy('company')
->limit(10)->get() as $o)
{
$result->push(['name'=>sprintf('%s (%s:%s)',$o->company,$o->supplier_name,$o->supplier_ref),'value'=>'/u/home/'.$o->user_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('id',$account_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->name,$o->lid),'value'=>'/u/home/'.$o->user_id,'category'=>'Accounts']);
}
# Look for a Service
// 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)
->with(['product'])
->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->product->category_name]);
}
# Look for an Invoice
// 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'=>'/r/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(fn($item)=>$item['category'].$item['name'])
->values();
}
}

View File

@ -2,146 +2,479 @@
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\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Models\Service;
use App\Http\Requests\{ServiceCancel,ServiceChange,ServiceChangeRequest};
use App\Mail\{CancelRequest,ChangeRequest};
use App\Models\{Charge,Invoice,Product,Service};
class ServiceController extends Controller
{
/* SERVICE WORKFLOW METHODS */
/**
* Edit a domain service details
* Cancel a request to cancel a service
*
* @param Request $request
* @param Service $o
* @return \Illuminate\Http\RedirectResponse
* @return bool
*/
public function domain_edit(Request $request,Service $o)
private function action_cancel_cancel(Service $o): bool
{
session()->flash('service_update',TRUE);
if (! $o->order_info)
$o->order_info = collect();
$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',
]);
$o->order_info->put('cancel_cancel',Carbon::now()->format('Y-m-d H:i:s'));
$o->order_status = 'ACTIVE';
$o->stop_at = NULL;
$o->type->forceFill($validation['service'])->save();
return $o->save();
}
return redirect()->back()->with('success','Record updated.');
private function action_cancel_pending_enter(Service $o): bool
{
$o->order_status = 'CANCEL-PENDING';
return $o->save();
}
private function action_cancelled(Service $o): bool
{
$o->order_status = 'CANCELLED';
$o->active = FALSE;
return $o->save();
}
/**
* Cancel a request to change a service
*
* @param Service $o
* @return bool
*/
private function action_change_cancel(Service $o): bool
{
if (! $o->order_info)
$o->order_info = collect();
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
$np->pivot->active = FALSE;
$np->pivot->save();
$o->order_status = 'ACTIVE';
return $o->save();
}
/**
* Action to change a service order_status to another stage
* This is a generic function that can redirect the user to a page that is required to completed to enter
* the new stage
*
* @param Service $o
* @param string $stage
* @return \Illuminate\Contracts\Foundation\Application|RedirectResponse|\Illuminate\Routing\Redirector
*/
private function action_request_enter_redirect(Service $o,string $stage)
{
return redirect(sprintf('u/service/%d/%s',$o->id,strtolower($stage)));
}
/* OTHER METHODS */
public function change_pending(ServiceChangeRequest $request,Service $o)
{
// @todo add some validation if this doesnt return a result
$np = $o->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->get()->pop();
if ($request->post()) {
foreach ($this->service_change_charges($request,$o) as $co)
$co->save();
$o->product_id = Arr::get($request->broadband,'product_id');
$o->price = Arr::get($request->broadband,'price');
$o->order_status = 'ACTIVE';
$o->save();
$np->pivot->complete = TRUE;
$np->pivot->effective_at = Carbon::now();
$np->pivot->save();
return redirect()->to(url('u/service',[$o->id]));
}
return view('theme.backend.adminlte.service.change_pending')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o)
->with('np',$np);
}
/**
* Process a request to cancel a service
*
* @param ServiceCancel $request
* @param Service $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|RedirectResponse|\Illuminate\Routing\Redirector
*/
public function cancel_request(ServiceCancel $request,Service $o)
{
if (! $o->order_info)
$o->order_info = collect();
$o->stop_at = $request->stop_at;
$o->order_info->put('cancel_note',$request->validated('notes'));
if ($request->validated('extra_charges'))
$o->order_info->put('cancel_extra_charges_accepted',$request->extra_charges_amount);
$o->order_status = 'CANCEL-REQUEST';
$o->save();
Mail::to(config('osb.ticket_admin'))
->queue((new CancelRequest($o,$request->notes))->onQueue('email'));
return redirect('u/service/'.$o->id)
->with('success','Cancellation lodged');
}
/**
* 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 ServiceChange $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(ServiceChange $request,Service $o)
{
$o->changes()->attach([$o->id=>[
'site_id'=> $o->site_id,
'ordered_by' => Auth::id(),
'ordered_at' => Carbon::now(),
'effective_at' => $request->validated('change_date'),
'product_id' => $request->validated('product_id'),
'notes' => $request->validated('notes'),
'active' => TRUE,
'complete' => FALSE,
]]);
$o->order_status = 'CHANGE-REQUEST';
$o->save();
Mail::to(config('osb.ticket_admin'))
->queue((new ChangeRequest($o,$request->validated('notes')))->onQueue('email'));
return redirect('u/service/'.$o->id)
->with('success','Upgrade requested');
}
/**
* 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'])
$o = Service\Domain::ServiceActive()
->AccountUserAuthorised('services')
->select('service_domain.*')
->join('services',['services.id'=>'service_domain.service_id'])
->with(['service.account','registrar'])
->get();
return view('r.service.domain.list')
return view('theme.backend.adminlte.service.domain.list')
->with('o',$o);
}
public function email_list(): View
{
$o = Service\Email::ServiceActive()
->AccountUserAuthorised('services')
->select('service_email.*')
->join('services',['services.id'=>'service_email.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.email.list')
->with('o',$o);
}
/**
* Update a service
* Return details on the users service
*
* @param Service $o
* @return View
*/
public function home(Service $o): View
{
return view('theme.backend.adminlte.service.home')
->with('breadcrumb',collect()->merge($o->account->breadcrumb))
->with('o',$o);
}
public function hosting_list(): View
{
$o = Service\Host::ServiceActive()
->AccountUserAuthorised('services')
->select('service_host.*')
->join('services',['services.id'=>'service_host.service_id'])
->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld'])
->get();
return view('theme.backend.adminlte.service.hosting.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->invoiced_items
->filter(fn($item)=>($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->diffInDays($iio->stop_at)/$iio->start_at->diffInDays($iio->stop_at);
$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->diffInDays($iio->stop_at)/$iio->start_at->diffInDays($iio->stop_at);
$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('theme.backend.adminlte.service.change_charge')
->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');
Session::put('service_update',true);
$o->date_end = $request->post('date_end');
// We dynamically create our validation
$validator = Validator::make(
$request->post(),
collect($o->type->validation())
->keys()
->map(fn($item)=>sprintf('%s.%s',$o->product->category,$item))
->combine(array_values($o->type->validation()))
->map(fn($item)=>is_string($item)
? preg_replace('/^exclude_without:/',sprintf('exclude_without:%s.',$o->product->category),$item)
: $item)
->merge(
[
'external_billing' => 'nullable|in:on',
'suspend_billing' => 'nullable|in:on',
'recur_schedule' => ['required',Rule::in(collect(Invoice::billing_periods)->keys())],
'invoice_next_at' => 'nullable|date',
'price' => 'nullable|numeric',
$o->product->category => 'array|min:1',
]
)
->toArray()
);
foreach (['cancel_notes'] as $key) {
if ($request->post($key))
$o->setOrderInfo($key,$request->post($key));
}
$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');
if ($validator->fails()) {
return redirect()
->back()
->withErrors($validator)
->withInput();
}
}
private function update_order_status(Service $o)
{
return View('r.service.order.sent',['o'=>$o]);
}
$validated = collect($validator->validated());
private function update_request_cancel(Service $o)
{
return View('u.service.order.cancel',['o'=>$o]);
}
// Store our service type values
$o->type->forceFill($validated->get($o->product->category));
private function update_provision_planned(Service $o)
{
return View('r.service.order.provision_plan',['o'=>$o]);
// Some special handling
switch ($o->product->category) {
case 'broadband':
// If pppoe is not set, then we dont need username/password
$o->type->pppoe = ($x=data_get($validated,$o->product->category.'.pppoe',FALSE));
if (! $x) {
$o->type->service_username = NULL;
$o->type->service_password = NULL;
}
break;
}
$o->type->save();
if ($validated->has('invoice_next_at'))
$o->invoice_next_at = $validated?->get('invoice_next_at');
if ($validated->has('recur_schedule'))
$o->recur_schedule = $validated->get('recur_schedule');
$o->suspend_billing = ($validated->get('suspend_billing') == 'on');
$o->external_billing = ($validated->get('external_billing') == 'on');
$o->price = $validated->get('price');
// 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 ($validated->has('start_at'))
$o->start_at = $validated->get('start_at');
else {
// For broadband, start_at is connect_at in the type record
switch ($o->product->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,149 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use App\Http\Requests\{SupplierAddEdit,SupplierProductAddEdit};
use App\Jobs\ImportCosts;
use App\Models\{Supplier,SupplierDetail};
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)
{
foreach (Arr::except($request->validated(),['supplier_details','api_key','api_secret','submit']) as $key => $item)
$o->{$key} = $item;
$o->active = (bool)$request->validated('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');
}
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');
}
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 (Arr::only($request->validated(),[
'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 (Arr::only($request->validated(),['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');
}
/**
* 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.products.services']);
return view('theme.backend.adminlte.supplier.product.widget.'.$type)
->with('o',$id ? $o : NULL)
->withErrors($request->errors);
}
}

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

@ -1,43 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\{Account,Invoice};
class AccountController extends Controller
{
/**
* Show the users next invoice
*/
public function view_invoice_next(Account $o)
{
$io = new Invoice;
$io->account = $o;
// Get the account services
$s = $o->services(TRUE)
->with(['invoice_items','charges'])
->get()
->filter(function($item) {
return ! $item->suspend_billing AND ! $item->external_billing;
});
// Get our invoice due date for this invoice
$io->due_date = $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);
// Work out items to add to this invoice, plus any in the next additional days
$days = now()->diffInDays($io->due_date)+1+7;
foreach ($s as $so)
{
if ($so->isInvoiceDueSoon($days))
foreach ($so->next_invoice_items() as $o)
$io->items->push($o);
}
return View('u.invoice.home',['o'=>$io]);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Session;
use App\Http\Requests\{AccountSupplierAdd,UserEdit};
use App\Models\{Account,Supplier,User};
class UserController extends Controller
{
/**
* Update user settings
*
* @param UserEdit $request
* @param User $o
* @return RedirectResponse
*/
public function edit(UserEdit $request,User $o): RedirectResponse
{
foreach (Arr::except($request->validated(),['password']) as $field => $value)
$o->{$field} = $value;
if ($x=$request->validated('password'))
$o->password = Hash::make($x);
return redirect()
->back()
->with('success',($o->isDirty() && $o->save()) ? 'User Updated' : 'No Changes');
}
/**
* Add a supplier to a user's profile
*
* @param AccountSupplierAdd $request
* @param Account $o
* @return RedirectResponse
*/
public function supplier_addedit(AccountSupplierAdd $request,Account $o): RedirectResponse
{
$o->suppliers()->attach([
$request->validated('supplier_id') => [
'supplier_ref'=>$request->validated('supplier_ref'),
'created_at'=>Carbon::now(),
]
]);
return redirect()
->back()
->with('success','Supplier Added');
}
/**
* Remove a supplier from a user's profile
*
* @param Account $o
* @param Supplier $so
* @return RedirectResponse
*/
public function supplier_delete(Account $o,Supplier $so): RedirectResponse
{
Session::put('supplier_update',TRUE);
$o->suppliers()->detach([$so->id]);
return redirect()
->back()
->with('success','Supplier Deleted');
}
}

View File

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

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Controllers\Wholesale;
use App\Http\Controllers\Controller;
class ReportController extends Controller
{
public function products()
{
return view('a/product/report');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,19 @@
namespace App\Http\Middleware;
use Illuminate\Http\Request;
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)
public function handle(Request $request, Closure $next, string $role)
{
if ($role AND ! Auth::user())
return abort(303,'Not Authenticated');
abort(403,'Not Authenticated');
switch ($role) {
case 'wholesaler':

View File

@ -2,10 +2,10 @@
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View;
use Closure;
use App\Models\Site;
@ -20,11 +20,11 @@ class SetSite
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request,\Closure $next): mixed
{
$so = new Site;
@ -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

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rule;
class AccountSupplierAdd extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('wholesaler');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
Session::put('supplier_update',true);
return [
'supplier_ref'=> [
'required',
'string',
'min:2',
Rule::unique('account_supplier')
->where(fn($query)=>$query
->where('account_id',request()->get('account_id')))
->where('supplier_id',request()->get('supplier_id')),
],
'supplier_id'=>'required|exists:suppliers,id',
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use App\Models\{Charge,InvoiceItem};
class ChargeAdd extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Auth::user()
->accounts_all
->contains(request()->post('account_id'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
Session::put('charge_add',true);
return [
'id' => 'sometimes|exists:charges,id',
'account_id' => 'required|exists:accounts,id',
'charge_at' => 'required|date',
'service_id' => 'required|exists:services,id',
'site_id' => 'required|exists:sites,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|min:5|max:128',
];
}
}

View File

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

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use App\Models\Invoice;
/**
* Editing Suppliers
*/
class PaymentAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Gate::allows('wholesaler');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'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:accounts,id',
'pending' => 'nullable|boolean',
'notes' => 'nullable|string',
'ip' => 'nullable|ip',
'invoices' => [
'nullable',
'array',
function($attribute,$value,$fail) {
if (($x=collect($value)->sum()) > request()->post('total_amt'))
$fail(sprintf('Allocation %3.2f is greater than payment total %3.2f.',$x,request()->post('total_amt')));
}
],
'invoices.*' => [
'nullable',
function($attribute,$value,$fail) {
if (! ($x=Invoice::where('id',$xx=str_replace('invoices.','',$attribute))->first()))
$fail(sprintf('Invoice [%d] doesnt exist in DB',$xx));
// @todo The due amount may be influenced by this payment (ie: payment edit)
elseif($x->due < $value)
$fail(sprintf('Invoice [%d] is over allocated by %3.2f',$x->id,$value-$x->due));
}
],
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use App\Models\ProviderOauth;
/**
* Editing Suppliers
*/
class ProductAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Gate::allows('wholesaler');
}
/**
* 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',
function (string $attribute,mixed $value,\Closure $fail) {
if (! is_array($value))
$fail("Invalid format for {$attribute}");
foreach ($value as $k=>$v) {
if (! ProviderOauth::where('id',$k)->exists())
$fail("Provider doesnt exist [$k]");
// @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,54 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
/**
* Editing Suppliers
*/
class ServiceCancel extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Gate::allows('view',$this->route('o'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
//dd(request()->post());
return [
'stop_at'=> [
'required',
'date',
'after:today',
'exclude_unless:extra_charges,null',
function($attribute,$value,$fail) {
if ($this->route('o')->cancel_date->greaterThan($value))
$fail(sprintf('Service cannot be cancelled before: %s',$this->route('o')->cancel_date->format('Y-m-d')));
}
],
'extra_charges_amount' => [
'nullable',
'exclude_unless:extra_charges,null',
function($attribute,$value,$fail) {
if ($this->route('o')->cancel_date->greaterThan(request('stop_at')) && (request('extra_charges') !== 1))
$fail('Extra charges must be accepted if cancelling before contract end');
},
],
'extra_charges' => 'sometimes|required|accepted',
'notes' => 'nullable|min:5',
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
/**
* Editing Suppliers
*/
class ServiceChange extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Gate::allows('view',$this->route('o'));
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'change_date'=> [
'required',
'date',
'after:today',
],
'product_id' => [
'required',
'exists:products,id',
Rule::notIn([request()->route('o')->product_id]),
],
'notes' => 'nullable|min:5',
];
}
}

View File

@ -0,0 +1,41 @@
<?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')
->AccountUserAuthorised();
}
/**
* 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,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class SiteEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('wholesaler');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'site_name' => 'required|string|min:2|max:255',
'site_email' => 'required|string|email|max:255',
'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',
'site_tax' => 'required|regex:/[0-9 ]+/|size:14',
'social' => 'nullable|array',
'top_menu' => 'nullable|array',
'site_logo' => 'nullable|image',
'email_logo' => 'nullable|image',
];
}
}

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

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Password;
class UserEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Auth::id() === $this->route('o')->id;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email'=>'required|email|min:5',
'password'=>['nullable','confirmed',Password::min(8)],
'firstname'=>'required|min:2',
'lastname'=>'required|min:2',
'address1'=>'required|min:8',
'address2'=>'nullable|min:8',
'city'=>'required|min:4',
'state'=>'required|min:3|max:3',
'postcode'=>'required|min:4|max:4',
'country_id'=>'required|exists:countries,id'
];
}
}

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,79 @@
<?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\{Invoice,ProviderToken};
/**
* Synchronise invoices ref numbers with our database
*/
class AccountingInvoiceSync implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JIS';
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 = Invoice::select('id')->get();
foreach ($api->getInvoices() 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(fn($item)=>$item->lid == $acc->DocNumber))->count() === 1) {
$o = $x->pop();
} else {
// Log not found
Log::alert(sprintf('%s:Invoice not found [%s:%s]',self::LOGKEY,$acc->id,$acc->DocNumber));
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:Invoice updated [%s:%s]',self::LOGKEY,$o->id,$acc->DocNumber));
}
}
}

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(fn($item)=>$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

@ -8,70 +8,81 @@ 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\DB;
use Illuminate\Support\Arr;
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\{Rtm,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
* Read and update the traffic for a 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\\';
private const class_prefix = 'App\Classes\External\Supplier\\';
public function __construct(AdslSupplier $o)
{
$this->aso = $o;
}
private const traffic = 'broadband';
public function __construct(private string $supplier) {}
/**
* Execute the job.
*
* @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__]);
$so = Supplier::active()
->where('name','ilike',$this->supplier)
->sole();
// Wholesaler email
$ro = Rtm::where('parent_id',NULL)->sole();
Log::info(sprintf('%s:Importing Broadband Traffic from [%s]',self::LOGKEY,$so->name));
if ((! $connection=$so->detail->connections->get('broadband')) || (count(array_intersect(array_keys($connection),ExternalSupplier::traffic_connection_keys)) !== 3))
throw new \Exception('No or missing connection details for:'.self::traffic);
// Count of updated records
$u = 0;
// Load our class for this supplier
$class = $this->class_prefix.$this->aso->name;
$class = self::class_prefix.$so->name;
if (class_exists($class)) {
$o = new $class($this->aso);
$o = new $class($so);
} 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',$so->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 +90,27 @@ 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'))
->where(function($query) use ($date) {
$query->whereNULL('ab_service.date_end')
->orWhere('ab_service.date_end','<=',$date->format('U'));
})
->get();
// Find the right service dependent on the dates we supplied the service
$oo = ServiceBroadband::where('service_username',$row[$o->getColumnKey('Login')])
->select('service_broadband.*')
->join('services',['service_broadband.service_id'=>'services.id'])
->where('services.start_at','<=',$date)
->where(fn($query)=>
$query->whereNULL('services.stop_at')
->orWhere('services.stop_at','>=',$date)
)
->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 = $so->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 +120,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.
$so->detail->connections = $so->detail->connections->put(self::traffic,array_merge($connection,['last'=>$last_update->format('Y-m-d')]));
$so->detail->save();
// Update our details for the next iteration.
$last_update = $last_update->addDay();
Arr::set($connection,'last',$last_update->format('Y-m-d'));
if ($u) {
$this->aso->stats_lastupdate = $this->aso->stats_lastupdate->addDay();
$this->aso->save();
if ($this->aso->trafficMismatch($date)->count())
Mail::to('deon@graytech.net.au') // @todo To change
->send(new TrafficMismatch($this->aso,$date));
if ($so->trafficMismatch($date)->count())
Mail::to($ro->owner->email)
->send(new TrafficMismatch($so,$date));
}
}
}

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

@ -0,0 +1,232 @@
<?php
namespace App\Jobs;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Models\{Cost,Service,Site,Supplier};
use App\Traits\Import;
class ImportCosts implements ShouldQueue
{
use Dispatchable,InteractsWithQueue,Queueable,SerializesModels,Import;
private const LOGKEY = 'JIC';
private Cost $co;
private Site $site;
private string $file;
protected Collection $columns;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Site $site,Supplier $so,Carbon $invoice_date,string $file)
{
$this->file = $file;
$this->site = $site;
$this->co = Cost::where('site_id',$site->site_id)
->where('supplier_id',$so->id)
->where('billed_at',$invoice_date)
->firstOrNew();
$this->co->active = TRUE;
$this->co->site_id = $site->site_id;
$this->co->billed_at = $invoice_date;
$this->co->supplier_id = $so->id;
$this->co->save();
Cost\Broadband::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
Cost\Phone::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
Cost\Generic::where('cost_id',$this->co->id)->where('site_id',$site->site_id)->delete();
// @todo to be stored in supplier config
$headers = [
'INVOICEID'=>'Item ID',
'REF'=>'Reference No',
'IDTAG'=>'ID Tag',
'CATEGORY'=>'Category',
'DESC'=>'Item Description',
'QUANTITY'=>'Quantity',
'PRICEUNIT'=>'Unit Price (inc-GST)',
'PRICETOTAL'=>'Total (inc-GST)'
];
$this->columns = collect($headers)->map(fn($item)=>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])->ServiceActive()->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->product->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->product->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->product->category,$c));
}
} else {
dump(['line'=>$line,'sql'=>Service::search($m[1])->ServiceActive()->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);
}
}

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