Compare commits

...

320 Commits

Author SHA1 Message Date
Tom Bloor e495f744d9
Merge pull request #127 from Pear-Trading/master
Master merge for v0.10.10
2019-09-24 11:57:51 +01:00
Tom Bloor 48f8f20202
Merge pull request #126 from Pear-Trading/Release-v0.10.10
Release v0.10.10
2019-09-16 11:08:08 +01:00
Thomas Bloor d9673f32e3
Update perl version in Travis CI 2019-09-16 10:46:38 +01:00
Thomas Bloor 5149121e11
Update changelog 2019-09-16 10:43:44 +01:00
Thomas Bloor 0d323c985f
Merge branch 'TBSliver/NewGraphs' into development 2019-09-16 10:41:06 +01:00
Finn ff04f44232 flash fixes on import 2019-09-11 15:02:08 +01:00
Finn fdbe86a464 made changes to CSV import to improve memory usage
uses same mechanism as postcode import code change does
2019-09-11 14:19:12 +01:00
Thomas Bloor acad46a9b5
need to check headers first 2019-09-10 09:32:15 +01:00
Thomas Bloor 9299c46fdf
reduce memory usage importing CSV files 2019-09-10 09:29:17 +01:00
Thomas Bloor 1efabeb45e
fix a few bugs, oops 2019-09-09 19:48:16 +01:00
Thomas Bloor 103cf61ec6
filter everything by date 2019-09-09 19:32:14 +01:00
Thomas Bloor 653f495a70
added search on supplier listings 2019-09-09 18:03:08 +01:00
Thomas Bloor eabc4e04fb
Merge remote-tracking branch 'origin/finn/postcodeimport' into TBSliver/NewGraphs 2019-09-09 16:53:44 +01:00
Thomas Bloor f89572e3de
oops still using wrong column name 2019-09-09 16:44:05 +01:00
Thomas Bloor d484b342df
fix transaction list in admin and speed up external count 2019-09-09 16:42:49 +01:00
Finn 916e77a238 Added type name filter to show data better on frontend 2019-09-09 16:24:07 +01:00
Thomas Bloor 962cf972da
Hopefully fix speed issue on external data 2019-09-09 15:37:56 +01:00
Finn 3b8b5b97f4 Added extra data showing 2019-09-06 17:31:09 +01:00
Finn af9069b17a
added ability to import sheets with extra data 2019-09-02 15:34:28 +01:00
Finn c977cf3279
added importing doogal data for wards on postcode 2019-08-29 16:37:55 +01:00
Felix 9b7d8530b6 Revert "custom graph duration and dateRange added"
This reverts commit bbb7edd269.
2019-08-14 14:02:01 +01:00
Felix bbb7edd269 custom graph duration and dateRange added
(untested)
2019-08-14 14:00:34 +01:00
Tom Bloor 0328bdc1f6
Fix import ref 2019-07-15 12:36:28 +01:00
Tom Bloor b1ab789455
Added search ref option 2019-07-15 10:11:59 +01:00
Tom Bloor 51f0fb406e
Allow for per page setting for supplier list 2019-07-15 10:03:44 +01:00
Tom Bloor 5accf45cde
And then called them against the right table 2019-07-15 05:34:11 +01:00
Tom Bloor 7dc7acb1a1
Turns out I wrote three queries 2019-07-15 04:54:55 +01:00
Tom Bloor 86d52e7bbc
Group by the right thing this time 2019-07-15 04:52:55 +01:00
Tom Bloor 43808f5510
Fix some annoyances with joins 2019-07-15 04:50:46 +01:00
Tom Bloor ed2b6970f4
Supplier history view 2019-07-15 04:45:57 +01:00
Tom Bloor 9357405445
Make value actually nnot be stupid 2019-07-15 04:28:44 +01:00
Tom Bloor b02f8b7c5f
This should have fixed a few issues... need to speed things up though 2019-07-15 04:25:51 +01:00
Tom Bloor 4fdff21f50
Change to using a name map temporarily 2019-07-15 03:45:42 +01:00
Tom Bloor 546193f0cb
Updated view with actual column spec 2019-07-15 02:48:49 +01:00
Tom Bloor 314c3e4bd5
More graph niceness 2019-07-15 02:41:23 +01:00
Tom Bloor 135fd890b2
use the correct column name 2019-07-15 01:55:02 +01:00
Tom Bloor 06b08cff07
Add new year spend graph 2019-07-15 01:33:07 +01:00
Tom Bloor 4d3aca2457
remove unused order by 2019-07-14 19:43:09 +01:00
Tom Bloor d661ab5996
Another possible fix for transactions under postgres 2019-07-14 19:41:02 +01:00
Tom Bloor 00dbb77130
Possible fix for transactions under postgres 2019-07-14 18:47:48 +01:00
Tom Bloor 330a4a93ed
Merge pull request #125 from Pear-Trading/TBSliver/MinionImport
Minion Import
2019-07-14 16:21:52 +01:00
Tom Bloor 39aa2d2083
Fix meta transaction undef error 2019-07-14 16:05:35 +01:00
Tom Bloor 7d862290d3
Possible fix for test failure 2019-07-14 15:52:39 +01:00
Tom Bloor 71189d18fc
Fix various bits for import 2019-07-14 15:15:14 +01:00
Finn a45c354834
Added entity postcode lookup 2019-07-12 21:30:34 +01:00
Finn cc84dbb76e
Added meta data to transactions 2019-07-12 20:46:39 +01:00
Finn 536d11f298
Fully working supplier list 2019-07-12 20:04:38 +01:00
Finn f6ed82a02f
Added API for supplier table 2019-07-12 18:51:38 +01:00
Finn 6ecc3d3c56
fixed snippets test 2019-07-12 17:16:19 +01:00
Finn 94aced117d
updated git ignore and commented out currently redundant code 2019-07-12 17:07:42 +01:00
Finn ee4e5a868c
fixed date on import AGAIN 2019-07-12 16:08:02 +01:00
Finn 275b9cef46
Fixing import 2019-07-12 16:02:19 +01:00
Finn 8f44e26a6d
Moved and added data for graphs etc. for org dashboard 2019-07-12 13:39:37 +01:00
Finn eeb7a852be
Added extra snippet data 2019-07-11 12:44:04 +01:00
Finn 5203df3d50
got rid of dwarn statements 2019-07-11 11:28:02 +01:00
Finn efbf8cbad7
Fully added working import and API 2019-07-10 17:23:27 +01:00
Finn aa2d429403
made it proper numerics 2019-07-09 18:06:31 +01:00
Finn 1444574d26
Properly catch errors loading minions 2019-07-09 17:55:50 +01:00
Finn 9c79e50eba
implemented API for transaction and supplier log 2019-07-09 16:25:32 +01:00
Finn 1c942f0be7
fixed some api and import stuff 2019-07-09 13:50:26 +01:00
Finn d0506c2a95
Added properly working imports with minions and status 2019-07-08 18:12:35 +01:00
Finn 22e6001362
Wip on changing flow with the minion 2019-07-08 16:41:59 +01:00
Finn bc74496738
Merge remote-tracking branch 'origin/TBSliver/Minion-Tasks' into finn/minionimport 2019-07-05 18:50:37 +01:00
Finn afc635fdb4
Added nicer explosions via try::tiny 2019-07-05 18:44:46 +01:00
Finn d1cd30928e
Fully added Transaction importing 2019-07-05 17:56:21 +01:00
Finn 46b5496901
Added submitting Transactions (currently breaking on headers) 2019-07-05 16:52:32 +01:00
Finn bf4b092a12
Added in importing Supplier CSV 2019-07-05 15:30:31 +01:00
Finn 4b7550f569
added initial UI for uploading csv 2019-07-04 14:16:49 +01:00
Finn c456428681
added new relations 2019-07-03 17:36:36 +01:00
Thomas Bloor bea1301475
In progress commit 2019-07-02 15:21:01 +01:00
Thomas Bloor 1d22da2bed
added new transaction meta table 2019-06-28 15:23:18 +01:00
Thomas Bloor 8f3da8ae1b
intellij! 2019-06-28 15:08:59 +01:00
Finn e974325d98
Merge pull request #124 from Pear-Trading/finn/icons
Added icons to category list
2018-06-18 12:29:34 +01:00
Finn aabfc6505b amended stats 2018-06-18 12:14:34 +01:00
Finn adb19e84f7 added default 2018-06-18 12:10:08 +01:00
Finn 8d4c9703a0 got rid of Dwarn 2018-06-14 16:47:36 +01:00
Finn 2b0abd2606 added ability to edit icons and gt them in stats 2018-06-14 16:40:50 +01:00
Finn 4323e49cfc Added icon line to category schema 2018-06-12 12:19:23 +01:00
Finn 969b529a66
Merge pull request #123 from Pear-Trading/master
Master
2018-06-06 14:32:14 +01:00
Finn 644ccadbfc
Merge pull request #122 from Pear-Trading/v0.10.9
v0.10.9
2018-06-06 14:15:46 +01:00
Finn 2296fb8d42 updated changelog 2018-06-05 14:47:43 +01:00
Finn aabe98cc67 Merge branch 'development' into v0.10.9 2018-06-05 14:44:43 +01:00
Finn b8b06c06fe
Merge pull request #121 from Pear-Trading/TBSliver/Recur-Calc-Fix
Fix recurring transactions calculator for after-the-fact updates
2018-06-05 14:43:51 +01:00
Finn 61f5d761da updated changelog 2018-06-05 14:23:39 +01:00
Finn 2fd0eed782
Merge pull request #120 from Pear-Trading/finn/moregraphs
added graph stuff for dashboard
2018-06-04 16:05:04 +01:00
Finn 0f98f22404 updated stats test 2018-06-04 15:48:21 +01:00
Thomas Bloor 25b6b3a9a4
Fix recurring transactions calculator for after-the-fact updates 2018-05-24 15:16:28 +01:00
Tom Bloor ccdbff42e6
Merge pull request #119 from Pear-Trading/master
Master
2018-05-24 14:49:33 +01:00
Tom Bloor 10ea8cfe92
Merge pull request #118 from Pear-Trading/Release-v0.10.8
Release v0.10.8
2018-05-24 13:32:16 +01:00
Thomas Bloor e30717383c
Updated Changelog 2018-05-24 13:01:22 +01:00
Finn 6741aba746 Removed sector data API for customer
Should eventually be given to organisation dashboard
2018-05-22 12:47:19 +01:00
Finn 779a6e77ff Added category all time purchase list 2018-05-22 12:23:17 +01:00
Finn 174ce1f105
Merge pull request #117 from Pear-Trading/finn/yearlysubmit
added being able to work with yearly submit
2018-05-15 17:16:31 +01:00
Finn 6d2cbecffa added being able to work with yearly submit 2018-05-15 16:51:39 +01:00
Finn eeeaaaaddd Merge branch 'master' into development 2018-04-16 17:04:11 +01:00
Finn eee925aa80
Merge pull request #116 from Pear-Trading/v0.10.7
v0.10.7 Release
2018-04-16 16:41:09 +01:00
Finn 59180ed310 Changelog amended 2018-04-16 16:25:07 +01:00
Thomas Bloor c3ac620a2d
Merge branch 'development' into TBSliver/Minion-Tasks 2018-04-16 12:49:59 +01:00
Tom Bloor 2ceecaa0a1
Merge pull request #114 from Pear-Trading/TBSliver/Cron-Jobs
Adding Cron Job script
2018-04-16 12:45:55 +01:00
Finn 376db29a7c
Merge pull request #115 from Pear-Trading/finn/graphs
added new graph views
2018-04-13 18:24:22 +01:00
Finn a4bcd9c6d7 changed stat viewing and amended tests 2018-04-13 18:08:10 +01:00
Finn 78fdfc1e1d added month listing 2018-04-11 19:10:19 +01:00
Finn bc92d2eb11 essential data added for bar chart 2018-04-09 19:21:14 +01:00
Finn 9284218431 changed pie data structure 2018-04-05 17:19:53 +01:00
Finn ec9fac293d fixed category list API 2018-03-26 15:13:35 +01:00
Finn b036d5494b changing pie format (broken) 2018-03-26 14:44:03 +01:00
Finn 4b4d50de07 Amended category listing 2018-03-26 14:43:23 +01:00
Thomas Bloor b22b85e0f2
Updated Changelog 2018-03-26 14:27:04 +01:00
Thomas Bloor 4b98de9075
Added new Daily Cron script 2018-03-26 14:26:02 +01:00
Finn 4844174ead
Merge pull request #113 from Pear-Trading/finn/statfix
fixed display error
2018-03-22 14:53:07 +00:00
Finn dc8f240243
Merge pull request #112 from Pear-Trading/TBSliver/Minor-Fixes
Minor Fixes
2018-03-21 18:05:56 +00:00
Finn e817dc29b1 fixed display error 2018-03-21 17:58:50 +00:00
Thomas Bloor 8a2aa3a73a
Update changelog 2018-03-21 17:49:00 +00:00
Thomas Bloor 2286e54104
Allow for parsing currency without a currency sign in front 2018-03-21 17:24:55 +00:00
Thomas Bloor 2b5bb9cd8c
Stop error on large csv exceeding size of cookies 2018-03-21 17:24:41 +00:00
Thomas Bloor 2d03d25916
Set secrets with decent default for production 2018-03-21 17:24:13 +00:00
Finn b35e18f181 Merge remote-tracking branch 'origin/master' into development 2018-03-21 17:16:26 +00:00
Thomas Bloor 3514a9e0ed
Created role for Minion Jobs to make dev easier 2018-03-21 17:14:51 +00:00
Finn 47bca8d39f
Merge pull request #111 from Pear-Trading/Release-v0.10.6
v0.10.6 Release
2018-03-21 16:58:58 +00:00
Finn 055b95e2fc customer snippet test amended 2018-03-21 16:37:40 +00:00
Finn c5341af3e7 customer stats test fixed 2018-03-21 16:01:46 +00:00
Finn 6527d1e36c fixed category list on postgres 2018-03-21 15:52:00 +00:00
Thomas Bloor a53479c6c8
Stopped example job being enqueued if Minion is enabled 2018-03-20 19:25:32 +00:00
Thomas Bloor 1302f9e843
Added initial Minion support and example test job 2018-03-20 19:24:48 +00:00
Finn fb60ba4c0f Amended changelog 2018-03-20 19:24:04 +00:00
Finn 047ec888fa
Merge pull request #110 from Pear-Trading/finn/recurring
added recurring transaction editing and deletion
2018-03-20 19:19:15 +00:00
Finn 4ff3f07f9a fixed transaction test for updating and deleting 2018-03-20 18:54:55 +00:00
Finn 49e5e91860 made tests sane 2018-03-20 18:46:50 +00:00
Finn 73d44feace added deleting and updating transactions 2018-03-20 18:43:00 +00:00
Finn 2cf0678126 fixed to category viewing and recurring transaction data 2018-03-20 12:19:04 +00:00
Finn bcb0cd642c Merge branch 'master' into finn/recurring 2018-03-19 16:17:24 +00:00
Finn 97462df1a2 Fix to add org submission 2018-03-19 13:36:51 +00:00
Finn cc6ea41ce5 Merge remote-tracking branch 'origin/master' into development 2018-03-15 17:09:56 +00:00
Finn 58ae5b5250
Merge pull request #109 from Pear-Trading/Release-v0.10.5
v0.10.5
2018-03-15 16:54:02 +00:00
Finn 44f2a321b2
Merge pull request #108 from Pear-Trading/finn/translistchange
Revamped transaction view on admin interface
2018-03-15 16:27:17 +00:00
Finn 75ae16cd8d Merge branch 'finn/translistchange' into finn/recurring 2018-03-15 16:05:44 +00:00
Finn 4fb85b9094 Changelog amended 2018-03-15 16:04:21 +00:00
Finn b157ba0843 revamped transaction list view 2018-03-15 16:02:17 +00:00
Finn b233acfd64 made changes to still have status view from delete 2018-03-15 13:38:06 +00:00
Finn cea9e62073 amended user info view to accordion 2018-03-15 13:28:31 +00:00
Finn 3ceb926cd4 removed transaction list code 2018-03-15 13:07:29 +00:00
Finn 60438ea51c amended transactionlist code 2018-03-15 13:06:50 +00:00
Finn 551a40a9a0 fixed critical bugs introduced earlier and changed category viewing 2018-03-14 19:38:05 +00:00
Finn d5e03cc9e3 amended api for recurring transaction list 2018-03-14 17:55:24 +00:00
Finn 0cb3426825 amended upload to allow for validation changes 2018-03-13 12:55:13 +00:00
Finn 29eaa291c5 Merge branch 'master' into development 2018-03-09 17:45:19 +00:00
Finn fdea4be36f fix DDL 2018-03-09 17:44:47 +00:00
Finn ca389a1788 Merge branch 'master' into development 2018-03-09 17:39:26 +00:00
Finn 371cf1ea42 Merge branch 'finn/medalhotfix' 2018-03-09 17:39:11 +00:00
Finn af43a3ce9f hotfix to medal schema data types 2018-03-09 17:38:12 +00:00
Finn 2edc45a22e
Merge pull request #107 from Pear-Trading/master
merge to dev
2018-03-09 17:28:41 +00:00
Finn 241fe26f26
Merge pull request #106 from Pear-Trading/Release-v0.10.4
Release 0.10.4
2018-03-09 17:11:35 +00:00
Finn 0441128023 Version bump 2018-03-09 16:54:03 +00:00
Finn 12bde771b2 Merge branch 'finn/changelog' into development 2018-03-09 13:53:08 +00:00
Finn ea18e467f4 changelog amended 2018-03-09 13:52:46 +00:00
Finn a3dcd57fea
Merge pull request #105 from Pear-Trading/finn/recurring
Added script for recurring transactions and changed recurring logic
2018-03-08 16:58:41 +00:00
Finn d4d1b841d7 changed logic of start time for transaction and submitted time 2018-03-08 16:41:45 +00:00
Finn bdf23b2f3d fully operational and functioning recurring script + changes to schema 2018-03-08 15:31:44 +00:00
Finn 813cbb82a5 whitespace fix and created initial recurring transaction script 2018-03-07 18:08:11 +00:00
Finn 98cd134d84 Changed logic of storing recurring transactions 2018-03-07 15:34:41 +00:00
Finn 3cf73aca37
Merge pull request #104 from Pear-Trading/finn/recurring
added uploading and viewing recurring purchases
2018-03-07 13:05:46 +00:00
Finn 38dc29f4a1 amended test to include a recurring period 2018-03-07 12:50:36 +00:00
Finn d937e64663 Fixed recurring purchase entry 2018-03-07 12:46:50 +00:00
Finn 79324ff5f7 Upgraded schema and made fixes 2018-03-05 16:18:17 +00:00
Finn 786cd1618a added initial possible schema and ability to submit on transaction 2018-03-05 15:49:30 +00:00
Finn 7aa93bf380 Added initial for upload to accept recurring type 2018-03-05 15:37:01 +00:00
Finn ab88755b00
Merge pull request #103 from Pear-Trading/finn/medalschema
added essential purchase option, fixed category view and added initial medal schema
2018-03-02 18:04:41 +00:00
Finn 712959c37e Fixed code to ensure tests passed 2018-03-02 17:33:57 +00:00
Finn 23185985b8 category test fixed 2018-03-02 17:00:01 +00:00
Finn 1377742acd removed debug line and unneeded code 2018-03-02 16:42:30 +00:00
Finn ebcc79fd36 added fixes for budget view and working essential 2018-03-02 16:32:28 +00:00
Finn af6d198c8f added viewing of purchase being essential in transaction read 2018-03-01 17:20:50 +00:00
Finn 396cb3c8f6 amended upload code to allow for essential purchases 2018-03-01 17:08:44 +00:00
Finn 28a33cf27f added essential flag to transactions in schema 2018-03-01 17:08:30 +00:00
Finn f519bb9f2f Fixed schema and added update for sql 2018-02-21 12:51:43 +00:00
Finn 413479b94f fixed table name and added initial org medal schema 2018-02-20 17:44:22 +00:00
Finn 940d96921c Added initial schema for medals 2018-02-20 17:20:51 +00:00
Finn 480c8ed78f
Merge pull request #101 from Pear-Trading/finn/medals
Added placeholder API for user points
2018-02-08 12:17:12 +00:00
Finn 156af841c2 Added placeholder API for user points 2018-02-05 14:58:55 +00:00
Finn 26191d413d
Merge pull request #100 from Pear-Trading/finn/medals
Placeholder medal and endpoint added
2018-02-01 11:11:30 +00:00
Finn 6879a8b3b8 made placeholder more obvious 2018-01-31 12:54:14 +00:00
Finn 613272f413 Added routing and placeholder for medal data 2018-01-31 12:52:52 +00:00
Finn da86e52735
Merge pull request #99 from Pear-Trading/finn/categoryAPI
Updated data view
2018-01-26 10:29:31 +00:00
Finn a4706ef05e Updated changelog 2018-01-26 10:09:47 +00:00
Finn 1b74931248 Updated data view 2018-01-26 09:51:28 +00:00
Finn 9f3971815e
Merge pull request #98 from Pear-Trading/finn/categorylist
Category budget view added with test
2018-01-25 16:22:45 +00:00
Finn 3ea79ad8de Working test added 2018-01-24 16:08:20 +00:00
Finn ecbcd205fe category purchase list fully grouped 2018-01-24 13:19:35 +00:00
Finn 6cd7df1259 working API with arrays 2018-01-22 16:26:45 +00:00
Finn 6e970de92c Merge branch 'development' into finn/categorylist 2018-01-19 12:31:32 +00:00
Finn 41928e1ff6 Merge branch 'master' into development 2018-01-18 14:26:18 +00:00
Finn 7a44797a56 Merge branch 'Hotfix-categoryandstatsfix' 2018-01-18 14:26:04 +00:00
Finn 65a4447478 fixed to admin interface categories & stats 2018-01-18 14:25:45 +00:00
Finn e64e4bcbc8 fixed to admin interface categories & stats 2018-01-18 14:24:47 +00:00
Finn cab32fbf92
Merge pull request #97 from Pear-Trading/development
Release of category API and admin interface
2018-01-18 12:24:05 +00:00
Finn 8c8da1f795 Updated changelog 2018-01-18 12:21:47 +00:00
Finn 39549d257f Added version to changelog 2018-01-18 12:06:07 +00:00
Finn b247183914 Changed logic for transaction list API 2018-01-17 17:38:24 +00:00
Finn de9d70432f added further fix 2018-01-17 17:38:07 +00:00
Finn cd4f13bf8c Fixed transaction ID edit 2018-01-17 17:36:55 +00:00
Finn 3e9746ca22 Added ability to change ID 2018-01-17 17:23:12 +00:00
Finn 6fe4cd9a31 fixed api and quantised statement code 2018-01-17 17:14:50 +00:00
Finn b383284519 Added initial ability to get transaction category list from month 2018-01-17 16:47:05 +00:00
Finn 36947bfeab Merge branch 'finn/transactioncategory' into development 2018-01-16 16:43:17 +00:00
Finn a9ca2efe9a updated changelog 2018-01-16 16:42:50 +00:00
Finn 7f9c53aa5a
Merge pull request #96 from Pear-Trading/finn/transactioncategory
Added categories API and backend
2018-01-16 16:40:46 +00:00
Finn 8e00135390 Tests fixed adding for category 2018-01-16 15:55:19 +00:00
Finn dddbb04023 fixed delete checking for category on transaction 2018-01-16 14:52:16 +00:00
Finn ad30cf9cd9 fixed reading transactions and category deletion 2018-01-16 14:47:17 +00:00
Finn 8a0f933f07 redid relationships of transactions/categories 2018-01-16 13:07:12 +00:00
Finn 1f70f9b40a Fixed upload and added category read to transaction 2018-01-15 17:14:16 +00:00
Finn c4f68332b9 Fixed upload code 2018-01-15 17:01:59 +00:00
Finn 175026e37e functional category loading and category in upload 2018-01-15 16:57:19 +00:00
Finn 53b63aa655 fixed faulty syntax 2018-01-15 14:34:02 +00:00
Finn 4369a95054 added category API 2018-01-15 14:18:27 +00:00
Finn 4e74c871a2 Categories admin interface fully implemented 2018-01-11 16:23:42 +00:00
Finn c2a099c3e2 Added transaction categories tables 2018-01-11 14:00:20 +00:00
Tom Bloor 7ea96a74d0
Merge pull request #95 from Pear-Trading/master
Master merge for v0.10.2
2018-01-03 15:41:07 +00:00
Tom Bloor 4ee40b0732
Merge pull request #94 from Pear-Trading/Release-v0.10.2
Release v0.10.2
2018-01-03 14:24:11 +00:00
Thomas Bloor 40dfa1c102
Updated Changelog 2018-01-03 14:23:26 +00:00
Tom Bloor bef55a85d5
Merge pull request #93 from Pear-Trading/TBSliver/Org-Fairly-Trading
Added fairly trading for Organisations
2018-01-03 14:21:58 +00:00
Tom Bloor cc2360ab22
Add missed location updating in organisation edit on admin backend 2018-01-02 22:12:38 +00:00
Tom Bloor 500b61928b
Disable postgres tests while investigating unrelated issue to current
branch
2018-01-02 21:58:20 +00:00
Tom Bloor 0f9a3eadd8
Added error dump on failing test for debugging 2018-01-02 21:40:41 +00:00
Tom Bloor e0d3035929
Added testing with postgres to travis 2018-01-02 20:05:27 +00:00
Tom Bloor 8be16867bb
Added fairly trading org field and change org transactions to show
purchases not sales
2018-01-02 19:57:12 +00:00
Tom Bloor d4ff855251
Added is_fair column for fairly trading organisations 2018-01-02 19:56:48 +00:00
Tom Bloor d6d4121cb3
Fix booleans on sqlite ddl 2018-01-02 19:56:10 +00:00
Finn 67394ce8fa
Merge pull request #92 from Pear-Trading/master
Master merge to develop
2017-12-21 16:14:46 +00:00
Finn 0214ad4558
Merge pull request #91 from Pear-Trading/Release-v0.10.1
Release v0.10.1
2017-12-21 15:51:10 +00:00
Finn 3346dbd0d4 updated changelog 2017-12-21 15:39:34 +00:00
Finn 2673f79f98
Merge pull request #90 from Pear-Trading/master
Master merge for v0.10.0
2017-12-21 15:37:53 +00:00
Finn 2bbc1d508e
Merge pull request #89 from Pear-Trading/finn/loopdashboard
frontpage stats amended for sectors and weeks
2017-12-20 13:25:22 +00:00
Finn bb254cb278 frontpage stats amended for sectors and weeks 2017-12-19 18:03:15 +00:00
Finn 12364c2a17
Merge pull request #88 from Pear-Trading/finn/sectorU
added new sector
2017-12-19 18:01:55 +00:00
Finn cdde2f5ca8 added new sector 2017-12-18 16:19:10 +00:00
Finn 38f8883ab1
Merge pull request #87 from Pear-Trading/finn/applegacy
reverted fix and did different fix to order_by
2017-12-18 15:50:17 +00:00
Finn feeba76523 reverted fix and did different fix to order_by 2017-12-18 15:36:24 +00:00
Finn 9851ef5b6f
Merge pull request #86 from Pear-Trading/finn/applegacy
fixed counts for sqlite vs postgres
2017-12-18 15:20:17 +00:00
Finn 8e34f34bd6 added missing pg_or_sqlite sub 2017-12-18 15:08:15 +00:00
Finn cc0ca06470 fixed counts for sqlite vs postgres 2017-12-18 14:46:32 +00:00
Finn 4653f713fa
Merge pull request #85 from Pear-Trading/finn/applegacy
added support for mobile app back
2017-12-18 14:20:09 +00:00
Finn f1c1748b34 Fixed stats test 2017-12-18 14:05:51 +00:00
Finn 3914ec44a8 added support for mobile app back 2017-12-18 12:56:45 +00:00
Finn 2dceb4067e
Merge pull request #84 from Pear-Trading/finn/customerdashboard
revamped API for customer dashboard
2017-12-15 18:10:48 +00:00
Finn 5ce2f0beea changelog updated 2017-12-15 17:56:39 +00:00
Finn a89a320685 tests fixed 2017-12-15 17:52:02 +00:00
Finn 5dabc7a28a pie test added 2017-12-15 17:09:15 +00:00
Finn 74a1b3d238 sectors code added 2017-12-15 15:30:47 +00:00
Finn 229c2bb801 fixed stats test 2017-12-15 15:05:04 +00:00
Finn 88aa5becff pie code made functional and relevant distance code updated 2017-12-15 14:59:38 +00:00
Finn c4b7fa5102 Added new stats and fixed test 2017-12-14 20:30:44 +00:00
Finn b9a57c7dcb org graphs and relevant test fixed 2017-12-14 17:25:00 +00:00
Finn 60615e9b4a widget graph test fixed 2017-12-14 17:22:09 +00:00
Finn b2627eea4f fixing graph code and added placeholder pie code 2017-12-14 17:20:06 +00:00
Finn af95b31eb8 Fixed admin interface for org users 2017-12-13 15:29:35 +00:00
Finn f50c5e685c working graphs on frontend 2017-12-13 14:29:17 +00:00
Finn 8067d3cb96 made customer graphs work with passing test 2017-12-13 13:33:29 +00:00
Finn 8e1e9b2ec2 fixed snippets if no values to get 2017-12-12 17:31:05 +00:00
Finn 59dd053b45 Placeholder test added for graphs 2017-12-12 17:23:32 +00:00
Finn 3100fec233 added snippet endpoint and test 2017-12-12 17:21:32 +00:00
Finn b3f1a018b5 adding paths and placeholder for customer dash API 2017-12-12 13:32:52 +00:00
Tom Bloor 20ca523f1e
Merge pull request #83 from Pear-Trading/Release-v0.10.0
Release v0.10.0
2017-12-08 16:36:22 +00:00
Thomas Bloor 5b0f8bffab
Updated Changelog 2017-12-08 16:21:31 +00:00
Finn da320d250f
Merge pull request #82 from Pear-Trading/finn/esta
added ESTA and fixed association map code
2017-12-08 14:01:10 +00:00
Finn db95f239cd changelog updated 2017-12-08 13:30:21 +00:00
Finn e6c236e1ff schema and map code updated, test fixed 2017-12-08 13:19:22 +00:00
Finn fb25cbe773 esta added to schema and updated admin interface 2017-12-08 12:30:49 +00:00
Finn 0b6d823145
Merge pull request #81 from Pear-Trading/finn/linkcorrections
added link corrections
2017-12-08 11:27:44 +00:00
Finn 1e02062ca3 added new tab linking and corrected redirect 2017-12-04 12:30:10 +00:00
Finn 8b91c3b3c8 Revert "added new tab linking and corrected redirect"
This reverts commit 4eb5a4fd3f.
2017-12-04 12:29:34 +00:00
Finn 4eb5a4fd3f added new tab linking and corrected redirect 2017-12-04 12:27:53 +00:00
Finn 3868721324
Merge pull request #80 from Pear-Trading/finn/validationfix
Amended validation on orgs
2017-11-30 12:27:17 +00:00
Finn a95bfccdf9 Amended validation on orgs 2017-11-29 16:24:03 +00:00
Tom Bloor a14277ee48
Merge pull request #79 from Pear-Trading/master
Master merge for v0.9.7
2017-11-29 15:08:08 +00:00
Tom Bloor 738a72b64e
Merge pull request #78 from Pear-Trading/Release-v0.9.7
Release v0.9.7
2017-11-28 18:36:03 +00:00
Thomas Bloor 6e016641cd
Updated changelog for 0.9.7 2017-11-28 18:33:58 +00:00
Tom Bloor c8e876711c
Merge pull request #77 from Pear-Trading/TBSliver/Import-Fixes
Fix import screen
2017-11-28 18:31:55 +00:00
Tom Bloor 843037ea78
Merge branch 'development' into TBSliver/Import-Fixes 2017-11-28 17:52:56 +00:00
Finn 6c738993f4
Merge pull request #76 from Pear-Trading/finn/association
associations added
2017-11-27 17:30:39 +00:00
Finn 65cadb5d24 test fixed 2017-11-27 17:17:22 +00:00
Finn 9a71c1b7f5 extra info fixed and test amended and fixtures changed 2017-11-27 17:16:22 +00:00
Finn 9adf9668b1 added location info to main map 2017-11-27 11:36:21 +00:00
Finn b6397aedf4 changelog updated 2017-11-23 17:19:21 +00:00
Tom Bloor 0799d6eeb1 Updated changelog 2017-11-23 17:18:07 +00:00
Tom Bloor 1bc56016e7 Fix dev_data Command for testing 2017-11-23 16:52:52 +00:00
Tom Bloor 6d138af016 Fixed issue with Importset for users and orgs trying to much data for
the group by
2017-11-23 16:52:33 +00:00
Finn 460c9e6049 extra org data added 2017-11-23 16:35:45 +00:00
Finn 8ada3f86b6 fixes added and working admin interface 2017-11-23 16:25:14 +00:00
Finn 0072a97a3a endpoint and code added for LIS orgs 2017-11-23 15:42:37 +00:00
Finn 8c166464cc database upgraded and interface added 2017-11-23 14:54:59 +00:00
Finn d36091528f Entity Assoiciations added to database 2017-11-23 13:20:26 +00:00
Tom Bloor cb9c219b27
Merge pull request #75 from Pear-Trading/master
Master merge for v0.9.6
2017-11-22 11:38:34 +00:00
Tom Bloor cf83137f9f
Merge pull request #74 from Pear-Trading/Release-v0.9.6
Release v0.9.6
2017-11-21 15:49:07 +00:00
Tom Bloor fc0d2f6fa0
Fixed schema issue for import value foreign key being set wrong 2017-11-21 11:13:04 +00:00
Tom Bloor 13cd5ed950
Updated Changelog for v0.9.6 2017-11-21 11:07:03 +00:00
Tom Bloor 9c500ab24b
Merge pull request #73 from Pear-Trading/TBSliver/Log-Improvement
Minor logging improvements
2017-11-21 11:04:17 +00:00
Tom Bloor 951a0b233f
Fix minor issue in ddl for SQLite 2017-11-21 10:48:32 +00:00
Tom Bloor 9f4d39e029
Added logging on admin login endpoint 2017-11-21 10:42:23 +00:00
Tom Bloor addd5c640c
dded minor logging to API login endpoint 2017-11-21 10:40:22 +00:00
Tom Bloor 6d508cc432
Merge pull request #72 from Pear-Trading/TBSliver/Admin-Improvements
Major improvements to Admin Interface
2017-11-20 16:07:33 +00:00
Tom Bloor b0883b5522
Updated Changelog 2017-11-20 13:32:10 +00:00
Tom Bloor 9aaf3b4718
Added badges to users and pagination 2017-11-20 13:26:52 +00:00
Tom Bloor bccb292441
Added badge on organisations showing if they are a user or not 2017-11-20 13:02:07 +00:00
Tom Bloor 14410b475a
Added major merge code for merging organisations 2017-11-17 18:10:16 +00:00
Tom Bloor a0cdaac370
Refactored valid read template slightly and added merge link 2017-11-17 18:09:49 +00:00
Tom Bloor e02638ac58
Removed unused template 2017-11-17 18:09:08 +00:00
Tom Bloor 7155011841
Changed non-local-org badge to secondary colour in backend 2017-11-16 15:12:46 +00:00
Tom Bloor f49fb93658
Finishing off CSV import functionality 2017-11-15 18:22:49 +00:00
Tom Bloor 1015be7810
Allow for ignoring of values in import and toggle showing of them 2017-11-14 18:41:54 +00:00
Tom Bloor 7df6fecfc4
Added org lookup and assignment for import 2017-11-14 15:02:46 +00:00
Tom Bloor 37bc29829f
Refactored csv import flash errors 2017-11-14 12:57:28 +00:00
Tom Bloor cb7ab797f5 Merge branch 'development' into TBSliver/Admin-Improvements
Conflicts:
	CHANGELOG.md
