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 generated 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 generated 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>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

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

20
.idea/dataSources.xml generated 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 generated 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 generated 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 generated 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 generated 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