2017-11-14 12:43:29 +00:00
Tom Bloor ead0dff76d
Merge pull request #71 from Pear-Trading/master
Master merge for 0.9.5
2017-11-14 12:42:00 +00:00
Tom Bloor 747a834156
Merge pull request #70 from Pear-Trading/Release-v0.9.5
Update for 0.9.5
2017-11-14 11:07:39 +00:00
Tom Bloor 233df00c7b Updated changelog for 0.9.5 2017-11-13 22:12:36 +00:00
Tom Bloor 594fc865eb
Merge pull request #69 from Pear-Trading/finn/appleaderboard
Web App Leaderboard endpoint
2017-11-13 11:48:37 +00:00
Finn 105c9093b8 fixes 2017-11-10 18:39:00 +00:00
Finn 049b4836c5 Added code to leaderboard web app API 2017-11-10 17:07:41 +00:00
Finn c4681ffc3a web app api leaderboard added 2017-11-10 16:45:58 +00:00
254 changed files with 57062 additions and 583 deletions

4
.gitignore vendored
View File

@ -2,9 +2,13 @@
myapp.conf
hypnotoad.pid
*.db
*.db-wal
*.db-shm
*.db-journal
*~
/images
*.swp
/upload
cover_db/
schema.png

7
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Default ignored files
/workspace.xml
/perl5local.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

13
.idea/Foodloop-Server.iml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<perl5>
<path value="$MODULE_DIR$/lib" type="perl-library" />
<path value="$MODULE_DIR$/templates" type="mojo-template" />
</perl5>
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

20
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="foodloop" uuid="7c088e2a-6667-4259-a2b3-9f440345d008">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/foodloop.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
<data-source source="LOCAL" name="PostgreSQL foodloop@localhost" uuid="8161e393-4db4-4e03-aa8b-7a961f14a591">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/</jdbc-url>
</data-source>
</component>
</project>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Foodloop-Server.iml" filepath="$PROJECT_DIR$/.idea/Foodloop-Server.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/share/ddl/PostgreSQL" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/share/ddl/SQLite" dialect="SQLite" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,13 +1,18 @@
sudo: false
language: perl
#addons:
# postgresql: "9.6"
perl:
- "5.20"
- "5.26"
env:
- HARNESS_PERL_SWITCHES="-MDevel::Cover"
install:
- cpanm --quiet --notest --installdeps .
- cpanm --quiet --notest --installdeps . #--with-feature=postgres .
- cpanm Devel::Cover
script:
- prove -lr
#- PEAR_TEST_PG=1 prove -lr
- cover

View File

@ -2,11 +2,119 @@
# Next Release
# v0.10.10
* Added proper minion job support
* **Admin Feature** Added importing of CSVs from Lancaster City Council
* Added pagination support to searching of organisations during transaction submission in API
# v0.10.9
* Removed sector list from dashboard stats and swapped it for category list
* Added fix to recurring transaction script
# v0.10.8
* Added yearly recurring payments
# v0.10.7
* Added `cron_daily` script for holding all daily cronjobs
* **Admin Fix** Parse currency without a currency symbol on import
* **Admin Fix** Fix large CSV issue on import
* Use custom secrets for encryption
* Made purchase categories easier to pull
* Added dashboard data for getting essential for all purchases along with
weekly and monthly view of category purchases
* Amended tests where relevant
# v0.10.6
* Fixed organisation submission
* Changed category listing code
* Made transaction upload code more lenient
* Added API ability to edit and delete transactions
* Added test for above
* Made test dumping more sane
* Fixed quantised transaction calcuations for weeks on sqlite
* Amended customer snippet, category list and customer stats tests
# v0.10.5
* **Admin Feature** Removed generic Transaction List, replaced with a new
transaction statistic viewing list
* **Admin Fix** Amended user view to have accordion
# v0.10.4
* Added API for category budget
* Added working test for the new API
* Added initial placeholder API for medals & user points being used in testing
* Added initial schema for medals
* Added essential flag to purchases in schema
* Amended upload API to account for essential purchases
* **Admin Feature** Added ability to view essential flag on purchases
* Made fixes to category viewing API
* Added schema for storing recurring purchases
* Amended Upload code to allow for if purchases are recurring
* Added script for checking recurring purchases and creating them if required
# v0.10.3
* Added Category and Transaction Category tables to DB
* Added API for categories in Transactions
* **Admin Feature** Added ability to add and delete categories
* **Admin Feature** Added ability to view transaction category
* Fixed all relevant tests to match
# v0.10.2
* Added fairly traded column for organisations
* **Admin Fix** Fix issue with setting location on Admin side
# v0.10.1
* Added API for customer graphs
* Revamped graphs code
* Added API for customer local purchase pie charts
* Added API for customer snippets
* Added API for sector purchase list for customer dashboard
* **Admin Fix** Fixed org sector on user edit layout and text
* **Admin Feature** Added Sector U
# v0.10.0
* **API Change** Updated API for story trail maps
* **Admin Feature** Improved links in relevant places to automatically open in
a new tab
* **Admin Feature** Ability to add ESTA to entity Added
* Trail map code updated
# v0.9.7
* **Admin Fix**: Fix error in Importing under Postgres
* **Admin Feature** Ability to add entity to LIS Added
* Added code endpoint for LIS organisations for web app use
* Schema updated to account for these changes
# v0.9.6
* **Admin Feature** Merged organisation lists into one list
* **Admin Feature** Paginated Organisation listings
* **Admin Feature** Added flags to Organisations listings
* **Admin Feature** Added `is_local` flag to Organisations to start categorising odd stores
* **Admin Feature** Feedback items now word wrap
* **Admin Feature** Rework transaction viewing
* **Admin Feature** Implemented import method for importing previous data from csv
* **Admin Feature** Added badges for various organisation flags eg. local, user, validated
* **Admin Feature** Enabled merging of organisations to reduce duplicates
* **Admin Feature** Added badges to user listing to show whether customer or organisation
* **Admin Feature** Added pagination to user listings
* Improved logging for debugging issues with login
# v0.9.5
* Added leaderboard api for web-app with pagination
* Location is now updated on registration. Customers location is truncated to 2
decimal places based on their postcode.
* Location is also updated on changing a users postcode

126
README.md
View File

@ -22,3 +22,129 @@ cpanm --installdeps . --with-feature postgres
PEAR_TEST_PG=1 prove -lr
```
# Minion
to set up minion support, you will need to create a database and user for
minion to connect to. In production his should be a PostgreSQL database,
however an SQLite db can be used in testing.
To use the SQLite version, run the following commands:
```
cpanm --installdeps --with-feature sqlite .
```
And then add the following to your configuration file:
```
minion => {
SQLite => 'sqlite:minion.db',
},
```
This will then use an SQLite db for the minion backend, using `minion.db` as
the database file. To start the minion itself, run:
```
./script/pear-local_loop minion worker
```
# Importing Ward Data
To import ward data, get the ward data csv and then run the following command:
```shell script
./script/pear-local_loop minion job \
--enqueue 'csv_postcode_import' \
--args '[ "/path/to/ward/csv" ]'
```
# Setting up Entity Postcodes
Assuming you have imported codepoint open, then to properly assign all
postcodes:
```shell script
./script/pear-local_loop minion job \
--enqueue entity_postcode_lookup
```
## Example PostgreSQL setup
```
# Example commands - probably not the best ones
# TODO come back and improve these with proper ownership and DDL rights
sudo -u postgres createuser minion
sudo -u postgres createdb localloop_minion
sudo -u postgres psql
psql=# alter user minion with encrypted password 'abc123';
psql=# grant all privileges on database localloop_minion to minion;
```
# Development
There are a couple of setup steps to getting a development environment ready.
Use the corresponding instructions depending on what state your current setup
is in.
## First Time Setup
First, decide if you're using SQLite or PostgreSQL locally. Development supports
both, however production uses PostgreSQL. For this example we will use SQLite.
As the default config is set up for this, no configuration changes are
needed initially. So, first off, install dependencies:
```shell script
cpanm --installdeps . --with-feature=sqlite
```
Then install the database:
```shell script
./script/deploy_db install -c 'dbi:SQLite:dbname=foodloop.db'
```
Then set up the development users:
```shell script
./script/pear-local_loop dev_data --force
```
***Note: do NOT run that script on production.***
Then you can start the application:
```shell script
morbo script/pear-local_loop -l http://*:3000
```
You can modify the host and port for listening as needed.
# Old Docs
## Local test database
To install a local DB:
```
./script/deploy_db install -c 'dbi:SQLite:dbname=foodloop.db'
```
To do an upgrade of it after making DB changes to commit:
```
./script/deploy_db write_ddl -c 'dbi:SQLite:dbname=foodloop.db'
./script/deploy_db upgrade -c 'dbi:SQLite:dbname=foodloop.db'
```
To redo leaderboards:
```
./script/pear-local_loop recalc_leaderboards
```
To serve a test version locally of the server:
```
morbo script/pear-local_loop
```

View File

@ -6,7 +6,6 @@ requires 'Mojo::JSON';
requires 'Email::Valid';
requires 'Geo::UK::Postcode::Regex' => '0.017';
requires 'Authen::Passphrase::BlowfishCrypt';
requires 'Time::Fake';
requires 'Scalar::Util';
requires 'DBIx::Class';
requires 'DBIx::Class::PassphraseColumn';
@ -15,7 +14,6 @@ requires 'DBIx::Class::Schema::Loader';
requires 'SQL::Translator';
requires 'DateTime';
requires 'DateTime::Format::Strptime', "1.73";
requires 'DateTime::Format::SQLite';
requires 'Try::Tiny';
requires 'MooX::Options::Actions';
requires 'Module::Runtime';
@ -24,6 +22,13 @@ requires 'DBIx::Class::Fixtures';
requires 'GIS::Distance';
requires 'Text::CSV';
requires 'Try::Tiny';
requires 'Throwable::Error';
requires 'Minion';
on 'test' => sub {
requires 'Test::More';
requires 'Test::MockTime';
};
feature 'schema-graph', 'Draw diagrams of Schema' => sub {
requires 'GraphViz';
@ -33,9 +38,15 @@ feature 'schema-graph', 'Draw diagrams of Schema' => sub {
feature 'postgres', 'PostgreSQL Support' => sub {
requires 'DBD::Pg';
requires 'Test::PostgreSQL';
requires 'Mojo::Pg';
requires 'DateTime::Format::Pg';
};
feature 'sqlite', 'SQLite Support' => sub {
requires 'Minion::Backend::SQLite';
requires 'DateTime::Format::SQLite';
};
feature 'codepoint-open', 'Code Point Open manipulation' => sub {
requires 'Geo::UK::Postcode::CodePointOpen';
};

View File

@ -26,6 +26,7 @@ sub startup {
$self->plugin('Config', {
default => {
storage_path => tempdir,
upload_path => $self->home->child('upload'),
sessionTimeSeconds => 60 * 60 * 24 * 7,
sessionTokenJsonName => 'session_key',
sessionExpiresJsonName => 'sessionExpires',
@ -34,13 +35,22 @@ sub startup {
});
my $config = $self->config;
if ( defined $config->{secret} ) {
$self->secrets([ $config->{secret} ]);
} elsif ( $self->mode eq 'production' ) {
# Just incase we end up in production and it hasnt been set!
$self->secrets([ Data::UUID->new->create() ]);
}
push @{ $self->commands->namespaces }, __PACKAGE__ . '::Command';
$self->plugin('Pear::LocalLoop::Plugin::BootstrapPagination', { bootstrap4 => 1 } );
$self->plugin('Pear::LocalLoop::Plugin::Validators');
$self->plugin('Pear::LocalLoop::Plugin::Datetime');
$self->plugin('Pear::LocalLoop::Plugin::Currency');
$self->plugin('Pear::LocalLoop::Plugin::Postcodes');
$self->plugin('Pear::LocalLoop::Plugin::TemplateHelpers');
$self->plugin('Pear::LocalLoop::Plugin::Minion');
$self->plugin('Authentication' => {
'load_user' => sub {
@ -143,19 +153,32 @@ sub startup {
});
$api->post('/upload')->to('api-upload#post_upload');
$api->post('/search')->to('api-upload#post_search');
$api->post('/search/category')->to('api-upload#post_category');
$api->post('/user')->to('api-user#post_account');
$api->post('/user/account')->to('api-user#post_account_update');
$api->post('/user-history')->to('api-user#post_user_history');
$api->post('/stats')->to('api-stats#post_index');
$api->post('/stats/category')->to('api-categories#post_category_list');
$api->post('/stats/customer')->to('api-stats#post_customer');
$api->post('/stats/organisation')->to('api-stats#post_organisation');
$api->post('/stats/leaderboard')->to('api-stats#post_leaderboards');
$api->post('/stats/leaderboard/paged')->to('api-stats#post_leaderboards_paged');
$api->post('/outgoing-transactions')->to('api-transactions#post_transaction_list_purchases');
$api->post('/recurring-transactions')->to('api-transactions#update_recurring');
$api->post('/recurring-transactions/delete')->to('api-transactions#delete_recurring');
my $api_v1 = $api->under('/v1');
my $api_v1_user = $api_v1->under('/user');
$api_v1_user->post('/medals')->to('api-v1-user-medals#index');
$api_v1_user->post('/points')->to('api-v1-user-points#index');
my $api_v1_supplier = $api_v1->under('/supplier');
$api_v1_supplier->post('/location')->to('api-v1-supplier-location#index');
$api_v1_supplier->post('/location/trail')->to('api-v1-supplier-location#trail_load');
my $api_v1_org = $api_v1->under('/organisation')->to('api-v1-organisation#auth');
@ -168,8 +191,29 @@ sub startup {
$api_v1_org->post('/employee')->to('api-organisation#post_employee_read');
$api_v1_org->post('/employee/add')->to('api-organisation#post_employee_add');
$api_v1_org->post('/external/transactions')->to('api-external#post_lcc_transactions');
$api_v1_org->post('/external/suppliers')->to('api-external#post_lcc_suppliers');
$api_v1_org->post('/external/year_spend')->to('api-external#post_year_spend');
$api_v1_org->post('/external/supplier_count')->to('api-external#post_supplier_count');
$api_v1_org->post('/external/supplier_history')->to('api-external#post_supplier_history');
$api_v1_org->post('/external/lcc_tables')->to('api-external#post_lcc_table_summary');
$api_v1_org->post('/pies')->to('api-v1-organisation-pies#index');
my $api_v1_cust = $api_v1->under('/customer')->to('api-v1-customer#auth');
$api_v1_cust->post('/graphs')->to('api-v1-customer-graphs#index');
$api_v1_cust->post('/snippets')->to('api-v1-customer-snippets#index');
$api_v1_cust->post('/pies')->to('api-v1-customer-pies#index');
my $admin_routes = $r->under('/admin')->to('admin#under');
if ( defined $config->{minion} ) {
$self->plugin( 'Minion::Admin' => {
return_to => '/admin/home',
route => $admin_routes->any('/minion'),
} );
}
$admin_routes->get('/home')->to('admin#home');
$admin_routes->get('/tokens')->to('admin-tokens#index');
@ -178,6 +222,12 @@ sub startup {
$admin_routes->post('/tokens/:id')->to('admin-tokens#update');
$admin_routes->post('/tokens/:id/delete')->to('admin-tokens#delete');
$admin_routes->get('/categories')->to('admin-categories#index');
$admin_routes->post('/categories')->to('admin-categories#create');
$admin_routes->get('/categories/:id')->to('admin-categories#read');
$admin_routes->post('/categories/:id')->to('admin-categories#update');
$admin_routes->post('/categories/:id/delete')->to('admin-categories#delete');
$admin_routes->get('/users')->to('admin-users#index');
$admin_routes->get('/users/:id')->to('admin-users#read');
$admin_routes->post('/users/:id')->to('admin-users#update');
@ -188,6 +238,9 @@ sub startup {
$admin_routes->post('/organisations/add')->to('admin-organisations#add_org_submit');
$admin_routes->get('/organisations/:id')->to('admin-organisations#valid_read');
$admin_routes->post('/organisations/:id')->to('admin-organisations#valid_edit');
$admin_routes->get('/organisations/:id/merge')->to('admin-organisations#merge_list');
$admin_routes->get('/organisations/:id/merge/:target_id')->to('admin-organisations#merge_detail');
$admin_routes->post('/organisations/:id/merge/:target_id')->to('admin-organisations#merge_confirm');
$admin_routes->get('/feedback')->to('admin-feedback#index');
$admin_routes->get('/feedback/:id')->to('admin-feedback#read');
@ -206,10 +259,16 @@ sub startup {
$admin_routes->get('/import/:set_id')->to('admin-import#list');
$admin_routes->get('/import/:set_id/user')->to('admin-import#get_user');
$admin_routes->get('/import/:set_id/org')->to('admin-import#get_org');
$admin_routes->post('/import/:set_id/org')->to('admin-import#set_org');
$admin_routes->get('/import/:set_id/:value_id')->to('admin-import#get_value');
$admin_routes->post('/import/:set_id/:value_id')->to('admin-import#post_value');
$admin_routes->get('/import/:set_id/ignore/:value_id')->to('admin-import#ignore_value');
$admin_routes->get('/import/:set_id/import')->to('admin-import#run_import');
$admin_routes->get('/import_from')->to('admin-import_from#index');
$admin_routes->post('/import_from/suppliers')->to('admin-import_from#post_suppliers');
$admin_routes->post('/import_from/transactions')->to('admin-import_from#post_transactions');
$admin_routes->post('/import_from/postcodes')->to('admin-import_from#post_postcodes');
$admin_routes->get('/import_from/org_search')->to('admin-import_from#org_search');
# my $user_routes = $r->under('/')->to('root#under');
# $user_routes->get('/home')->to('root#home');
@ -220,9 +279,9 @@ sub startup {
# $portal_api->post('/search')->to('api-upload#post_search');
$self->hook( before_dispatch => sub {
my $self = shift;
my $c = shift;
$self->res->headers->header('Access-Control-Allow-Origin' => '*') if $self->app->mode eq 'development';
$c->res->headers->header('Access-Control-Allow-Origin' => '*') if $c->app->mode eq 'development';
});
$self->helper( copy_transactions_and_delete => sub {

View File

@ -22,7 +22,12 @@ sub run {
unless ( -d $output_dir ) {
print "Unzipping code-point-open data\n" unless $quiet_mode;
system( 'unzip', '-q', $zip_file, '-d', $output_dir );
eval { system( 'unzip', '-q', $zip_file, '-d', $output_dir ) };
if ( my $err = $@ ) {
print "Error extracting zip: " . $err . "\n";
print "Manually create etc/code-point-open/codepo_gb directory and extract zip into it";
die;
}
}
my $cpo = Geo::UK::Postcode::CodePointOpen->new( path => $output_dir );

View File

@ -28,45 +28,57 @@ sub run {
$schema->resultset('User')->create({
email => 'test@example.com',
password => 'abc123',
customer => {
full_name => 'Test User',
display_name => 'Test User',
year_of_birth => 2006,
postcode => 'LA1 1AA',
entity => {
type => 'customer',
customer => {
full_name => 'Test User',
display_name => 'Test User',
year_of_birth => 2006,
postcode => 'LA1 1AA',
}
},
administrator => {},
is_admin => 1,
});
$schema->resultset('User')->create({
email => 'test2@example.com',
password => 'abc123',
customer => {
full_name => 'Test User 2',
display_name => 'Test User 2',
year_of_birth => 2006,
postcode => 'LA1 1AA',
entity => {
type => 'customer',
customer => {
full_name => 'Test User 2',
display_name => 'Test User 2',
year_of_birth => 2006,
postcode => 'LA1 1AA',
},
},
});
$schema->resultset('User')->create({
email => 'test3@example.com',
password => 'abc123',
customer => {
full_name => 'Test User 3',
display_name => 'Test User 3',
year_of_birth => 2006,
postcode => 'LA1 1AA',
entity => {
type => 'customer',
customer => {
full_name => 'Test User 3',
display_name => 'Test User 3',
year_of_birth => 2006,
postcode => 'LA1 1AA',
},
},
});
$schema->resultset('User')->create({
email => 'testorg@example.com',
password => 'abc123',
organisation => {
name => 'Test Org',
street_name => 'Test Street',
town => 'Lancaster',
postcode => 'LA1 1AA',
entity => {
type => 'organisation',
organisation => {
name => 'Test Org',
street_name => 'Test Street',
town => 'Lancaster',
postcode => 'LA1 1AA',
},
},
});
}

View File

@ -0,0 +1,140 @@
package Pear::LocalLoop::Command::recur_transactions;
use Mojo::Base 'Mojolicious::Command';
use Mojo::Util 'getopt';
use DateTime;
use DateTime::Format::Strptime;
has description => 'Recur Transactions';
has usage => sub { shift->extract_usage };
sub run {
my ( $self, @args ) = @_;
my $app = $self->app;
getopt \@args,
'f|force' => \my $force,
'd|date=s' => \my $date;
unless ( defined $force ) {
say "Will not do anything without force option";
return;
}
my $date_formatter = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%d'
);
my $datetime;
if ( defined $date ) {
$datetime = $date_formatter->parse_datetime($date);
unless ( defined $datetime ) {
say "Unrecognised date format, please use 'YYYY-MM-DD' Format";
return;
}
} else {
$datetime = DateTime->today;
}
my $match_date_day = $app->format_iso_date($datetime->clone->subtract( days => 1 ));
my $match_date_week = $app->format_iso_date($datetime->clone->subtract( weeks => 1 ));
my $match_date_fortnight = $app->format_iso_date($datetime->clone->subtract( weeks => 2 ));
my $match_date_month = $app->format_iso_date($datetime->clone->subtract( months => 1 ));
my $match_date_quarter = $app->format_iso_date($datetime->clone->subtract( months => 3));
my $match_date_year = $app->format_iso_date($datetime->clone->subtract( years => 1 ));
my $schema = $app->schema;
my $dtf = $schema->storage->datetime_parser;
my $recur_rs = $schema->resultset('TransactionRecurring');
for my $recur_result ( $recur_rs->all ) {
my $start_time_dt;
if ( defined $recur_result->last_updated ) {
$start_time_dt = $recur_result->last_updated;
} else {
$start_time_dt = $recur_result->start_time;
}
my $start_time = $app->format_iso_date($start_time_dt);
my $recurring_period = $recur_result->recurring_period;
if ( $recurring_period eq 'daily' ) {
next unless $start_time eq $match_date_day;
say "matched recurring transaction ID " . $recur_result->id . " to daily";
} elsif ( $recurring_period eq 'weekly' ) {
next unless $start_time eq $match_date_week;
say "matched recurring transaction ID " . $recur_result->id . " to weekly";
} elsif ( $recurring_period eq 'fortnightly' ) {
next unless $start_time eq $match_date_fortnight;
say "matched recurring transaction ID " . $recur_result->id . " to fortnightly";
} elsif ( $recurring_period eq 'monthly' ) {
next unless $start_time eq $match_date_month;
say "matched recurring transaction ID " . $recur_result->id . " to monthly";
} elsif ( $recurring_period eq 'quarterly' ) {
next unless $start_time eq $match_date_quarter;
say "matched recurring transaction ID " . $recur_result->id . " to quarterly";
} elsif ( $recurring_period eq 'yearly' ) {
next unless $start_time eq $match_date_year;
say "matched recurring transaction ID " . $recur_result->id . " to yearly";
} else {
say "Invalid recurring time period given";
return;
}
my $purchase_time = DateTime->new(
year => $datetime->year,
month => $datetime->month,
day => $datetime->day,
hour => $start_time_dt->hour,
minute => $start_time_dt->minute,
second => $start_time_dt->second,
time_zone => 'UTC',
);
my $category = $recur_result->category_id;
my $essential = $recur_result->essential;
my $distance = $recur_result->distance;
my $new_transaction = $schema->resultset('Transaction')->create({
buyer_id => $recur_result->buyer_id,
seller_id => $recur_result->seller_id,
value => $recur_result->value,
purchase_time => $app->format_db_datetime($purchase_time),
distance => $distance,
essential => ( defined $essential ? $essential : 0 ),
});
unless ( defined $new_transaction ) {
say "Error Adding Transaction";
return;
}
if ( defined $category ) {
$schema->resultset('TransactionCategory')->create({
category_id => $category,
transaction_id => $new_transaction->id,
});
}
$recur_result->update({ last_updated => $purchase_time });
}
}
=head1 SYNOPSIS
Usage: APPLICATION recur_transactions [OPTIONS]
Options:
-f, --force Actually insert the data
-d, --date Date to recur the transactions on
=cut
1;

View File

@ -38,9 +38,12 @@ sub home {
sub auth_login {
my $c = shift;
$c->app->log->debug( __PACKAGE__ . " admin login attempt for [" . $c->param('email') . "]" );
if ( $c->authenticate($c->param('email'), $c->param('password')) ) {
$c->redirect_to('/admin/home');
} else {
$c->app->log->info( __PACKAGE__ . " failed admin login for [" . $c->param('email') . "]" );
$c->redirect_to('/admin');
}
}

View File

@ -0,0 +1,100 @@
package Pear::LocalLoop::Controller::Admin::Categories;
use Mojo::Base 'Mojolicious::Controller';
has result_set => sub {
my $c = shift;
return $c->schema->resultset('Category');
};
sub index {
my $c = shift;
my $category_rs = $c->result_set;
$category_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
$c->stash( categories => [ $category_rs->all ] );
}
# POST
sub create {
my $c = shift;
my $validation = $c->validation;
$validation->required('category', 'trim')->not_in_resultset('name', $c->result_set);
my $category_name = $validation->param('category');
if ( $validation->has_error ) {
my $check = shift @{ $c->validation->error('category') };
if ( $check eq 'required' ) {
$c->flash( error => 'Category name is required' );
} elsif ( $check eq 'like' ) {
$c->flash( error => 'Category name not valid - Alphanumeric characters and Underscore only' );
} elsif ( $check eq 'not_in_resultset' ) {
$c->flash( error => 'Category Already Exists' );
}
} else {
$c->flash( success => 'Category Created' );
$c->result_set->create({ name => $category_name });
}
$c->redirect_to( '/admin/categories' );
}
# GET
sub read {
my $c = shift;
my $id = $c->param('id');
if ( my $category = $c->result_set->find($id) ) {
$c->stash( category => $category );
} else {
$c->flash( error => 'No Category found' );
$c->redirect_to( '/admin/categories' );
}
}
# POST
sub update {
my $c = shift;
my $validation = $c->validation;
$validation->required('id');
$validation->required('category', 'trim')->like(qr/^[\w]*$/);
$validation->optional('line_icon');
my $id = $c->param('id');
if ( $validation->has_error ) {
my $names = $validation->failed;
$c->flash( error => 'Error in submitted data: ' . join(', ', @$names) );
$c->redirect_to( '/admin/categories/' . $id );
} elsif ( my $category = $c->result_set->find($id) ) {
$category->update({
id => $validation->param('id'),
name => $validation->param('category'),
line_icon => (defined $validation->param('line_icon') ? $validation->param('line_icon') : undef ),
});
$c->flash( success => 'Category Updated' );
$c->redirect_to( '/admin/categories/' . $validation->param('id') );
} else {
$c->flash( error => 'No Category found' );
$c->redirect_to( '/admin/categories' );
}
}
# DELETE
sub delete {
my $c = shift;
my $id = $c->param('id');
if ( my $category = $c->result_set->find($id) ) {
$category->transaction_category->delete;
$category->delete;
$c->flash( success => 'Category Deleted' );
} else {
$c->flash( error => 'No Category found' );
}
$c->redirect_to( '/admin/categories' );
}
1;

View File

@ -27,10 +27,13 @@ sub list {
my $c = shift;
my $set_id = $c->param('set_id');
my $include_ignored = $c->param('ignored');
my $include_imported = $c->param('imported');
my $import_set = $c->result_set->find($set_id);
my $import_value_rs = $c->result_set->get_values($set_id);
my $import_users_rs = $c->result_set->get_users($set_id);
my $import_org_rs = $c->result_set->get_orgs($set_id);
my $import_value_rs = $c->result_set->get_values($set_id, $include_ignored, $include_imported);
my $import_users_rs = $c->result_set->get_users($set_id, $include_ignored, $include_imported);
my $import_org_rs = $c->result_set->get_orgs($set_id, $include_ignored, $include_imported);
my $import_lookup_rs = $c->result_set->get_lookups($set_id);
$c->stash(
@ -69,7 +72,7 @@ sub post_add {
};
if ( defined $error ) {
$c->flash( error => $error, csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( $error );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -78,7 +81,7 @@ sub post_add {
my @required = grep {/^user$|^value$|^date$|^organisation$/} @csv_headers;
unless ( scalar( @required ) == 4 ) {
$c->flash( error => 'Required columns not available', csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( 'Required columns not available' );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -86,7 +89,7 @@ sub post_add {
my $csv_output = $csv->getline_hr_all( $fh );
unless ( scalar( @$csv_output ) ) {
$c->flash( error => "No data found", csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( "No data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -94,7 +97,7 @@ sub post_add {
for my $data ( @$csv_output ) {
for my $key ( qw/ user value organisation / ) {
unless ( defined $data->{$key} ) {
$c->flash( error => "Undefined [$key] data found", csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( "Undefined [$key] data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -103,7 +106,7 @@ sub post_add {
my $dtp = DateTime::Format::Strptime->new( pattern => $date_format );
my $dt_obj = $dtp->parse_datetime($data->{date});
unless ( defined $dt_obj ) {
$c->flash( error => "Undefined or incorrect format for [date] data found", csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( "Undefined or incorrect format for [date] data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -126,7 +129,7 @@ sub post_add {
);
unless ( defined $value_set ) {
$c->flash( error => 'Error creating new Value Set', csv_data => $csv_data, date_format => $date_format );
$c->_csv_flash_error( 'Error creating new Value Set' );
$c->redirect_to( '/admin/import/add' );
return;
}
@ -135,6 +138,18 @@ sub post_add {
$c->redirect_to( '/admin/import/' . $value_set->id );
}
sub _csv_flash_error {
my ( $c, $error ) = @_;
$error //= "An error occurred";
$c->flash(
error => $error,
# If csv info is huge, this fails epically
#csv_data => $c->param('csv'),
date_format => $c->param('date_format'),
);
}
sub get_user {
my $c = shift;
my $set_id = $c->param('set_id');
@ -184,22 +199,128 @@ sub get_user {
sub get_org {
my $c = shift;
my $set_id = $c->param('set_id');
my $org_name = $c->param('org');
my $values_rs = $c->result_set->find($set_id)->values->search(
{
org_name => $org_name,
ignore_value => 0,
}
);
unless ( $values_rs->count > 0 ) {
$c->flash( error => 'Organisation not found or all values are ignored' );
return $c->redirect_to( '/admin/import/' . $set_id );
}
my $lookup_result = $c->result_set->find($set_id)->lookups->find(
{ name => $org_name },
);
my $entity_id = $c->param('entity');
my $orgs_rs = $c->schema->resultset('Organisation');
if ( defined $entity_id && $orgs_rs->find({ entity_id => $entity_id }) ) {
if ( defined $lookup_result ) {
$lookup_result->update({ entity_id => $entity_id });
} else {
$lookup_result = $c->result_set->find($set_id)->lookups->create(
{
name => $org_name,
entity_id => $entity_id,
},
);
}
} elsif ( defined $entity_id ) {
$c->stash( error => "Organisation does not exist" );
}
$c->stash(
orgs_rs => $orgs_rs,
lookup => $lookup_result,
org_name => $org_name,
);
}
sub set_org {
my $c = shift;
}
sub get_value {
sub ignore_value {
my $c = shift;
my $set_id = $c->param('set_id');
my $value_id = $c->param('value_id');
my $set_result = $c->result_set->find($set_id);
unless ( defined $set_result ) {
$c->flash( error => "Set does not exist" );
return $c->redirect_to( '/admin/import' );
}
my $value_result = $set_result->values->find($value_id);
unless ( defined $value_result ) {
$c->flash( error => "Value does not exist" );
return $c->redirect_to( '/admin/import/' . $set_id );
}
$value_result->update({ ignore_value => $value_result->ignore_value ? 0 : 1 });
$c->flash( success => "Updated value" );
my $referer = $c->req->headers->header('Referer');
return $c->redirect_to(
defined $referer
? $c->url_for($referer)->path_query
: '/admin/import/' . $set_id
);
}
sub post_value {
sub run_import {
my $c = shift;
my $set_id = $c->param('set_id');
my $set_result = $c->result_set->find($set_id);
unless ( defined $set_result ) {
$c->flash( error => "Set does not exist" );
return $c->redirect_to( '/admin/import' );
}
my $import_value_rs = $c->result_set->get_values($set_id, undef, undef);
my $import_lookup = $c->result_set->get_lookups($set_id);
my $entity_rs = $c->schema->resultset('Entity');
$c->schema->txn_do(
sub {
for my $value_result ( $import_value_rs->all ) {
my $user_lookup = $import_lookup->{ $value_result->user_name };
my $org_lookup = $import_lookup->{ $value_result->org_name };
my $value_lookup = $c->parse_currency( $value_result->purchase_value );
if ( defined $user_lookup && defined $org_lookup && $value_lookup ) {
my $user_entity = $entity_rs->find($user_lookup->{entity_id});
my $org_entity = $entity_rs->find($org_lookup->{entity_id});
my $distance = $c->get_distance_from_coords( $user_entity->type_object, $org_entity->type_object );
my $transaction = $c->schema->resultset('Transaction')->create(
{
buyer => $user_entity,
seller => $org_entity,
value => $value_lookup * 100000,
purchase_time => $value_result->purchase_date,
distance => $distance,
}
);
$value_result->update({transaction_id => $transaction->id });
} else {
$c->app->log->warn("Failed value import for value id [" . $value_result->id . "], ignoring");
}
}
}
);
$c->flash( success => "Import completed for ready values" );
my $referer = $c->req->headers->header('Referer');
return $c->redirect_to(
defined $referer
? $c->url_for($referer)->path_query
: '/admin/import/' . $set_id
);
}
1;

View File

@ -0,0 +1,127 @@
package Pear::LocalLoop::Controller::Admin::ImportFrom;
use Mojo::Base 'Mojolicious::Controller';
use Moo;
use Try::Tiny;
use Mojo::File qw/path/;
sub index {
my $c = shift;
$c->stash->{org_entities} = [
map {
{ id => $_->entity_id, name => $_->name }
} $c->schema->resultset('Organisation')->search({ name => { like => '%lancashire%' }}, { columns => [qw/ entity_id name / ]})
];
$c->app->max_request_size(104857600);
}
sub post_suppliers {
my $c = shift;
unless ($c->param('suppliers_csv')) {
$c->flash(error => "No CSV file given");
return $c->redirect_to('/admin/import_from');
}
# Check file size
if ($c->req->is_limit_exceeded) {
$c->flash(error => "CSV file size is too large");
return $c->redirect_to('/admin/import_from');
}
my $file = $c->param('suppliers_csv');
my $filename = path($c->app->config->{upload_path}, time . 'suppliers.csv');
$file->move_to($filename);
my $job_id = $c->minion->enqueue('csv_supplier_import' => [ $filename ]);
my $job_url = $c->url_for("/admin/minion/jobs?id=$job_id")->to_abs;
$c->flash(success => "CSV import started, see status of minion job at: $job_url");
return $c->redirect_to('/admin/import_from');
}
sub post_postcodes {
my $c = shift;
unless ($c->param('postcodes_csv')) {
$c->flash(error => "No CSV file given");
return $c->redirect_to('/admin/import_from');
}
# Check file size
if ($c->req->is_limit_exceeded) {
$c->flash(error => "CSV file size is too large");
return $c->redirect_to('/admin/import_from');
}
my $file = $c->param('postcodes_csv');
my $filename = path($c->app->config->{upload_path}, time . 'postcodes.csv');
$file->move_to($filename);
my $job_id = $c->minion->enqueue('csv_postcode_import' => [ $filename ]);
my $job_url = $c->url_for("/admin/minion/jobs?id=$job_id")->to_abs;
$c->flash(success => "CSV import started, see status of minion job at: $job_url");
return $c->redirect_to('/admin/import_from');
}
sub post_transactions {
my $c = shift;
unless ($c->param('entity_id') ne '') {
$c->flash(error => "Please Choose an organisation");
return $c->redirect_to('/admin/import_from');
}
unless ($c->param('transactions_csv')) {
$c->flash(error => "No CSV file given");
return $c->redirect_to('/admin/import_from');
}
# Check file size
if ($c->req->is_limit_exceeded) {
$c->flash(error => "CSV file size is too large");
return $c->redirect_to('/admin/import_from');
}
my $file = $c->param('transactions_csv');
my $filename = path($c->app->config->{upload_path}, time . 'transactions.csv');
$file->move_to($filename);
my $job_id = $c->minion->enqueue('csv_transaction_import' => [ $filename, $c->param('entity_id') ]);
my $job_url = $c->url_for("/admin/minion/jobs?id=$job_id")->to_abs;
$c->flash(success => "CSV import started, see status of minion job at: $job_url");
return $c->redirect_to('/admin/import_from');
}
sub org_search {
my $c = shift;
my $term = $c->param('term');
my $rs = $c->schema->resultset('Organisation')->search(
{ name => { like => $term . '%' } },
{
join => 'entity',
columns => [ qw/ me.name entity.id / ]
},
);
my @results = ( map { {
label => $_->name,
value => $_->entity->id,
} } $rs->all);
$c->render( json => \@results );
}
1;

View File

@ -3,6 +3,11 @@ use Mojo::Base 'Mojolicious::Controller';
use Try::Tiny;
has result_set => sub {
my $c = shift;
return $c->schema->resultset('Organisation');
};
sub list {
my $c = shift;
@ -36,6 +41,7 @@ sub add_org_submit {
$validation->optional('postcode')->postcode;
$validation->optional('pending');
$validation->optional('is_local');
$validation->optional('is_fair');
if ( $validation->has_error ) {
$c->flash( error => 'The validation has failed' );
@ -44,6 +50,11 @@ sub add_org_submit {
my $organisation;
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
'organisation',
);
try {
my $entity = $c->schema->resultset('Entity')->create({
organisation => {
@ -52,9 +63,11 @@ sub add_org_submit {
town => $validation->param('town'),
sector => $validation->param('sector'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ),
submitted_by_id => $c->current_user->id,
pending => defined $validation->param('pending') ? 0 : 1,
is_local => $validation->param('is_local'),
is_fair => $validation->param('is_fair'),
},
type => 'organisation',
});
@ -73,16 +86,23 @@ sub add_org_submit {
sub valid_read {
my $c = shift;
my $valid_org = $c->schema->resultset('Organisation')->find( $c->param('id') );
my $transactions = $valid_org->entity->sales->search(
my $transactions = $valid_org->entity->purchases->search(
undef, {
page => $c->param('page') || 1,
rows => 10,
order_by => { -desc => 'submitted_at' },
},
);
my $associations = $valid_org->entity->associations;
my $assoc = {
lis => defined $associations ? $associations->lis : 0,
esta => defined $associations ? $associations->esta : 0,
};
$c->stash(
valid_org => $valid_org,
transactions => $transactions,
associations => $assoc,
);
}
@ -91,12 +111,15 @@ sub valid_edit {
my $validation = $c->validation;
$validation->required('name');
$validation->required('street_name');
$validation->optional('street_name');
$validation->required('town');
$validation->optional('sector');
$validation->required('postcode')->postcode;
$validation->optional('pending');
$validation->optional('is_local');
$validation->optional('is_fair');
$validation->optional('is_lis');
$validation->optional('is_esta');
if ( $validation->has_error ) {
$c->flash( error => 'The validation has failed' );
@ -105,6 +128,11 @@ sub valid_edit {
my $valid_org = $c->schema->resultset('Organisation')->find( $c->param('id') );
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
'organisation',
);
try {
$c->schema->storage->txn_do( sub {
$valid_org->update({
@ -113,18 +141,124 @@ sub valid_edit {
town => $validation->param('town'),
sector => $validation->param('sector'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ),
pending => defined $validation->param('pending') ? 0 : 1,
is_local => $validation->param('is_local'),
is_fair => $validation->param('is_fair'),
});
$valid_org->entity->update_or_create_related( 'associations', {
lis => $validation->param('is_lis'),
esta => $validation->param('is_esta')
});
} );
} finally {
if ( @_ ) {
if ( @_ ) {use Devel::Dwarn; Dwarn \@_;
$c->flash( error => 'Something went wrong Updating the Organisation' );
} else {
$c->flash( success => 'Updated Organisation' );
}
};
$c->redirect_to( '/admin/organisations/');
$c->redirect_to( '/admin/organisations/' . $c->param('id') );
}
sub merge_list {
my $c = shift;
my $org_id = $c->param('id');
my $org_result = $c->result_set->find($org_id);
if ( defined $org_result->entity->user ) {
$c->flash( error => 'Cannot merge from user-owned organisation!' );
$c->redirect_to( '/admin/organisations/' . $org_id );
return;
}
my $org_rs = $c->result_set->search(
{
id => { '!=' => $org_id },
},
{
page => $c->param('page') || 1,
rows => 10,
order_by => { '-asc' => 'name' },
}
);
$c->stash(
org_result => $org_result,
org_rs => $org_rs,
);
}
sub merge_detail {
my $c = shift;
my $org_id = $c->param('id');
my $org_result = $c->result_set->find($org_id);
if ( defined $org_result->entity->user ) {
$c->flash( error => 'Cannot merge from user-owned organisation!' );
$c->redirect_to( '/admin/organisations/' . $org_id );
return;
}
my $target_id = $c->param('target_id');
my $target_result = $c->result_set->find($target_id);
unless ( defined $target_result ) {
$c->flash( error => 'Unknown target organisation' );
$c->redirect_to( '/admin/organisations/' . $org_id . '/merge' );
return;
}
$c->stash(
org_result => $org_result,
target_result => $target_result,
);
}
sub merge_confirm {
my $c = shift;
my $org_id = $c->param('id');
my $org_result = $c->result_set->find($org_id);
if ( defined $org_result->entity->user ) {
$c->flash( error => 'Cannot merge from user-owned organisation!' );
$c->redirect_to( '/admin/organisations/' . $org_id );
return;
}
my $target_id = $c->param('target_id');
my $target_result = $c->result_set->find($target_id);
my $confirm = $c->param('confirm');
if ( $confirm eq 'checked' && defined $org_result && defined $target_result ) {
try {
$c->schema->txn_do( sub {
# Done as an update, not update_all, so its damn fast - we're only
# editing an id which is guaranteed to be an integer here, and this
# makes it only one update statement.
$org_result->entity->sales->update(
{ seller_id => $target_result->entity->id }
);
my $count = $org_result->entity->sales->count;
die "Failed to migrate all sales" if $count;
$org_result->entity->delete;
$c->schema->resultset('ImportLookup')->search({ entity_id => $org_result->entity->id })->delete;
my $org_count = $c->result_set->search({id => $org_result->id })->count;
my $entity_count = $c->schema->resultset('Entity')->search({id => $org_result->entity->id })->count;
die "Failed to remove org" if $org_count;
die "Failed to remove entity" if $entity_count;
});
} catch {
$c->app->log->warn($_);
};
$c->flash( error => 'Engage' );
} else {
$c->flash( error => 'You must tick the confirmation box to proceed' );
}
$c->redirect_to( '/admin/organisations/' . $org_id . '/merge/' . $target_id );
}
1;

View File

@ -1,6 +1,8 @@
package Pear::LocalLoop::Controller::Admin::Transactions;
use Mojo::Base 'Mojolicious::Controller';
use List::Util qw/ max sum /;
has result_set => sub {
my $c = shift;
return $c->schema->resultset('Transaction');
@ -9,15 +11,49 @@ has result_set => sub {
sub index {
my $c = shift;
my $transactions = $c->result_set->search(
undef, {
page => $c->param('page') || 1,
rows => 10,
order_by => { -desc => 'submitted_at' },
},
my $pending_transaction_rs = $c->schema->resultset('Organisation')->search({ pending => 1 })->entity->sales;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $week_transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{},
{
select => [
{ count => 'me.value', '-as' => 'count' },
{ sum => 'me.value', '-as' => 'sum_value' },
'quantised_weeks',
],
group_by => 'quantised_weeks',
order_by => { '-asc' => 'quantised_weeks' },
}
);
my @all_weeks = $week_transaction_rs->all;
my $first_week_count = defined $all_weeks[0] ? $all_weeks[0]->get_column('count') || 0 : 0;
my $first_week_value = defined $all_weeks[0] ? $all_weeks[0]->get_column('sum_value') / 100000 || 0 : 0;
my $second_week_count = defined $all_weeks[1] ? $all_weeks[1]->get_column('count') || 0 : 0;
my $second_week_value = defined $all_weeks[1] ? $all_weeks[1]->get_column('sum_value') / 100000 || 0 : 0;
my $transaction_rs = $c->schema->resultset('Transaction');
my $value_rs_col = $transaction_rs->get_column('value');
my $max_value = $value_rs_col->max / 100000 || 0;
my $avg_value = sprintf( '%.2f', $value_rs_col->func('AVG') / 100000) || 0;
my $sum_value = $value_rs_col->sum / 100000 || 0;
my $count = $transaction_rs->count || 0;
my $placeholder = 'Placeholder';
$c->stash(
transactions => $transactions,
placeholder => $placeholder,
pending_trans => $pending_transaction_rs->count,
weeks => {
first_count => $first_week_count,
second_count => $second_week_count,
first_value => $first_week_value,
second_value => $second_week_value,
max => $max_value,
avg => $avg_value,
sum => $sum_value,
count => $count,
},
);
}
@ -54,6 +90,9 @@ sub delete {
my $id = $c->param('id');
if ( my $transaction = $c->result_set->find($id) ) {
if (defined $transaction->category) {
$transaction->category->delete;
}
$transaction->delete;
$c->flash( success => 'Successfully deleted transaction' );
$c->redirect_to( '/admin/transactions' );
@ -63,4 +102,19 @@ if ( my $transaction = $c->result_set->find($id) ) {
}
}
sub pg_or_sqlite {
my ( $c, $pg_sql, $sqlite_sql ) = @_;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
if ( $driver eq 'Pg' ) {
return \$pg_sql;
} elsif ( $driver eq 'SQLite' ) {
return \$sqlite_sql;
} else {
$c->app->log->warn('Unknown Driver Used');
return undef;
}
}
1;

View File

@ -22,9 +22,15 @@ has organisation_result_set => sub {
sub index {
my $c = shift;
my $user_rs = $c->user_result_set;
$user_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
$c->stash( users => [ $user_rs->all ] );
my $user_rs = $c->user_result_set->search(
undef, {
prefech => { entity => [ qw/ customer organisation / ] },
page => $c->param('page') || 1,
rows => 10,
order_by => { -asc => 'email' },
}
);
$c->stash( user_rs => $user_rs );
}
sub read {
@ -86,6 +92,11 @@ sub update {
return $c->redirect_to( '/admin/users/' . $id );
}
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
$user->type,
);
if ( $user->type eq 'customer' ){
try {
@ -94,6 +105,7 @@ sub update {
full_name => $validation->param('full_name'),
display_name => $validation->param('display_name'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ),
});
$user->update({
email => $validation->param('email'),
@ -119,6 +131,7 @@ sub update {
town => $validation->param('town'),
sector => $validation->param('sector'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ),
});
$user->update({
email => $validation->param('email'),

View File

@ -74,6 +74,8 @@ sub post_login {
my $email = $validation->param('email');
my $password = $validation->param('password');
$c->app->log->debug( __PACKAGE__ . " login attempt for [" . $email . "]" );
my $user_result = $c->schema->resultset('User')->find({ email => $email });
if ( defined $user_result ) {
@ -86,6 +88,8 @@ sub post_login {
display_name => $user_result->name,
user_type => $user_result->type,
});
} else {
$c->app->log->info( __PACKAGE__ . " failed login for [" . $email . "]" );
}
}
return $c->render(

View File

@ -0,0 +1,86 @@
package Pear::LocalLoop::Controller::Api::Categories;
use Mojo::Base 'Mojolicious::Controller';
use List::Util qw/ max /;
sub post_category_list {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $duration = DateTime::Duration->new( days => 28 );
my $end = DateTime->today;
my $start = $end->clone->subtract_duration( $duration );
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $month_transaction_category_rs = $c->schema->resultset('ViewQuantisedTransactionCategory' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_weeks',
value => { sum => 'value' },
category_id => 'category_id',
essential => 'essential',
},
],
group_by => [ qw/ category_id quantised_weeks essential / ],
}
);
my $data = { categories => {}, essentials => {} };
my $category_list = $c->schema->resultset('Category')->as_hash;
for my $cat_trans ( $month_transaction_category_rs->all ) {
my $quantised = $c->db_datetime_parser->parse_datetime($cat_trans->get_column('quantised'));
my $days = $c->format_iso_date( $quantised ) || 0;
my $category = $cat_trans->get_column('category_id') || 0;
my $value = ($cat_trans->get_column('value') || 0) / 100000;
$data->{categories}->{$days}->{$category_list->{$category}} += $value;
next unless $cat_trans->get_column('essential');
$data->{essentials}->{$days}->{value} += $value;
}
for my $day ( keys %{ $data->{categories} } ) {
my @days = ( map{ {
days => $day,
value => $data->{categories}->{$day}->{$_},
category => $_,
} } keys %{ $data->{categories}->{$day} } );
$data->{categories}->{$day} = [ sort { $b->{value} <=> $a->{value} } @days ];
}
return $c->render(
json => {
success => Mojo::JSON->true,
data => $data,
}
);
}
sub pg_or_sqlite {
my ( $c, $pg_sql, $sqlite_sql ) = @_;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
if ( $driver eq 'Pg' ) {
return \$pg_sql;
} elsif ( $driver eq 'SQLite' ) {
return \$sqlite_sql;
} else {
$c->app->log->warn('Unknown Driver Used');
return undef;
}
}
1;

View File

@ -0,0 +1,489 @@
package Pear::LocalLoop::Controller::Api::External;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON;
sub post_lcc_transactions {
my $c = shift;
my $user = $c->stash->{api_user};
# TODO Check the user is lancaster city council
my $validation = $c->validation;
$validation->input($c->stash->{api_json});
$validation->optional('page')->number;
$validation->optional('per_page')->number;
$validation->optional('search');
return $c->api_validation_error if $validation->has_error;
my $search_ref = { 'me.buyer_id' => $user->entity->id };
if ($validation->param('search')) {
$search_ref->{"organisation.name"} = { '-like' => join('', '%', $validation->param('search'), '%') };
}
my $lcc_transactions = $c->schema->resultset('Transaction')->search(
$search_ref,
{
page => $validation->param('page') || 1,
rows => $validation->param('per_page') || 10,
join => [ 'transaction', 'organisation' ],
order_by => { -desc => 'transaction.purchase_time' },
});
# purchase_time needs timezone attached to it
my @transaction_list = (
map {{
transaction_external_id => $_->external_id,
seller => $_->transaction->seller->name,
net_value => $_->transaction->meta->net_value,
gross_value => $_->transaction->meta->gross_value,
sales_tax_value => $_->transaction->meta->sales_tax_value,
purchase_time => $c->format_iso_datetime($_->transaction->purchase_time),
}} $lcc_transactions->all
);
return $c->render(json => {
success => Mojo::JSON->true,
transactions => \@transaction_list,
page_no => $lcc_transactions->pager->total_entries,
});
}
sub post_lcc_suppliers {
my $c = shift;
my $user = $c->stash->{api_user};
# TODO give an error if user is not of Lancashire County Council
# my $is_lcc = $user->entity->organisation->count({ name => "Lancashire County Council" });
my $v = $c->validation;
$v->input($c->stash->{api_json});
$v->optional('page')->number;
$v->optional('sort_by');
$v->optional('sort_dir');
$v->optional('search');
my $order_by = [
{ -asc => 'organisation.name' },
];
if ($v->param('sort_by')) {
my %dirs = ('asc' => '-asc', 'desc' => '-desc');
my $dir = $dirs{$v->param('sort_dir')} // '-asc';
my %sorts = (
'name' => 'organisation.name',
'postcode' => 'organisation.postcode',
'spend' => 'total_spend',
);
my $sort = $sorts{$v->param('sort_by')} || 'organisation.name';
$order_by->[0] = { $dir => $sort };
}
return $c->api_validation_error if $v->has_error;
my $lcc_suppliers = $c->schema->resultset('Entity')->search(
{
'sales.buyer_id' => $user->entity->id,
($v->param('search') ? (
'-or' => [
{ 'organisation.name' => { 'like' => $v->param('search') . '%' } },
{ 'organisation.postcode' => { 'like' => $v->param('search') . '%' } },
]
) : ()),
},
{
join => [ 'sales', 'organisation' ],
group_by => [ 'me.id', 'organisation.id' ],
'+select' => [
{
'sum' => 'sales.value',
'-as' => 'total_spend',
}
],
'+as' => [ 'total_spend' ],
page => $v->param('page') || 1,
rows => 10,
order_by => $order_by,
}
);
my @supplier_list = (
map {{
entity_id => $_->id,
name => $_->name,
street => $_->organisation->street_name,
town => $_->organisation->town,
postcode => $_->organisation->postcode,
country => $_->organisation->country,
spend => ($_->get_column('total_spend') / 100000) // 0,
}} $lcc_suppliers->all
);
return $c->render(json => {
success => Mojo::JSON->true,
suppliers => \@supplier_list,
page_no => $lcc_suppliers->pager->total_entries,
});
}
sub post_year_spend {
my $c = shift;
my $user = $c->stash->{api_user};
my $v = $c->validation;
$v->input($c->stash->{api_json});
$v->required('from');
$v->required('to');
return $c->api_validation_error if $v->has_error;
my $last = $c->parse_iso_date($v->param('to'));
my $first = $c->parse_iso_date($v->param('from'));
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $spend_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($first),
$dtf->format_datetime($last),
],
},
buyer_id => $user->entity->id,
},
{
columns => [
{
quantised => 'quantised_days',
count => \"COUNT(*)",
total_spend => { sum => 'value' },
}
],
group_by => 'quantised_days',
order_by => { '-asc' => 'quantised_days' },
}
);
my @graph_data = (
map {{
count => $_->get_column('count'),
value => ($_->get_column('total_spend') / 100000) // 0,
date => $_->get_column('quantised'),
}} $spend_rs->all,
);
return $c->render(json => {
success => Mojo::JSON->true,
data => \@graph_data,
});
}
sub post_supplier_count {
my $c = shift;
my $user = $c->stash->{api_user};
my $v = $c->validation;
$v->input($c->stash->{api_json});
$v->required('from');
$v->required('to');
return $c->api_validation_error if $v->has_error;
my $last = $c->parse_iso_date($v->param('to'));
my $first = $c->parse_iso_date($v->param('from'));
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $spend_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($first),
$dtf->format_datetime($last),
],
},
buyer_id => $user->entity->id,
},
{
join => { 'seller' => 'organisation' },
select => [
{ count => 'me.value', '-as' => 'count' },
{ sum => 'me.value', '-as' => 'total_spend' },
'organisation.name',
'me.quantised_days',
],
as => [ qw/count total_spend name quantised_days/ ],
group_by => [ qw/me.quantised_days seller.id organisation.id/ ],
order_by => { '-asc' => 'me.quantised_days' },
}
);
my @graph_data = (
map {{
count => $_->get_column('count'),
value => ($_->get_column('total_spend') / 100000) // 0,
date => $_->get_column('quantised_days'),
seller => $_->get_column('name'),
}} $spend_rs->all,
);
return $c->render(json => {
success => Mojo::JSON->true,
data => \@graph_data,
});
}
sub post_supplier_history {
my $c = shift;
my $user = $c->stash->{api_user};
# Temporary date lock for dev data
my $last = DateTime->new(
year => 2019,
month => 4,
day => 1
);
my $first = $last->clone->subtract(years => 1);
my $second = $last->clone->subtract(months => 6);
my $third = $last->clone->subtract(months => 3);
my $dtf = $c->schema->storage->datetime_parser;
my $year_rs = $c->schema->resultset('Entity')->search(
{
'sales.purchase_time' => {
-between => [
$dtf->format_datetime($first),
$dtf->format_datetime($last),
],
},
'sales.buyer_id' => $user->entity->id,
},
{
join => [ 'sales', 'organisation' ],
columns => [
{
id => 'me.id',
name => 'organisation.name',
count => \"COUNT(*)",
total_spend => { sum => 'sales.value' },
}
],
group_by => [ 'me.id', 'organisation.id' ],
order_by => { '-asc' => 'organisation.name' },
}
);
my $half_year_rs = $c->schema->resultset('Entity')->search(
{
'sales.purchase_time' => {
-between => [
$dtf->format_datetime($second),
$dtf->format_datetime($last),
],
},
'sales.buyer_id' => $user->entity->id,
},
{
join => [ 'sales', 'organisation' ],
columns => [
{
id => 'me.id',
name => 'organisation.name',
count => \"COUNT(*)",
total_spend => { sum => 'sales.value' },
}
],
group_by => [ 'me.id', 'organisation.id' ],
order_by => { '-asc' => 'organisation.name' },
}
);
my $quarter_year_rs = $c->schema->resultset('Entity')->search(
{
'sales.purchase_time' => {
-between => [
$dtf->format_datetime($third),
$dtf->format_datetime($last),
],
},
'sales.buyer_id' => $user->entity->id,
},
{
join => [ 'sales', 'organisation' ],
columns => [
{
id => 'me.id',
name => 'organisation.name',
count => \"COUNT(*)",
total_spend => { sum => 'sales.value' },
}
],
group_by => [ 'me.id', 'organisation.id' ],
order_by => { '-asc' => 'organisation.name' },
}
);
my %data;
for my $row ($year_rs->all) {
$data{$row->get_column('id')} = {
id => $row->get_column('id'),
name => $row->get_column('name'),
quarter_count => 0,
quarter_total => 0,
half_count => 0,
half_total => 0,
year_count => $row->get_column('count'),
year_total => $row->get_column('total_spend') / 100000,
};
}
for my $row ($half_year_rs->all) {
$data{$row->get_column('id')} = {
id => $row->get_column('id'),
name => $row->get_column('name'),
quarter_count => 0,
quarter_total => 0,
half_count => $row->get_column('count'),
half_total => $row->get_column('total_spend') / 100000,
year_count => 0,
year_total => 0,
%{$data{$row->get_column('id')}},
};
}
for my $row ($quarter_year_rs->all) {
$data{$row->get_column('id')} = {
id => $row->get_column('id'),
name => $row->get_column('name'),
quarter_count => $row->get_column('count'),
quarter_total => $row->get_column('total_spend') / 100000,
half_count => 0,
half_total => 0,
year_count => 0,
year_total => 0,
%{$data{$row->get_column('id')}},
};
}
return $c->render(json => {
success => Mojo::JSON->true,
data => [ values %data ],
});
}
sub post_lcc_table_summary {
my $c = shift;
my $user = $c->stash->{api_user};
my $v = $c->validation;
$v->input($c->stash->{api_json});
$v->required('from');
$v->required('to');
return $c->api_validation_error if $v->has_error;
my $last = $c->parse_iso_date($v->param('to'));
my $first = $c->parse_iso_date($v->param('from'));
my $transaction_rs = $c->schema->resultset('Transaction');
my $dtf = $c->schema->storage->datetime_parser;
my $ward_transactions_rs = $transaction_rs->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($first),
$dtf->format_datetime($last),
],
},
buyer_id => $user->entity->id,
},
{
join => { seller => { postcode => { gb_postcode => 'ward' } } },
group_by => 'ward.id',
select => [
{ count => 'me.id', '-as' => 'count' },
{ sum => 'me.value', '-as' => 'sum' },
'ward.ward'
],
as => [ qw/count sum ward_name/ ],
}
);
my $transaction_type_data = {};
my %meta_names = (
local_service => "Local Services",
regional_service => "Regional Services",
national_service => "National Services",
private_household_rebate => "Private Household Rebates etc",
business_tax_and_rebate => "Business Tax & Service Rebates",
stat_loc_gov => "Statutory Loc Gov",
central_loc_gov => "Central Gov HMRC",
);
for my $meta (qw/
local_service
regional_service
national_service
private_household_rebate
business_tax_and_rebate
stat_loc_gov
central_loc_gov
/) {
my $transaction_type_rs = $transaction_rs->search(
{
'me.purchase_time' => {
-between => [
$dtf->format_datetime($first),
$dtf->format_datetime($last),
],
},
'me.buyer_id' => $user->entity->id,
'meta.' . $meta => 1,
},
{
join => 'meta',
group_by => 'meta.' . $meta,
select => [
{ count => 'me.id', '-as' => 'count' },
{ sum => 'me.value', '-as' => 'sum' },
],
as => [ qw/count sum/ ],
}
)->first;
$transaction_type_data->{$meta} = {
($transaction_type_rs ? (
count => $transaction_type_rs->get_column('count'),
sum => $transaction_type_rs->get_column('sum'),
type => $meta_names{$meta},
) : (
count => 0,
sum => 0,
type => $meta_names{$meta},
)),
}
}
my @ward_transaction_list = (
map {{
ward => $_->get_column('ward_name') || "N/A",
sum => $_->get_column('sum') / 100000,
count => $_->get_column('count'),
}} $ward_transactions_rs->all
);
return $c->render(json => {
success => Mojo::JSON->true,
wards => \@ward_transaction_list,
types => $transaction_type_data,
});
}
1;

View File

@ -1,7 +1,7 @@
package Pear::LocalLoop::Controller::Api::Stats;
use Mojo::Base 'Mojolicious::Controller';
use List::Util qw/ first /;
use List::Util qw/ max sum /;
has error_messages => sub {
return {
@ -58,6 +58,320 @@ sub post_index {
});
}
sub post_customer {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $purchase_rs = $entity->purchases;
my $duration_weeks = DateTime::Duration->new( weeks => 7 );
my $end = DateTime->today;
my $start_weeks = $end->clone->subtract_duration( $duration_weeks );
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $week_transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start_weeks),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_weeks',
count => \"COUNT(*)",
}
],
group_by => 'quantised_weeks',
order_by => { '-asc' => 'quantised_weeks' },
}
);
my @all_weeks = $week_transaction_rs->all;
my $first = defined $all_weeks[0] ? $all_weeks[0]->get_column('count') || 0 : 0;
my $second = defined $all_weeks[1] ? $all_weeks[1]->get_column('count') || 0 : 0;
my $max = max( map { $_->get_column('count') } @all_weeks );
my $sum = sum( map { $_->get_column('count') } @all_weeks );
my $count = $week_transaction_rs->count;
my $weeks = {
first => $first,
second => $second,
max => $max,
sum => $sum,
count => $count,
};
my $data = { cat_total => {}, categories => {}, essentials => {}, cat_list => {} };
my $category_list = $c->schema->resultset('Category')->as_hash;
my $category_purchase_rs = $purchase_rs->search({},
{
join => 'category',
columns => {
category_id => "category.category_id",
value => { sum => 'value' },
},
group_by => "category.category_id",
}
);
my %cat_total_list;
for ( $category_purchase_rs->all ) {
my $category = $_->get_column('category_id') || 0;
my $value = ($_->get_column('value') || 0) / 100000;
$cat_total_list{$category_list->{$category}} += $value;
}
my @cat_lists = map { { category => $_, value => $cat_total_list{$_},
icon => $c->schema->resultset('Category')->as_hash_name_icon->{$_} || 'question'} } sort keys %cat_total_list;
$data->{cat_list} = [ sort { $b->{value} <=> $a->{value} } @cat_lists ];
my $purchase_no_essential_rs = $purchase_rs->search({
"me.essential" => 1,
});
$data->{essentials} = {
purchase_no_total => $purchase_rs->count,
purchase_no_essential_total => $purchase_no_essential_rs->count,
};
my $duration_month = DateTime::Duration->new( days => 28 );
my $start_month = $end->clone->subtract_duration( $duration_month );
my $month_transaction_category_rs = $c->schema->resultset('ViewQuantisedTransactionCategory' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start_month),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_weeks',
value => { sum => 'value' },
category_id => 'category_id',
essential => 'essential',
},
],
group_by => [ qw/ category_id quantised_weeks essential / ],
}
);
for my $cat_trans ( $month_transaction_category_rs->all ) {
my $quantised = $c->db_datetime_parser->parse_datetime($cat_trans->get_column('quantised'));
my $days = $c->format_iso_date( $quantised ) || 0;
my $category = $cat_trans->get_column('category_id') || 0;
my $value = ($cat_trans->get_column('value') || 0) / 100000;
$data->{cat_total}->{$category_list->{$category}} += $value;
$data->{categories}->{$days}->{$category_list->{$category}} += $value;
next unless $cat_trans->get_column('essential');
$data->{essentials}->{$days}->{value} += $value;
}
for my $day ( keys %{ $data->{categories} } ) {
my @days = ( map{ {
days => $day,
value => $data->{categories}->{$day}->{$_},
category => $_,
} } keys %{ $data->{categories}->{$day} } );
$data->{categories}->{$day} = [ sort { $b->{value} <=> $a->{value} } @days ];
}
return $c->render( json => {
success => Mojo::JSON->true,
data => $data,
weeks => $weeks,
});
}
sub post_organisation {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $purchase_rs = $entity->purchases;
my $duration_weeks = DateTime::Duration->new( weeks => 7 );
my $end = DateTime->today;
my $start_weeks = $end->clone->subtract_duration( $duration_weeks );
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $week_transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start_weeks),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_weeks',
count => \"COUNT(*)",
}
],
group_by => 'quantised_weeks',
order_by => { '-asc' => 'quantised_weeks' },
}
);
my @all_weeks = $week_transaction_rs->all;
my $first = defined $all_weeks[0] ? $all_weeks[0]->get_column('count') || 0 : 0;
my $second = defined $all_weeks[1] ? $all_weeks[1]->get_column('count') || 0 : 0;
my $max = max( map { $_->get_column('count') } @all_weeks );
my $sum = sum( map { $_->get_column('count') } @all_weeks );
my $count = $week_transaction_rs->count;
my $weeks = {
first => $first,
second => $second,
max => $max,
sum => $sum,
count => $count,
};
my $data = {
cat_total => {},
categories => {},
essentials => {},
cat_list => {},
sector_monthly => {}
};
my $category_list = $c->schema->resultset('Category')->as_hash;
my $category_purchase_rs = $purchase_rs->search({},
{
join => 'category',
columns => {
category_id => "category.category_id",
value => { sum => 'value' },
},
group_by => "category.category_id",
}
);
my %cat_total_list;
for ( $category_purchase_rs->all ) {
my $category = $_->get_column('category_id') || 0;
my $value = ($_->get_column('value') || 0) / 100000;
$cat_total_list{$category_list->{$category}} += $value;
}
my @cat_lists = map { { category => $_, value => $cat_total_list{$_},
icon => $c->schema->resultset('Category')->as_hash_name_icon->{$_} || 'question'} } sort keys %cat_total_list;
$data->{cat_list} = [ sort { $b->{value} <=> $a->{value} } @cat_lists ];
my $purchase_no_essential_rs = $purchase_rs->search({
"me.essential" => 1,
});
$data->{essentials} = {
purchase_no_total => $purchase_rs->count,
purchase_no_essential_total => $purchase_no_essential_rs->count,
};
my $duration_month = DateTime::Duration->new( days => 28 );
my $start_month = $end->clone->subtract_duration( $duration_month );
my $month_transaction_category_rs = $c->schema->resultset('ViewQuantisedTransactionCategory' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start_month),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_weeks',
value => { sum => 'value' },
category_id => 'category_id',
essential => 'essential',
},
],
group_by => [ qw/ category_id quantised_weeks essential / ],
}
);
for my $cat_trans ( $month_transaction_category_rs->all ) {
my $quantised = $c->db_datetime_parser->parse_datetime($cat_trans->get_column('quantised'));
my $days = $c->format_iso_date( $quantised ) || 0;
my $category = $cat_trans->get_column('category_id') || 0;
my $value = ($cat_trans->get_column('value') || 0) / 100000;
$data->{cat_total}->{$category_list->{$category}} += $value;
$data->{categories}->{$days}->{$category_list->{$category}} += $value;
next unless $cat_trans->get_column('essential');
$data->{essentials}->{$days}->{value} += $value;
}
for my $day ( keys %{ $data->{categories} } ) {
my @days = ( map{ {
days => $day,
value => $data->{categories}->{$day}->{$_},
category => $_,
} } keys %{ $data->{categories}->{$day} } );
$data->{categories}->{$day} = [ sort { $b->{value} <=> $a->{value} } @days ];
}
# my $start_year_monthly = DateTime->now->truncate( to => 'year' );
# my $current_year_monthly = DateTime->now->add( months => 1, end_of_month => 'limit' );
# my $monthly_sector_transactions_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
# {
# purchase_time => {
# -between => [
# $dtf->format_datetime($start_year_monthly),
# $dtf->format_datetime($current_year_monthly),
# ],
# },
# buyer_id => $entity->id,
# },
# {
# columns => [
# {
# quantised => 'quantised_months',
# value => { sum => 'value' },
# },
# ],
# group_by => [ qw/ quantised_months / ],
# }
# );
#
# for my $sector_transaction ( $monthly_sector_transactions_rs->all ) {
# my $quantised = $c->db_datetime_parser->parse_datetime($cat_trans->get_column('quantised'));
# my $months = $c->format_iso_date( $quantised ) || 0;
# my $category = $cat_trans->get_column('category_id') || 0;
# my $value = ($cat_trans->get_column('value') || 0) / 100000;
# }
return $c->render( json => {
success => Mojo::JSON->true,
data => $data,
weeks => $weeks,
});
}
sub post_leaderboards {
my $c = shift;
@ -109,4 +423,90 @@ sub post_leaderboards {
});
}
sub post_leaderboards_paged {
my $c = shift;
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
my $leaderboard_rs = $c->schema->resultset('Leaderboard');
$validation->required('type')->in_resultset( 'type', $leaderboard_rs );
$validation->optional('page')->number;
return $c->api_validation_error if $validation->has_error;
my $page = 1;
my $today_board = $leaderboard_rs->get_latest( $validation->param('type') );
my @leaderboard_array;
my $current_user_position;
my $values_count = 0;
if ( defined $today_board ) {
if ( !defined $validation->param('page') || $validation->param('page') < 1 ) {
my $user_position = $today_board->values->find({ entity_id => $c->stash->{api_user}->entity->id });
$page = int(defined $user_position ? $user_position->{position} : 0 / 10) + 1;
} else {
$page = $validation->param('page');
}
my $today_values = $today_board->values->search(
{},
{
page => $page,
rows => 10,
order_by => { -asc => 'me.position' },
columns => [
qw/
me.value
me.trend
me.position
/,
{ display_name => 'customer.display_name' },
],
join => { entity => 'customer' },
},
);
$today_values->result_class( 'DBIx::Class::ResultClass::HashRefInflator' );
@leaderboard_array = $today_values->all;
$values_count = $today_values->pager->total_entries;
if ( $validation->param('type') =~ /total$/ ) {
@leaderboard_array = (map {
{
%$_,
value => $_->{value} / 100000,
}
} @leaderboard_array);
}
$current_user_position = $today_values->find({ entity_id => $c->stash->{api_user}->entity->id });
}
return $c->render( json => {
success => Mojo::JSON->true,
leaderboard => [ @leaderboard_array ],
user_position => defined $current_user_position ? $current_user_position->{position} : 0,
page => $page,
count => $values_count,
});
}
sub pg_or_sqlite {
my ( $c, $pg_sql, $sqlite_sql ) = @_;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
if ( $driver eq 'Pg' ) {
return \$pg_sql;
} elsif ( $driver eq 'SQLite' ) {
return \$sqlite_sql;
} else {
$c->app->log->warn('Unknown Driver Used');
return undef;
}
}
1;

View File

@ -8,6 +8,21 @@ has error_messages => sub {
required => { message => 'No email sent.', status => 400 },
email => { message => 'Email is invalid.', status => 400 },
},
value => {
required => { message => 'transaction amount is missing', status => 400 },
number => { message => 'transaction amount does not look like a number', status => 400 },
gt_num => { message => 'transaction amount cannot be equal to or less than zero', status => 400 },
},
apply_time => {
required => { message => 'purchase time is missing', status => 400 },
is_full_iso_datetime => { message => 'time is in incorrect format', status => 400 },
},
id => {
required => { message => 'Recurring Transaction not found', status => 400 },
},
category => {
in_resultset => { message => 'Category is invalid', status => 400 },
},
};
};
@ -30,20 +45,135 @@ sub post_transaction_list_purchases {
},
);
# purchase_time needs timezone attached to it
my $recurring_transactions = $c->schema->resultset('TransactionRecurring')->search({
buyer_id => $user->id,
});
# purchase_time needs timezone attached to it
my @transaction_list = (
map {{
seller => $_->seller->name,
value => $_->value / 100000,
purchase_time => $c->format_iso_datetime($_->purchase_time),
( $_->meta ? (
net_value => $_->meta->net_value / 100000,
sales_tax_value => $_->meta->sales_tax_value / 100000,
gross_value => $_->meta->gross_value / 100000,
) : (
net_value => undef,
sales_tax_value => undef,
gross_value => undef,
)),
}} $transactions->all
);
my @recurring_transaction_list = (
map {{
id => $_->id,
seller => $_->seller->name,
value => $_->value / 100000,
start_time => $c->format_iso_datetime($_->start_time),
last_updated => $c->format_iso_datetime($_->last_updated) || undef,
essential => $_->essential,
category => $_->category_id || 0,
recurring_period => $_->recurring_period,
}} $recurring_transactions->all
);
return $c->render( json => {
success => Mojo::JSON->true,
transactions => \@transaction_list,
recurring_transactions => \@recurring_transaction_list,
page_no => $transactions->pager->total_entries,
});
}
sub update_recurring {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
$validation->required('id');
return $c->api_validation_error if $validation->has_error;
my $id = $validation->param('id');
my $recur_transaction = $c->schema->resultset('TransactionRecurring')->find($id);
unless ( $recur_transaction ) {
return $c->render(
json => {
success => Mojo::JSON->false,
message => 'Error Finding Recurring Transaction',
error => 'recurring_error',
},
status => 400,
);
}
$validation->required('recurring_period');
$validation->required('apply_time')->is_full_iso_datetime;
$validation->optional('category')->in_resultset( 'id', $c->schema->resultset('Category'));
$validation->optional('essential');
$validation->required('value');
return $c->api_validation_error if $validation->has_error;
my $apply_time = $c->parse_iso_datetime($validation->param('apply_time'));
$c->schema->storage->txn_do( sub {
$recur_transaction->update({
start_time => $c->format_db_datetime($apply_time),
last_updated => undef,
category_id => $validation->param('category'),
essential => $validation->param('essential'),
value => $validation->param('value') * 100000,
recurring_period => $validation->param('recurring_period'),
});
});
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Recurring Transaction Updated Successfully',
});
}
sub delete_recurring {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
$validation->required('id');
return $c->api_validation_error if $validation->has_error;
my $id = $validation->param('id');
my $recur_transaction = $c->schema->resultset('TransactionRecurring')->find($id);
unless ( $recur_transaction ) {
return $c->render(
json => {
success => Mojo::JSON->false,
message => 'Error Finding Recurring Transaction',
error => 'recurring_error',
},
status => 400,
);
}
$recur_transaction->delete;
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Recurring Transaction Deleted Successfully',
});
}
1;

View File

@ -75,6 +75,9 @@ has error_messages => sub {
organisation_name => {
required => { message => 'organisation name is missing', status => 400 },
},
category => {
in_resultset => { message => 'Category is invalid', status => 400 },
},
town => {
required => { message => 'town/city is missing', status => 400 },
},
@ -104,6 +107,9 @@ sub post_upload {
#Check a proper purchase time was submitted
$validation->optional('purchase_time')->is_full_iso_datetime;
$validation->optional('category')->in_resultset( 'id', $c->schema->resultset('Category'));
$validation->optional('essential');
$validation->optional('recurring');
# First pass of required items
return $c->api_validation_error if $validation->has_error;
@ -117,7 +123,7 @@ sub post_upload {
my $valid_org_rs = $c->schema->resultset('Organisation')->search({
pending => 0,
entity_id => { "!=" => $user->entity_id },
});
});
$validation->required('organisation_id')->number->in_resultset( 'id', $valid_org_rs );
return $c->api_validation_error if $validation->has_error;
@ -141,17 +147,23 @@ sub post_upload {
# Unknown Organisation
$validation->required('organisation_name');
$validation->optional('street_name');
$validation->required('town');
$validation->optional('town');
$validation->optional('postcode')->postcode;
return $c->api_validation_error if $validation->has_error;
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
'organisation',
);
my $entity = $c->schema->resultset('Entity')->create_org({
submitted_by_id => $user->id,
name => $validation->param('organisation_name'),
street_name => $validation->param('street_name'),
town => $validation->param('town'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : ( latitude => undef, longitude => undef ) ),
pending => 1,
});
$organisation = $entity->organisation;
@ -173,6 +185,9 @@ sub post_upload {
my $purchase_time = $c->parse_iso_datetime($validation->param('purchase_time') || '');
$purchase_time ||= DateTime->now();
my $file = defined $upload ? $c->store_file_from_upload( $upload ) : undef;
my $category = $validation->param('category');
my $essential = $validation->param('essential');
my $recurring_period = $validation->param('recurring');
my $distance = $c->get_distance_from_coords( $user->entity->type_object, $organisation );
my $new_transaction = $organisation->entity->create_related(
@ -182,6 +197,7 @@ sub post_upload {
value => $transaction_value * 100000,
( defined $file ? ( proof_image => $file ) : () ),
purchase_time => $c->format_db_datetime($purchase_time),
essential => ( defined $essential ? $essential : 0 ),
distance => $distance,
}
);
@ -197,12 +213,45 @@ sub post_upload {
);
}
if ( defined $category ) {
$c->schema->resultset('TransactionCategory')->create({
category_id => $category,
transaction_id => $new_transaction->id,
});
}
if ( defined $recurring_period ) {
$c->schema->resultset('TransactionRecurring')->create({
buyer => $user->entity,
seller => $organisation->entity,
value => $transaction_value * 100000,
start_time => $c->format_db_datetime($purchase_time),
essential => ( defined $essential ? $essential : 0 ),
distance => $distance,
category_id => ( defined $category ? $category : undef ),
recurring_period => $recurring_period,
});
}
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Upload Successful',
});
}
sub post_category {
my $c = shift;
my $self = $c;
my $category_list = $c->schema->resultset('Category')->as_hash;
delete $category_list->{0};
return $self->render( json => {
success => Mojo::JSON->true,
categories => $category_list,
});
}
# TODO Limit search results, possibly paginate them?
# TODO Search by location as well
sub post_search {
@ -216,6 +265,7 @@ sub post_search {
$validation->input( $c->stash->{api_json} );
$validation->required('search_name');
$validation->optional('page')->number;
return $c->api_validation_error if $validation->has_error;
@ -227,6 +277,11 @@ sub post_search {
my $valid_orgs_rs = $org_rs->search({
pending => 0,
entity_id => { "!=" => $user->entity_id },
},
{
page => $validation->param('page') || 1,
rows => 10,
order_by => { -desc => 'name' },
})->search(
\$search_stmt,
);

View File

@ -0,0 +1,21 @@
package Pear::LocalLoop::Controller::Api::V1::Customer;
use Mojo::Base 'Mojolicious::Controller';
sub auth {
my $c = shift;
return 1 if $c->stash->{api_user}->type eq 'customer';
$c->render(
json => {
success => Mojo::JSON->false,
message => 'Not an Customer',
error => 'user_not_cust',
},
status => 403,
);
return 0;
}
1;

View File

@ -0,0 +1,167 @@
package Pear::LocalLoop::Controller::Api::V1::Customer::Graphs;
use Mojo::Base 'Mojolicious::Controller';
has error_messages => sub {
return {
graph => {
required => { message => 'Must request graph type', status => 400 },
in => { message => 'Unrecognised graph type', status => 400 },
},
};
};
sub index {
my $c = shift;
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
$validation->required('graph')->in( qw/
total_last_week
avg_spend_last_week
total_last_month
avg_spend_last_month
/ );
return $c->api_validation_error if $validation->has_error;
my $graph_sub = "graph_" . $validation->param('graph');
unless ( $c->can($graph_sub) ) {
# Secondary catch in case a mistake has been made
return $c->render(
json => {
success => Mojo::JSON->false,
message => $c->error_messages->{graph}->{in}->{message},
error => 'in',
},
status => $c->error_messages->{graph}->{in}->{status},
);
}
return $c->$graph_sub;
}
sub graph_total_last_week { return shift->_purchases_total_duration( 7 ) }
sub graph_total_last_month { return shift->_purchases_total_duration( 30 ) }
sub _purchases_total_duration {
my ( $c, $day_duration ) = @_;
my $duration = DateTime::Duration->new( days => $day_duration );
my $entity = $c->stash->{api_user}->entity;
my $data = { labels => [], data => [] };
my ( $start, $end ) = $c->_get_start_end_duration( $duration );
$data->{bounds} = {
min => $c->format_iso_datetime( $start ),
max => $c->format_iso_datetime( $end ),
};
while ( $start < $end ) {
my $next_end = $start->clone->add( days => 1 );
my $transactions = $entity->purchases
->search_between( $start, $next_end )
->get_column('value')
->sum || 0 * 1;
push @{ $data->{ labels } }, $c->format_iso_datetime( $start );
push @{ $data->{ data } }, $transactions / 100000;
$start->add( days => 1 );
}
return $c->render(
json => {
success => Mojo::JSON->true,
graph => $data,
}
);
}
sub graph_avg_spend_last_week { return shift->_purchases_avg_spend_duration( 7 ) }
sub graph_avg_spend_last_month { return shift->_purchases_avg_spend_duration( 30 ) }
sub _purchases_avg_spend_duration {
my ( $c, $day_duration ) = @_;
my $duration = DateTime::Duration->new( days => $day_duration );
my $entity = $c->stash->{api_user}->entity;
my $data = { labels => [], data => [] };
my ( $start, $end ) = $c->_get_start_end_duration( $duration );
$data->{bounds} = {
min => $c->format_iso_datetime( $start ),
max => $c->format_iso_datetime( $end ),
};
my $dtf = $c->schema->storage->datetime_parser;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{
purchase_time => {
-between => [
$dtf->format_datetime($start),
$dtf->format_datetime($end),
],
},
buyer_id => $entity->id,
},
{
columns => [
{
quantised => 'quantised_days',
count => \"COUNT(*)",
sum_value => $c->pg_or_sqlite(
'SUM("me"."value")',
'SUM("me"."value")',
),
average_value => $c->pg_or_sqlite(
'AVG("me"."value")',
'AVG("me"."value")',
),
}
],
group_by => 'quantised_days',
order_by => { '-asc' => 'quantised_days' },
}
);
for ( $transaction_rs->all ) {
my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised'));
push @{ $data->{ labels } }, $c->format_iso_datetime( $quantised );
push @{ $data->{ data } }, ($_->get_column('average_value') || 0) / 100000;
}
return $c->render(
json => {
success => Mojo::JSON->true,
graph => $data,
}
);
}
sub _get_start_end_duration {
my ( $c, $duration ) = @_;
my $end = DateTime->today;
my $start = $end->clone->subtract_duration( $duration );
return ( $start, $end );
}
sub pg_or_sqlite {
my ( $c, $pg_sql, $sqlite_sql ) = @_;
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
if ( $driver eq 'Pg' ) {
return \$pg_sql;
} elsif ( $driver eq 'SQLite' ) {
return \$sqlite_sql;
} else {
$c->app->log->warn('Unknown Driver Used');
return undef;
}
}
1;

View File

@ -0,0 +1,63 @@
package Pear::LocalLoop::Controller::Api::V1::Customer::Pies;
use Mojo::Base 'Mojolicious::Controller';
sub index {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $purchase_rs = $entity->purchases;
my $local_org_local_purchase = $purchase_rs->search({
"me.distance" => { '<', 20000 },
'organisation.is_local' => 1,
},
{
join => { 'seller' => 'organisation' },
}
);
my $local_org_non_local_purchase = $purchase_rs->search({
"me.distance" => { '>=', 20000 },
'organisation.is_local' => 1,
},
{
join => { 'seller' => 'organisation' },
}
);
my $non_local_org_local_purchase = $purchase_rs->search({
"me.distance" => { '<', 20000 },
'organisation.is_local' => 0,
},
{
join => { 'seller' => 'organisation' },
}
);
my $non_local_org_non_local_purchase = $purchase_rs->search({
"me.distance" => { '>=', 20000 },
'organisation.is_local' => 0,
},
{
join => { 'seller' => 'organisation' },
}
);
my $local_all = {
'Local shop local purchaser' => $local_org_local_purchase->count,
'Local shop non-local purchaser' => $local_org_non_local_purchase->count,
'Non-local shop local purchaser' => $non_local_org_local_purchase->count,
'Non-local shop non-local purchaser' => $non_local_org_non_local_purchase->count,
};
return $c->render(
json => {
success => Mojo::JSON->true,
local_all => $local_all,
}
);
}
1;

View File

@ -0,0 +1,32 @@
package Pear::LocalLoop::Controller::Api::V1::Customer::Snippets;
use Mojo::Base 'Mojolicious::Controller';
sub index {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $data = {
user_sum => 0,
user_position => 0,
};
my $user_rs = $entity->purchases;
$data->{ user_sum } = $user_rs->get_column('value')->sum || 0;
$data->{ user_sum } /= 100000;
my $leaderboard_rs = $c->schema->resultset('Leaderboard');
my $monthly_board = $leaderboard_rs->get_latest( 'monthly_total' );
if (defined $monthly_board) {
my $monthly_values = $monthly_board->values;
$data->{ user_position } = $monthly_values ? $monthly_values->find({ entity_id => $entity->id })->position : 0;
}
return $c->render(
json => {
success => Mojo::JSON->true,
snippets => $data,
}
);
}
1;

View File

@ -78,35 +78,30 @@ sub graph_customers_range {
);
}
sub graph_customers_last_7_days {
my $c = shift;
my $duration = DateTime::Duration->new( days => 7 );
return $c->_customers_last_duration( $duration );
}
sub graph_customers_last_30_days {
my $c = shift;
my $duration = DateTime::Duration->new( days => 30 );
return $c->_customers_last_duration( $duration );
}
sub graph_customers_last_7_days { return shift->_customers_last_duration( 7 ) }
sub graph_customers_last_30_days { return shift->_customers_last_duration( 30 ) }
sub _customers_last_duration {
my ( $c, $duration ) = @_;
my ( $c, $day_duration ) = @_;
my $duration = DateTime::Duration->new( days => $day_duration );
my $entity = $c->stash->{api_user}->entity;
my $data = { labels => [], data => [] };
my ( $start, $end ) = $c->_get_start_end_duration( $duration );
$data->{bounds} = {
min => $c->format_iso_datetime( $start ),
max => $c->format_iso_datetime( $end ),
};
while ( $start < $end ) {
my $next_end = $start->clone->add( days => 1 );
my $transactions = $entity->sales
->search_between( $start, $next_end )
->count;
push @{ $data->{ labels } }, $start->day_name;
push @{ $data->{ labels } }, $c->format_iso_datetime( $start );
push @{ $data->{ data } }, $transactions;
$start->add( days => 1 );
}
@ -132,13 +127,18 @@ sub _sales_last_duration {
my ( $start, $end ) = $c->_get_start_end_duration( $duration );
$data->{bounds} = {
min => $c->format_iso_datetime( $start ),
max => $c->format_iso_datetime( $end ),
};
while ( $start < $end ) {
my $next_end = $start->clone->add( days => 1 );
my $transactions = $entity->sales
->search_between( $start, $next_end )
->get_column('value')
->sum || 0 + 0;
push @{ $data->{ labels } }, $start->day_name;
push @{ $data->{ labels } }, $c->format_iso_datetime( $start );
push @{ $data->{ data } }, $transactions / 100000;
$start->add( days => 1 );
}
@ -164,13 +164,18 @@ sub _purchases_last_duration {
my ( $start, $end ) = $c->_get_start_end_duration( $duration );
$data->{bounds} = {
min => $c->format_iso_datetime( $start ),
max => $c->format_iso_datetime( $end ),
};
while ( $start < $end ) {
my $next_end = $start->clone->add( days => 1 );
my $transactions = $entity->purchases
->search_between( $start, $next_end )
->get_column('value')
->sum || 0 + 0;
push @{ $data->{ labels } }, $start->day_name;
push @{ $data->{ labels } }, $c->format_iso_datetime( $start );
push @{ $data->{ data } }, $transactions / 100000;
$start->add( days => 1 );
}

View File

@ -0,0 +1,63 @@
package Pear::LocalLoop::Controller::Api::V1::Organisation::Pies;
use Mojo::Base 'Mojolicious::Controller';
sub index {
my $c = shift;
my $entity = $c->stash->{api_user}->entity;
my $purchase_rs = $entity->purchases;
my $local_org_local_purchase = $purchase_rs->search({
"me.distance" => { '<', 20000 },
'organisation.is_local' => 1,
},
{
join => { 'seller' => 'organisation' },
}
);
my $local_org_non_local_purchase = $purchase_rs->search({
"me.distance" => { '>=', 20000 },
'organisation.is_local' => 1,
},
{
join => { 'seller' => 'organisation' },
}
);
my $non_local_org_local_purchase = $purchase_rs->search({
"me.distance" => { '<', 20000 },
'organisation.is_local' => [0, undef],
},
{
join => { 'seller' => 'organisation' },
}
);
my $non_local_org_non_local_purchase = $purchase_rs->search({
"me.distance" => { '>=', 20000 },
'organisation.is_local' => [0, undef],
},
{
join => { 'seller' => 'organisation' },
}
);
my $local_all = {
'Local shop local purchaser' => $local_org_local_purchase->count,
'Local shop non-local purchaser' => $local_org_non_local_purchase->count,
'Non-local shop local purchaser' => $non_local_org_local_purchase->count,
'Non-local shop non-local purchaser' => $non_local_org_non_local_purchase->count,
};
return $c->render(
json => {
success => Mojo::JSON->true,
local_all => $local_all,
}
);
}
1;

View File

@ -6,6 +6,10 @@ sub index {
my $entity = $c->stash->{api_user}->entity;
my $data = {
all_sales_count => 0,
all_sales_total => 0,
all_purchases_count => 0,
all_purchases_total => 0,
this_month_sales_count => 0,
this_month_sales_total => 0,
this_month_purchases_count => 0,
@ -25,6 +29,12 @@ sub index {
my $week_ago = $today->clone->subtract( days => 7 );
my $month_ago = $today->clone->subtract( days => 30 );
# TODO check that sales is doing the right thing here
my $all_sales = $entity->sales;
$data->{ all_sales_count } = $all_sales->count;
$data->{ all_sales_total } = $all_sales->get_column('value')->sum || 0;
$data->{ all_sales_total } /= 100000;
my $today_sales = $entity->sales->search_between( $today, $now );
$data->{ today_sales_count } = $today_sales->count;
$data->{ today_sales_total } = $today_sales->get_column('value')->sum || 0;
@ -40,6 +50,11 @@ sub index {
$data->{ this_month_sales_total } = $month_sales->get_column('value')->sum || 0;
$data->{ this_month_sales_total } /= 100000;
my $all_purchases = $entity->purchases;
$data->{ all_purchases_count } = $all_purchases->count;
$data->{ all_purchases_total } = $all_purchases->get_column('value')->sum || 0;
$data->{ all_purchases_total } /= 100000;
my $today_purchases = $entity->purchases->search_between( $today, $now );
$data->{ today_purchases_count } = $today_purchases->count;
$data->{ today_purchases_total } = $today_purchases->get_column('value')->sum || 0;

View File

@ -79,6 +79,9 @@ sub index {
'organisation.name',
'organisation.latitude',
'organisation.longitude',
'organisation.street_name',
'organisation.town',
'organisation.postcode',
],
group_by => [ qw/ organisation.id / ],
},
@ -86,11 +89,14 @@ sub index {
$org_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
my $suppliers = [ map {
my $suppliers = [ map {
{
latitude => $_->{organisation}->{latitude} * 1,
longitude => $_->{organisation}->{longitude} * 1,
name => $_->{organisation}->{name},
street_name => $_->{organisation}->{street_name},
town => $_->{organisation}->{town},
postcode => $_->{organisation}->{postcode},
}
} $org_rs->all ];
@ -106,4 +112,82 @@ sub index {
);
}
sub trail_load {
my $c = shift;
return if $c->validation_error('index');
my $json = $c->stash->{api_json};
# Extra custom error, because its funny
if ( $json->{north_east}->{latitude} < $json->{south_west}->{latitude} ) {
return $c->render(
json => {
success => Mojo::JSON->false,
errors => [ 'upside_down' ],
},
status => 400,
);
}
my $entity = $c->stash->{api_user}->entity;
my $entity_type_object = $entity->type_object;
my $orgs_lis = $c->schema->resultset('EntityAssociation')->search(
{
$json->{association} => 1,
},
);
# need: organisations only, with name, latitude, and longitude
my $org_rs = $orgs_lis->search_related('entity',
{
'entity.type' => 'organisation',
'organisation.latitude' => { -between => [
$json->{south_west}->{latitude},
$json->{north_east}->{latitude},
] },
'organisation.longitude' => { -between => [
$json->{south_west}->{longitude},
$json->{north_east}->{longitude},
] },
},
{
join => [ qw/ organisation / ],
columns => [
'organisation.name',
'organisation.latitude',
'organisation.longitude',
'organisation.street_name',
'organisation.town',
'organisation.postcode',
],
group_by => [ qw/ organisation.id / ],
},
);
$org_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
my $locations = [ map {
{
latitude => $_->{organisation}->{latitude} * 1,
longitude => $_->{organisation}->{longitude} * 1,
name => $_->{organisation}->{name},
street_name => $_->{organisation}->{street_name},
town => $_->{organisation}->{town},
postcode => $_->{organisation}->{postcode},
}
} $org_rs->all ];
$c->render(
json => {
success => Mojo::JSON->true,
locations => $locations,
self => {
latitude => $entity_type_object->latitude,
longitude => $entity_type_object->longitude,
}
},
);
}
1;

View File

@ -0,0 +1,48 @@
package Pear::LocalLoop::Controller::Api::V1::User::Medals;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw/true false/;
sub index {
my $c = shift;
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
# Placeholder data
my $global_placeholder = {
group_name => {
threshold => {
awarded => true,
awarded_at => '2017-01-02T01:00:00Z',
threshold => 1,
points => 1,
},
total => 1,
},
};
my $organisation_placeholder = {
org_id => {
group_name => {
threshold => {
awarded => true,
awarded_at => '2017-01-02T01:00:00Z',
threshold => 1,
points => 1,
multiplier => 1,
},
total => 1,
},
name => 'Placeholder',
},
};
return $c->render(
json => {
success => Mojo::JSON->true,
global => $global_placeholder,
organisation => $organisation_placeholder,
}
);
}
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Controller::Api::V1::User::Points;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw/true false/;
sub index {
my $c = shift;
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
# Placeholder data
my $snippets_placeholder = {
points_total => 1,
point_last => 1,
trans_count => 1,
avg_multi => 1,
};
my $widget_line_placeholder = { labels => [], data => [] };
my $widget_progress_placeholder = {
this_week => 1,
last_week => 1,
max => 1,
sum => 1,
count => 1,
};
return $c->render(
json => {
success => Mojo::JSON->true,
snippets => $snippets_placeholder,
widget_line => $widget_line_placeholder,
widget_progress => $widget_progress_placeholder,
}
);
}
1;

View File

@ -0,0 +1,10 @@
package Pear::LocalLoop::Error;
use Moo;
extends 'Throwable::Error';
package Pear::LocalLoop::ImplementationError;
use Moo;
use namespace::clean;
extends 'Pear::LocalLoop::Error';
1;

View File

@ -0,0 +1,23 @@
package Pear::LocalLoop::Import::LCCCsv;
use Moo;
use Pear::LocalLoop::Error;
has external_name => (
is => 'ro',
default => 'LCC CSV',
);
has csv_required_columns => (
is => 'lazy',
builder => sub {
Pear::LocalLoop::ImplementationError->throw("Must be implemented by child class");
},
);
with qw/
Pear::LocalLoop::Import::Role::ExternalName
Pear::LocalLoop::Import::Role::Schema
Pear::LocalLoop::Import::Role::CSV
/;
1;

View File

@ -0,0 +1,43 @@
package Pear::LocalLoop::Import::LCCCsv::Postcodes;
use Moo;
use Geo::UK::Postcode::Regex;
extends qw/Pear::LocalLoop::Import::LCCCsv/;
has '+csv_required_columns' => (
builder => sub { return [ qw/
postcode
ward
/ ]},
);
sub import_csv {
my ($self) = @_;
$self->check_headers;
while ( my $row = $self->get_csv_line ) {
$self->_row_to_result($row);
}
}
sub _row_to_result {
my ( $self, $row ) = @_;
my $postcode_obj = Geo::UK::Postcode::Regex->parse( $row->{postcode} );
my $ward = $self->schema->resultset('GbWard')->find_or_create(ward => $row->{ward});
my $postcode_r = $self->schema->resultset('GbPostcode')->find({
outcode => $postcode_obj->{outcode},
incode => $postcode_obj->{incode},
});
return unless $postcode_r;
return if $postcode_r->ward;
$postcode_r->update({ ward_id => $ward->id });
}
1;

View File

@ -0,0 +1,48 @@
package Pear::LocalLoop::Import::LCCCsv::Suppliers;
use Moo;
extends qw/Pear::LocalLoop::Import::LCCCsv/;
has '+csv_required_columns' => (
builder => sub { return [ qw/
supplier_id
name
/ ]},
);
sub import_csv {
my ($self) = @_;
$self->check_headers;
while ( my $row = $self->get_csv_line ) {
$self->_row_to_result($row);
}
}
sub _row_to_result {
my ( $self, $row ) = @_;
my $addr2 = $row->{post_town};
my $address = ( defined $addr2 ? ( $row->{"address line 2"} . ' ' . $addr2) : $row->{"address line 2"} );
return if $self->external_result->organisations->find({external_id => $row->{supplier_id}});
$self->schema->resultset('Entity')->create({
type => 'organisation',
organisation => {
name => $row->{name},
street_name => $row->{"address line 1"},
town => $address,
postcode => $row->{post_code},
country => $row->{country_code},
external_reference => [ {
external_reference => $self->external_result,
external_id => $row->{supplier_id},
} ],
}
});
}
1;

View File

@ -0,0 +1,128 @@
package Pear::LocalLoop::Import::LCCCsv::Transactions;
use Moo;
use DateTime;
use DateTime::Format::Strptime;
use Geo::UK::Postcode::Regex;
extends qw/Pear::LocalLoop::Import::LCCCsv/;
has target_entity_id => (
is => 'ro',
required => 1,
);
has target_entity => (
is => 'lazy',
builder => sub {
my $self = shift;
my $entity = $self->schema->resultset('Entity')->find($self->target_entity_id);
Pear::LocalLoop::Error->throw("Cannot find LCC Entity, did you pass the right id?") unless $entity;
return $entity;
},
);
has '+csv_required_columns' => (
builder => sub {return [ (
'transaction_id',
'supplier_id',
'net_amount',
'vat amount',
'gross_amount',
) ]},
);
sub import_csv {
my ($self) = @_;
$self->check_headers;
my $lcc_org = $self->target_entity;
while ( my $row = $self->get_csv_line ) {
$self->_row_to_result($row, $lcc_org);
}
}
sub _row_to_result {
my ($self, $row, $lcc_org) = @_;
my $supplier_id = $row->{supplier_id};
my $organisation = $self->schema->resultset('Organisation')->find({
'external_reference.external_id' => $supplier_id
}, { join => 'external_reference' });
unless ($organisation) {
# Pear::LocalLoop::Error->throw("Cannot find an organisation with supplier_id $supplier_id");
return unless $row->{'Company Name (WHO)'};
my $town = $row->{post_town};
unless ($town) {
my $postcode_obj = Geo::UK::Postcode::Regex->parse( $row->{post_code} );
$town = Geo::UK::Postcode::Regex->outcode_to_posttowns($postcode_obj->{outcode});
$town = $town->[0];
}
return if $self->external_result->organisations->find({external_id => $row->{supplier_id}});
$organisation = $self->schema->resultset('Entity')->create({
type => 'organisation',
organisation => {
name => $row->{'Company Name (WHO)'},
street_name => $row->{"address line 1"},
town => $town,
postcode => $row->{post_code},
country => $row->{country_code},
external_reference => [ {
external_reference => $self->external_result,
external_id => $row->{supplier_id},
} ],
}
});
}
my $date_formatter = DateTime::Format::Strptime->new(
pattern => '%m/%d/%Y',
time_zone => 'Europe/London'
);
my $paid_date = ( $row->{paid_date} ?
$date_formatter->parse_datetime($row->{paid_date}) :
$date_formatter->parse_datetime($row->{invoice_date}) );
my $gross_value = $row->{gross_amount};
$gross_value =~ s/,//g;
my $sales_tax_value = $row->{"vat amount"};
$sales_tax_value =~ s/,//g;
my $net_value = $row->{net_amount};
$net_value =~ s/,//g;
# TODO negative values are sometimes present
my $external_transaction = $self->external_result->update_or_create_related('transactions', { # This is a TransactionExternal result
external_id => $row->{transaction_id},
});
my $transaction_result = $external_transaction->update_or_create_related( 'transaction', {
seller => $organisation->entity,
buyer => $lcc_org,
purchase_time => $paid_date,
value => $gross_value * 100000,
});
my $meta_result = $transaction_result->update_or_create_related('meta', {
gross_value => $gross_value * 100000,
sales_tax_value => $sales_tax_value * 100000,
net_value => $net_value * 100000,
($row->{"local service"} ? (local_service => $row->{"local service"}) : ()),
($row->{"regional service"} ? (regional_service => $row->{"regional service"}) : ()),
($row->{"national service"} ? (national_service => $row->{"national service"}) : ()),
($row->{"private household rebate"} ? (private_household_rebate => $row->{"private household rebate"}) : ()),
($row->{"business tax and rebate"} ? (business_tax_and_rebate => $row->{"business tax and rebate"}) : ()),
($row->{"stat loc gov"} ? (stat_loc_gov => $row->{"stat loc gov"}) : ()),
($row->{"central loc gov"} ? (central_loc_gov => $row->{"central loc gov"}) : ()),
});
}
1;

View File

@ -0,0 +1,88 @@
package Pear::LocalLoop::Import::Role::CSV;
use strict;
use warnings;
use Moo::Role;
use Text::CSV;
use Try::Tiny;
use Pear::LocalLoop::Error;
requires 'csv_required_columns';
has csv_file => (
is => 'ro',
predicate => 1,
);
has csv_string => (
is => 'ro',
predicate => 1,
);
has csv_error => (
is => 'ro',
predicate => 1,
);
has _csv_filehandle => (
is => 'lazy',
builder => sub {
my $self = shift;
my $fh;
if ( $self->has_csv_file ) {
open $fh, '<', $self->csv_file;
} elsif ( $self->has_csv_string ) {
my $string = $self->csv_string;
open $fh, '<', \$string;
} else {
die "Must provide csv_file or csv_string"
}
return $fh;
}
);
has text_csv_options => (
is => 'lazy',
builder => sub {
return {
binary => 1,
allow_whitespace => 1,
};
}
);
has _text_csv => (
is => 'lazy',
builder => sub {
return Text::CSV->new(shift->text_csv_options);
}
);
has csv_data => (
is => 'lazy',
builder => sub {
my $self = shift;
my $header_check = $self->check_headers;
return 0 unless $header_check;
return $self->_text_csv->getline_hr_all( $self->_csv_filehandle );
}
);
sub get_csv_line {
my $self = shift;
return $self->_text_csv->getline_hr( $self->_csv_filehandle );
}
sub check_headers {
my $self = shift;
my $req_headers = $self->csv_required_columns;
my @headers;
@headers = $self->_text_csv->header( $self->_csv_filehandle );
my %header_map = ( map { $_ => 1 } @headers );
for my $req_header ( @$req_headers ) {
next if $header_map{$req_header};
die "Require header [" . $req_header . "]";
}
return 1;
}
1;

View File

@ -0,0 +1,19 @@
package Pear::LocalLoop::Import::Role::ExternalName;
use strict;
use warnings;
use Moo::Role;
requires qw/
external_name
schema
/;
has external_result => (
is => 'lazy',
builder => sub {
my $self = shift;
return $self->schema->resultset('ExternalReference')->find_or_create({ name => $self->external_name });
}
);
1;

View File

@ -0,0 +1,11 @@
package Pear::LocalLoop::Import::Role::Schema;
use strict;
use warnings;
use Moo::Role;
has schema => (
is => 'ro',
required => 1,
);
1;

View File

@ -0,0 +1,24 @@
package Pear::LocalLoop::Plugin::Currency;
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ( $plugin, $app, $cong ) = @_;
$app->helper( parse_currency => sub {
my ( $c, $currency_string ) = @_;
my $value;
if ( $currency_string =~ /^£([\d.]+)/ ) {
$value = $1 * 1;
} elsif ( $currency_string =~ /^([\d.]+)/ ) {
$value = $1 * 1;
}
return $value;
});
$app->helper( format_currency_from_db => sub {
my ( $c, $value ) = @_;
return sprintf( '£%.2f', $value / 100000 );
});
}
1;

View File

@ -66,6 +66,7 @@ sub register {
$app->helper( format_iso_datetime => sub {
my ( $c, $datetime_obj ) = @_;
return unless defined $datetime_obj;
return $c->iso_datetime_parser->format_datetime(
$datetime_obj,
);

View File

@ -0,0 +1,40 @@
package Pear::LocalLoop::Plugin::Minion;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Loader qw/ find_modules load_class /;
sub register {
my ( $plugin, $app, $cong ) = @_;
if ( defined $app->config->{minion} ) {
$app->log->debug('Setting up Minion tasks');
$app->plugin('Minion' => $app->config->{minion} );
$app->log->debug('Loaded Minion Job packages:');
my $job_namespace = __PACKAGE__ . '::Job';
my @modules = find_modules $job_namespace;
for my $package ( @modules ) {
my ( $job_name ) = $package =~ /${job_namespace}::(.*)$/;
$app->log->debug( $package );
if (my $e = load_class $package) {
die ref $e ? "Exception: $e" : "$package not found";
}
$app->minion->add_task(
$job_name => sub {
my ( $job, @args ) = @_;
my $job_runner = $package->new(
job => $job,
);
$job_runner->run( @args );
}
);
}
# $app->minion->enqueue('test' => [ 'test arg 1', 'test_arg 2' ] );
} else {
$app->log->debug('No Minion Config');
}
}
1;

View File

@ -0,0 +1,12 @@
package Pear::LocalLoop::Plugin::Minion::Job;
use Mojo::Base -base;
has [ qw/ job / ];
has app => sub { shift->job->app };
sub run {
die ( __PACKAGE__ . " must implement run sub" );
}
1;

View File

@ -0,0 +1,15 @@
package Pear::LocalLoop::Plugin::Minion::Job::csv_postcode_import;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
use Pear::LocalLoop::Import::LCCCsv::Postcodes;
sub run {
my ( $self, $filename ) = @_;
my $csv_import = Pear::LocalLoop::Import::LCCCsv::Postcodes->new(
csv_file => $filename,
schema => $self->app->schema
)->import_csv;
}
1;

View File

@ -0,0 +1,15 @@
package Pear::LocalLoop::Plugin::Minion::Job::csv_supplier_import;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
use Pear::LocalLoop::Import::LCCCsv::Suppliers;
sub run {
my ( $self, $filename ) = @_;
my $csv_import = Pear::LocalLoop::Import::LCCCsv::Suppliers->new(
csv_file => $filename,
schema => $self->app->schema
)->import_csv;
}
1;

View File

@ -0,0 +1,16 @@
package Pear::LocalLoop::Plugin::Minion::Job::csv_transaction_import;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
use Pear::LocalLoop::Import::LCCCsv::Transactions;
sub run {
my ($self, $filename, $entity_id) = @_;
Pear::LocalLoop::Import::LCCCsv::Transactions->new(
csv_file => $filename,
schema => $self->app->schema,
target_entity_id => $entity_id,
)->import_csv;
}
1;

View File

@ -0,0 +1,30 @@
package Pear::LocalLoop::Plugin::Minion::Job::entity_postcode_lookup;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
sub run {
my ( $self, $entity_id ) = @_;
my $entity_rs = $self->app->schema->resultset('Entity');
$entity_rs = $entity_rs->search({id => $entity_id }) if $entity_id;
while ( my $entity = $entity_rs->next ) {
my $obj = $entity->type_object;
next unless $obj;
my $postcode_obj = Geo::UK::Postcode::Regex->parse( $obj->postcode );
unless ( defined $postcode_obj && $postcode_obj->{non_geographical} ) {
my $pc_result = $self->app->schema->resultset('GbPostcode')->find({
incode => $postcode_obj->{incode},
outcode => $postcode_obj->{outcode},
});
if ( defined $pc_result ) {
$entity->update_or_create_related('postcode', {
gb_postcode => $pc_result,
});
}
}
}
}
1;

View File

@ -0,0 +1,12 @@
package Pear::LocalLoop::Plugin::Minion::Job::leaderboards_recalc;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
sub run {
my ( $self, @args ) = @_;
my $leaderboard_rs = $self->app->schema->resultset('Leaderboard');
$leaderboard_rs->recalculate_all;
}
1;

View File

@ -0,0 +1,13 @@
package Pear::LocalLoop::Plugin::Minion::Job::test;
use Mojo::Base 'Pear::LocalLoop::Plugin::Minion::Job';
sub run {
my ( $self, @args ) = @_;
$self->job->app->log->debug( 'Testing Job' );
for my $arg ( @args ) {
$self->job->app->log->debug( $arg );
}
}
1;

View File

@ -6,7 +6,7 @@ use warnings;
use base 'DBIx::Class::Schema';
our $VERSION = 16;
our $VERSION = 30;
__PACKAGE__->load_namespaces;

View File

@ -0,0 +1,46 @@
package Pear::LocalLoop::Schema::Result::Category;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("category");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
# See here for all possible options http://simplelineicons.com/
"line_icon" => {
data_type => "varchar",
size => 255,
is_nullable => 1,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint(["name"]);
__PACKAGE__->has_many(
"transaction_category",
"Pear::LocalLoop::Schema::Result::TransactionCategory",
{ "foreign.category_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 1 },
);
__PACKAGE__->many_to_many(
"transactions",
"transaction_category",
"transaction",
);
1;

View File

@ -37,6 +37,16 @@ __PACKAGE__->might_have(
"Pear::LocalLoop::Schema::Result::User" => "entity_id",
);
__PACKAGE__->might_have(
"associations",
"Pear::LocalLoop::Schema::Result::EntityAssociation" => "entity_id",
);
__PACKAGE__->might_have(
"postcode",
"Pear::LocalLoop::Schema::Result::EntityPostcode" => "entity_id",
);
__PACKAGE__->has_many(
"purchases",
"Pear::LocalLoop::Schema::Result::Transaction",
@ -51,6 +61,34 @@ __PACKAGE__->has_many(
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"global_user_medals",
"Pear::LocalLoop::Schema::Result::GlobalUserMedals",
{ "foreign.entity_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"global_user_medal_progress",
"Pear::LocalLoop::Schema::Result::GlobalUserMedalProgress",
{ "foreign.entity_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"org_user_medals",
"Pear::LocalLoop::Schema::Result::OrgUserMedals",
{ "foreign.entity_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"org_user_medal_progress",
"Pear::LocalLoop::Schema::Result::OrgUserMedalProgress",
{ "foreign.entity_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
sub name {
my $self = shift;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::EntityAssociation;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("entity_association");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"entity_id" => {
data_type => 'integer',
is_nullable => 0,
is_foreign_key => 1,
},
"lis" => {
data_type => 'boolean',
default_value => undef,
is_nullable => 1,
},
"esta" => {
data_type => 'boolean',
default_value => undef,
is_nullable => 1,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);

View File

@ -0,0 +1,44 @@
package Pear::LocalLoop::Schema::Result::EntityPostcode;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table('entities_postcodes');
__PACKAGE__->add_columns(
outcode => {
data_type => 'char',
size => 4,
is_nullable => 0,
},
incode => {
data_type => 'char',
size => 3,
is_nullable => 0,
},
entity_id => {
data_type => 'integer',
is_nullable => 0,
is_foreign_key => 1,
},
);
__PACKAGE__->set_primary_key(qw/ outcode incode entity_id /);
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);
__PACKAGE__->belongs_to(
"gb_postcode",
"Pear::LocalLoop::Schema::Result::GbPostcode",
{
"foreign.outcode" => "self.outcode",
"foreign.incode" => "self.incode",
},
);
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::ExternalReference;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("external_references");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint([ qw/name/ ]);
__PACKAGE__->has_many(
'transactions',
"Pear::LocalLoop::Schema::Result::TransactionExternal",
{ 'foreign.external_reference_id' => 'self.id' },
);
__PACKAGE__->has_many(
'organisations',
"Pear::LocalLoop::Schema::Result::OrganisationExternal",
{ 'foreign.external_reference_id' => 'self.id' },
);
1;

View File

@ -31,8 +31,19 @@ __PACKAGE__->add_columns(
is_nullable => 1,
default_value => undef,
},
ward_id => {
data_type => 'integer',
is_nullable => 1,
default_value => undef,
},
);
__PACKAGE__->set_primary_key(qw/ outcode incode /);
__PACKAGE__->belongs_to(
"ward",
"Pear::LocalLoop::Schema::Result::GbWard",
"ward_id",
);
1;

View File

@ -0,0 +1,32 @@
package Pear::LocalLoop::Schema::Result::GbWard;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table('gb_wards');
__PACKAGE__->add_columns(
id => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
ward => {
data_type => 'varchar',
size => 100,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key(qw/ id /);
__PACKAGE__->has_many(
"postcodes",
"Pear::LocalLoop::Schema::Result::GbPostcode",
{ "foreign.ward_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
1;

View File

@ -0,0 +1,34 @@
package Pear::LocalLoop::Schema::Result::GlobalMedalGroup;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("global_medal_group");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"group_name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint(["group_name"]);
__PACKAGE__->has_many(
"medals",
"Pear::LocalLoop::Schema::Result::GlobalMedals",
{ "foreign.group_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::GlobalMedals;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("global_medals");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"threshold" => {
data_type => "integer",
is_nullable => 0,
},
"points" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::GlobalMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -0,0 +1,45 @@
package Pear::LocalLoop::Schema::Result::GlobalUserMedalProgress;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("global_user_medal_progress");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"entity_id" => {
data_type => "integer",
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"total" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::GlobalMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -0,0 +1,59 @@
package Pear::LocalLoop::Schema::Result::GlobalUserMedals;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
/);
__PACKAGE__->table("global_user_medals");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"entity_id" => {
data_type => "integer",
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"points" => {
data_type => "integer",
is_nullable => 0,
},
"awarded_at" => {
data_type => "datetime",
is_nullable => 0,
set_on_create => 1,
},
"threshold" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::GlobalMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -39,7 +39,7 @@ __PACKAGE__->add_columns(
size => 255,
},
transaction_id => {
data_type => 'varchar',
data_type => 'integer',
is_foreign_key => 1,
is_nullable => 1,
},

View File

@ -0,0 +1,34 @@
package Pear::LocalLoop::Schema::Result::OrgMedalGroup;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("org_medal_group");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"group_name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint(["group_name"]);
__PACKAGE__->has_many(
"medals",
"Pear::LocalLoop::Schema::Result::OrgMedals",
{ "foreign.group_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::OrgMedals;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("org_medals");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"threshold" => {
data_type => "integer",
is_nullable => 0,
},
"points" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::OrgMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -0,0 +1,45 @@
package Pear::LocalLoop::Schema::Result::OrgUserMedalProgress;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("org_user_medal_progress");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"entity_id" => {
data_type => "integer",
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"total" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::OrgMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -0,0 +1,59 @@
package Pear::LocalLoop::Schema::Result::OrgUserMedals;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
/);
__PACKAGE__->table("org_user_medals");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"entity_id" => {
data_type => "integer",
is_nullable => 0,
},
"group_id" => {
data_type => "integer",
is_nullable => 0,
},
"points" => {
data_type => "integer",
is_nullable => 0,
},
"awarded_at" => {
data_type => "datetime",
is_nullable => 0,
set_on_create => 1,
},
"threshold" => {
data_type => "integer",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
"entity_id",
);
__PACKAGE__->belongs_to(
"group",
"Pear::LocalLoop::Schema::Result::OrgMedalGroup",
{ id => "group_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -10,71 +10,92 @@ __PACKAGE__->load_components("InflateColumn::DateTime", "FilterColumn");
__PACKAGE__->table("organisations");
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
is_nullable => 0,
},
entity_id => {
data_type => 'integer',
is_nullable => 0,
entity_id => {
data_type => 'integer',
is_nullable => 0,
is_foreign_key => 1,
},
name => {
data_type => 'varchar',
size => 255,
name => {
data_type => 'varchar',
size => 255,
is_nullable => 0,
},
street_name => {
data_type => 'text',
street_name => {
data_type => 'text',
is_nullable => 1,
},
town => {
data_type => 'varchar',
size => 255,
town => {
data_type => 'varchar',
size => 255,
is_nullable => 0,
},
postcode => {
data_type => 'varchar',
size => 16,
postcode => {
data_type => 'varchar',
size => 16,
is_nullable => 1,
},
country => {
data_type => 'varchar',
size => 255,
country => {
data_type => 'varchar',
size => 255,
is_nullable => 1,
},
sector => {
data_type => 'varchar',
size => 1,
# Stores codes based on https://www.ons.gov.uk/methodology/classificationsandstandards/ukstandardindustrialclassificationofeconomicactivities/uksic2007
sector => {
data_type => 'varchar',
size => 1,
is_nullable => 1,
},
pending => {
data_type => 'boolean',
default => \"false",
pending => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
is_local => {
data_type => 'boolean',
default => undef,
is_local => {
data_type => 'boolean',
default_value => undef,
is_nullable => 1,
},
is_fair => {
data_type => 'boolean',
default_value => undef,
is_nullable => 1,
},
submitted_by_id => {
data_type => 'integer',
data_type => 'integer',
is_nullable => 1,
},
latitude => {
data_type => 'decimal',
size => [8,5],
is_nullable => 1,
latitude => {
data_type => 'decimal',
size => [ 8, 5 ],
is_nullable => 1,
default_value => undef,
},
longitude => {
data_type => 'decimal',
size => [8,5],
is_nullable => 1,
longitude => {
data_type => 'decimal',
size => [ 8, 5 ],
is_nullable => 1,
default_value => undef,
},
type_id => {
data_type => 'integer',
is_nullable => 1,
is_foreign_key => 1,
},
social_type_id => {
data_type => 'integer',
is_nullable => 1,
is_foreign_key => 1,
},
is_anchor => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'FALSE',
}
);
__PACKAGE__->set_primary_key('id');
@ -85,6 +106,24 @@ __PACKAGE__->belongs_to(
"entity_id",
);
__PACKAGE__->belongs_to(
"organisation_type",
"Pear::LocalLoop::Schema::Result::OrganisationType",
"type_id",
);
__PACKAGE__->belongs_to(
"social_type",
"Pear::LocalLoop::Schema::Result::OrganisationSocialType",
"social_type_id",
);
__PACKAGE__->has_many(
"external_reference",
"Pear::LocalLoop::Schema::Result::OrganisationExternal",
{ 'foreign.org_id' => 'self.id' },
);
__PACKAGE__->has_many(
"payroll",
"Pear::LocalLoop::Schema::Result::OrganisationPayroll",
@ -93,34 +132,45 @@ __PACKAGE__->has_many(
);
__PACKAGE__->filter_column(
pending => {
pending => {
filter_to_storage => 'to_bool',
},
is_local => {
is_local => {
filter_to_storage => 'to_bool',
},
is_anchor => {
filter_to_storage => 'to_bool',
}
);
# Only works when calling ->deploy, but atleast helps for tests
sub sqlt_deploy_hook {
my ( $source_instance, $sqlt_table ) = @_;
my ($source_instance, $sqlt_table) = @_;
my $pending_field = $sqlt_table->get_field('pending');
if ( $sqlt_table->schema->translator->producer_type =~ /SQLite$/ ) {
if ($sqlt_table->schema->translator->producer_type =~ /SQLite$/) {
$pending_field->{default_value} = 0;
} else {
}
else {
$pending_field->{default_value} = \"false";
}
}
sub to_bool {
my ( $self, $val ) = @_;
return if ! defined $val;
my ($self, $val) = @_;
return if !defined $val;
my $driver_name = $self->result_source->schema->storage->dbh->{Driver}->{Name};
if ( $driver_name eq 'SQLite' ) {
if ($driver_name eq 'SQLite') {
return $val ? 1 : 0;
} else {
}
else {
return $val ? 'true' : 'false';
}
}
sub user {
my $self = shift;
return $self->entity->user;
}
1;

View File

@ -0,0 +1,49 @@
package Pear::LocalLoop::Schema::Result::OrganisationExternal;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("organisations_external");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"org_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"external_reference_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"external_id" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
}
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint([ qw/external_reference_id external_id/ ]);
__PACKAGE__->belongs_to(
"organisation",
"Pear::LocalLoop::Schema::Result::Organisation",
{ 'foreign.id' => 'self.org_id' },
);
__PACKAGE__->belongs_to(
"external_reference",
"Pear::LocalLoop::Schema::Result::ExternalReference",
{ 'foreign.id' => 'self.external_reference_id' },
);
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::OrganisationSocialType;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("organisation_social_types");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"key" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
"name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
}
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint([ qw/key/ ]);
__PACKAGE__->has_many(
"organisations",
"Pear::LocalLoop::Schema::Result::Organisation",
{ 'foreign.social_type_id' => 'self.id' },
);
1;
1;

View File

@ -0,0 +1,38 @@
package Pear::LocalLoop::Schema::Result::OrganisationType;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("organisation_types");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"key" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
"name" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
}
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint([ qw/key/ ]);
__PACKAGE__->has_many(
"organisations",
"Pear::LocalLoop::Schema::Result::Organisation",
{ 'foreign.type_id' => 'self.id' },
);
1;

View File

@ -48,6 +48,11 @@ __PACKAGE__->add_columns(
is_nullable => 0,
set_on_create => 1,
},
"essential" => {
data_type => "boolean",
default_value => \"false",
is_nullable => 0,
},
distance => {
data_type => 'numeric',
size => [15],
@ -71,4 +76,31 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->might_have(
"category",
"Pear::LocalLoop::Schema::Result::TransactionCategory" => "transaction_id",
);
__PACKAGE__->has_one(
"meta",
"Pear::LocalLoop::Schema::Result::TransactionMeta",
{ 'foreign.transaction_id' => 'self.id' },
);
__PACKAGE__->has_many(
"external_reference",
"Pear::LocalLoop::Schema::Result::TransactionExternal",
{ 'foreign.transaction_id' => 'self.id' },
);
sub sqlt_deploy_hook {
my ( $source_instance, $sqlt_table ) = @_;
my $pending_field = $sqlt_table->get_field('essential');
if ( $sqlt_table->schema->translator->producer_type =~ /SQLite$/ ) {
$pending_field->{default_value} = 0;
} else {
$pending_field->{default_value} = \"false";
}
}
1;

View File

@ -0,0 +1,39 @@
package Pear::LocalLoop::Schema::Result::TransactionCategory;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("transaction_category");
__PACKAGE__->add_columns(
"category_id" => {
data_type => "integer",
is_nullable => 0,
is_foreign_key => 1,
},
"transaction_id" => {
data_type => 'integer',
is_nullable => 0,
is_foreign_key => 1,
},
);
__PACKAGE__->add_unique_constraint(["transaction_id"]);
__PACKAGE__->belongs_to(
"category",
"Pear::LocalLoop::Schema::Result::Category",
"category_id",
{ cascade_delete => 0 },
);
__PACKAGE__->belongs_to(
"transaction",
"Pear::LocalLoop::Schema::Result::Transaction",
"transaction_id",
{ cascade_delete => 0 },
);
1;

View File

@ -0,0 +1,49 @@
package Pear::LocalLoop::Schema::Result::TransactionExternal;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("transactions_external");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"transaction_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"external_reference_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"external_id" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
}
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint([ qw/external_reference_id external_id/ ]);
__PACKAGE__->belongs_to(
"transaction",
"Pear::LocalLoop::Schema::Result::Transaction",
{ 'foreign.id' => 'self.transaction_id' },
);
__PACKAGE__->belongs_to(
"external_reference",
"Pear::LocalLoop::Schema::Result::ExternalReference",
{ 'foreign.id' => 'self.external_reference_id' },
);
1;

View File

@ -0,0 +1,81 @@
package Pear::LocalLoop::Schema::Result::TransactionMeta;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("transactions_meta");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"transaction_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"net_value" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"sales_tax_value" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"gross_value" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"local_service" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"regional_service" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"national_service" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"private_household_rebate" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"business_tax_and_rebate" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"stat_loc_gov" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
"central_loc_gov" => {
data_type => 'boolean',
default_value => \"false",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"transaction",
"Pear::LocalLoop::Schema::Result::Transaction",
{ 'foreign.id' => 'self.transaction_id' },
);
1;

View File

@ -0,0 +1,92 @@
package Pear::LocalLoop::Schema::Result::TransactionRecurring;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
/);
__PACKAGE__->table("transaction_recurring");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"buyer_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"seller_id" => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
"value" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"start_time" => {
data_type => "datetime",
timezone => "UTC",
is_nullable => 0,
},
"last_updated" => {
data_type => "datetime",
timezone => "UTC",
is_nullable => 1,
datetime_undef_if_invalid => 1,
},
"essential" => {
data_type => "boolean",
default_value => \"false",
is_nullable => 0,
},
"distance" => {
data_type => 'numeric',
size => [15],
is_nullable => 1,
},
"category_id" => {
data_type => "integer",
is_nullable => 1,
is_foreign_key => 1,
},
"recurring_period" => {
data_type => "varchar",
size => 255,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"buyer",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "buyer_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->belongs_to(
"seller",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "seller_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->belongs_to(
"category",
"Pear::LocalLoop::Schema::Result::Category",
"category_id",
{ cascade_delete => 0 },
);
1;

View File

@ -0,0 +1,27 @@
package Pear::LocalLoop::Schema::Result::ViewQuantisedTransactionCategoryPg;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('view_quantised_transactions');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition( qq/
SELECT "transactions"."value",
"transactions"."distance",
"transactions"."purchase_time",
"transactions"."buyer_id",
"transactions"."seller_id",
"transactions"."essential",
"transaction_category"."category_id",
DATE_TRUNC('hour', "transactions"."purchase_time") AS "quantised_hours",
DATE_TRUNC('day', "transactions"."purchase_time") AS "quantised_days",
DATE_TRUNC('week', "transactions"."purchase_time") AS "quantised_weeks"
FROM "transactions"
LEFT JOIN "transaction_category" ON "transactions"."id" = "transaction_category"."transaction_id"
/);
1;

View File

@ -0,0 +1,27 @@
package Pear::LocalLoop::Schema::Result::ViewQuantisedTransactionCategorySQLite;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('view_quantised_transactions');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition( qq/
SELECT "transactions"."value",
"transactions"."distance",
"transactions"."purchase_time",
"transactions"."buyer_id",
"transactions"."seller_id",
"transactions"."essential",
"transaction_category"."category_id",
DATETIME(STRFTIME('%Y-%m-%d %H:00:00',"transactions"."purchase_time")) AS "quantised_hours",
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"transactions"."purchase_time")) AS "quantised_days",
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"transactions"."purchase_time", 'weekday 0','-6 days')) AS "quantised_weeks"
FROM "transactions"
LEFT JOIN "transaction_category" ON "transactions"."id" = "transaction_category"."transaction_id"
/);
1;

View File

@ -13,9 +13,27 @@ __PACKAGE__->result_source_instance->view_definition( qq/
SELECT "value",
"distance",
"purchase_time",
"buyer_id",
"seller_id",
DATE_TRUNC('hour', "purchase_time") AS "quantised_hours",
DATE_TRUNC('day', "purchase_time") AS "quantised_days"
DATE_TRUNC('day', "purchase_time") AS "quantised_days",
DATE_TRUNC('week', "purchase_time") AS "quantised_weeks",
DATE_TRUNC('month', "purchase_time") AS "quantised_months"
FROM "transactions"
/);
__PACKAGE__->belongs_to(
"buyer",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "buyer_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->belongs_to(
"seller",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "seller_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -13,9 +13,28 @@ __PACKAGE__->result_source_instance->view_definition( qq/
SELECT "value",
"distance",
"purchase_time",
"buyer_id",
"seller_id",
"sector",
DATETIME(STRFTIME('%Y-%m-%d %H:00:00',"purchase_time")) AS "quantised_hours",
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time")) AS "quantised_days"
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time")) AS "quantised_days",
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time",'weekday 0','-6 days')) AS "quantised_weeks",
DATETIME(STRFTIME('%Y-%m-00 00:00:00',"purchase_time")) AS "quantised_months"
FROM "transactions"
/);
__PACKAGE__->belongs_to(
"buyer",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "buyer_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->belongs_to(
"seller",
"Pear::LocalLoop::Schema::Result::Entity",
{ id => "seller_id" },
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
1;

View File

@ -0,0 +1,36 @@
package Pear::LocalLoop::Schema::ResultSet::Category;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';
sub as_hash {
my ( $self ) = @_;
my %category_list = (
(
map {
$_->id => $_->name,
} $self->all
),
0 => 'Uncategorised',
);
return \%category_list;
}
sub as_hash_name_icon {
my ( $self ) = @_;
my %category_list = (
(
map {
$_->name => $_->line_icon,
} $self->all
),
0 => 'Uncategorised',
);
return \%category_list;
}
1;

View File

@ -8,34 +8,54 @@ use base 'DBIx::Class::ResultSet';
sub get_values {
my $self = shift;
my $id = shift;
my $include_ignored = shift;
my $include_imported = shift;
return $self->find($id)->search_related(
'values',
undef,
{
( $include_ignored ? () : ( ignore_value => 0 ) ),
( $include_imported ? () : ( transaction_id => undef ) ),
},
{
order_by => { '-asc' => 'id' },
},
);
}
sub get_users {
sub _unordered_get_values {
my $self = shift;
my $id = shift;
my $include_ignored = shift;
my $include_imported = shift;
return $self->get_values($id)->search({},
return $self->find($id)->search_related(
'values',
{
( $include_ignored ? () : ( ignore_value => 0 ) ),
( $include_imported ? () : ( transaction_id => undef ) ),
},
);
}
sub get_users {
my $self = shift;
return $self->_unordered_get_values(@_)->search({},
{
group_by => 'user_name',
columns => [ qw/ user_name / ],
},
);
}
sub get_orgs {
my $self = shift;
my $id = shift;
return $self->get_values($id)->search({},
return $self->_unordered_get_values(@_)->search({},
{
group_by => 'org_name',
columns => [ qw/ org_name / ],
},
);
}
@ -44,13 +64,23 @@ sub get_lookups {
my $self = shift;
my $id = shift;
return $self->find($id)->search_related(
my $lookup_rs = $self->find($id)->search_related(
'lookups',
undef,
{
order_by => { '-asc' => 'id' },
prefetch => { entity => [ qw/ organisation customer / ] },
order_by => { '-asc' => 'me.id' },
},
);
my $lookup_map = {
map {
$_->name => {
entity_id => $_->entity->id,
name => $_->entity->name,
},
} $lookup_rs->all
};
return $lookup_map;
}
1;

View File

@ -114,6 +114,8 @@ sub dump_error {
my $self = shift;
if ( my $error = $self->tx->res->dom->at('pre[id="error"]') ) {
diag $error->text;
} elsif ( my $route_error = $self->tx->res->dom->at('div[id="routes"] > p') ) {
diag $route_error->content;
} else {
diag $self->tx->res->to_string;
}

View File

@ -3,4 +3,7 @@
user => undef,
pass => undef,
key => "a",
};
minion => {
SQLite => 'sqlite:minion.db',
},
};

View File

@ -4,4 +4,7 @@
user => undef,
pass => undef,
key => "a",
minion => {
SQLite => 'sqlite:minion.db',
},
};

10
script/cron_daily Normal file
View File

@ -0,0 +1,10 @@
#! /bin/bash
# Scripts to run daily.
# This will be run sometime between 2 & 3AM every morning.
# If order matters, make sure they are in the right place.
eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
MOJO_MODE=production ./script/pear-local_loop recur_transactions --force
MOJO_MODE=production ./script/pear-local_loop recalc_leaderboards

View File

@ -5,7 +5,10 @@ use warnings;
use FindBin qw/ $Bin /;
use lib "$Bin/../lib";
use lib "$Bin/..";
use Devel::Dwarn;
Dwarn $Bin;
use Pear::LocalLoop::Schema::Script::DeploymentHandler;
Pear::LocalLoop::Schema::Script::DeploymentHandler->new_with_actions(schema_class => 'Pear::LocalLoop::Schema');

View File

@ -1,5 +0,0 @@
#! /bin/bash
eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
MOJO_MODE=production ./script/pear-local_loop recalc_leaderboards

View File

@ -206,7 +206,7 @@ CREATE TABLE "import_values" (
"purchase_date" timestamp NOT NULL,
"purchase_value" character varying(255) NOT NULL,
"org_name" character varying(255) NOT NULL,
"transaction_id" character varying,
"transaction_id" integer,
PRIMARY KEY ("id")
);
CREATE INDEX "import_values_idx_set_id" on "import_values" ("set_id");

View File

@ -206,7 +206,7 @@ CREATE TABLE "import_values" (
"purchase_date" timestamp NOT NULL,
"purchase_value" character varying(255) NOT NULL,
"org_name" character varying(255) NOT NULL,
"transaction_id" character varying,
"transaction_id" integer,
"ignore_value" boolean DEFAULT false NOT NULL,
PRIMARY KEY ("id")
);

View File

@ -220,7 +220,7 @@ CREATE TABLE "import_values" (
"purchase_date" timestamp NOT NULL,
"purchase_value" character varying(255) NOT NULL,
"org_name" character varying(255) NOT NULL,
"transaction_id" character varying,
"transaction_id" integer,
"ignore_value" boolean DEFAULT false NOT NULL,
PRIMARY KEY ("id")
);

View File

@ -0,0 +1,18 @@
--
-- Created by SQL::Translator::Producer::PostgreSQL
-- Created on Thu Nov 23 14:08:17 2017
--
;
--
-- Table: dbix_class_deploymenthandler_versions
--
CREATE TABLE "dbix_class_deploymenthandler_versions" (
"id" serial NOT NULL,
"version" character varying(50) NOT NULL,
"ddl" text,
"upgrade_sql" text,
PRIMARY KEY ("id"),
CONSTRAINT "dbix_class_deploymenthandler_versions_version" UNIQUE ("version")
);
;

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