Compare commits

...

401 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
72929bf86b Added assigning users to import values 2017-11-13 19:00:34 +00:00
Tom Bloor
9096bef00d Further work on import functions 2017-11-13 13:30:33 +00:00
Tom Bloor
593efcedfa Schema update for v 16 to include import lookups 2017-11-13 13:02:22 +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
Tom Bloor
40dbd1eb50 Added ignore_value boolean to Import Value table 2017-10-27 12:10:52 +01:00
Tom Bloor
e09974f0d9 First pass of importing data 2017-10-26 17:01:23 +01:00
Tom Bloor
eebac70276 update changelog 2017-10-24 17:56:24 +01:00
Tom Bloor
3e3f667053 Rework transaction views 2017-10-24 17:55:49 +01:00
Tom Bloor
751c6e64f2 Updated Changelog 2017-10-24 15:28:40 +01:00
Tom Bloor
ef36cbc541 Add setting of is_local to organisations 2017-10-24 15:23:09 +01:00
Tom Bloor
d881b3f95f Added is_local flag to organisation table 2017-10-24 13:47:41 +01:00
Tom Bloor
dcd5896cea Added pagination to organisation listings 2017-10-24 13:31:04 +01:00
Tom Bloor
ed8607fd35 Combine organisation lists into one with badges for warnings 2017-10-23 17:44:38 +01:00
Tom Bloor
11d6785062 Fix mobile breakpoints for feedback view 2017-10-23 16:14:50 +01:00
Tom Bloor
de90fe1f77 Added toggling of actioned on feedback, and view change for lists 2017-10-23 16:09:20 +01:00
Tom Bloor
0302a3d299 Upgrade to have actioned boolean on feedback - also fix boolean
deployment defaults
2017-10-23 15:46:30 +01:00
Tom Bloor
3d31e94367 Added feedback id number to feedback view 2017-10-23 15:08:51 +01:00
Tom Bloor
6c798d8d3f Word wrap feedback in admin console 2017-10-23 15:02:18 +01:00
Tom Bloor
afac877d12 Merge pull request #68 from Pear-Trading/TBSliver/Transaction-Distance
Transaction Distance Calculation
2017-10-04 16:12:36 +01:00
Tom Bloor
62881a0eda Added transaction distance calculation 2017-10-03 18:08:30 +01:00
Tom Bloor
37e8f0b46a Updated to allow for user updates to change location by postcode 2017-10-03 15:47:05 +01:00
Tom Bloor
202deb9178 Added output of location on User endpoint, and setting lat/long on user
registration
2017-10-03 15:33:43 +01:00
Tom Bloor
f1e3756075 updated Geo::UK::Postcode::Regex 2017-10-03 15:27:05 +01:00
Tom Bloor
5dafed5054 Merge pull request #67 from Pear-Trading/master
Master merge for v0.9.4
2017-10-03 11:43:29 +01:00
Tom Bloor
8f699c25c7 Merge pull request #66 from Pear-Trading/Release-v0.9.4
Release v0.9.4
2017-10-02 14:41:34 +01:00
Tom Bloor
1a54bae4e9 Updated changelog for next release 2017-10-02 14:27:10 +01:00
Tom Bloor
ede5fe1cee Merge pull request #65 from Pear-Trading/TBSliver/Mobile-Admin-View
Admin area improvements
2017-10-02 14:26:10 +01:00
Tom Bloor
9e127b8851 Finalised Transaction report graph and test 2017-10-02 14:08:24 +01:00
Tom Bloor
9631c78f57 Added changelog comments 2017-09-29 16:26:40 +01:00
Tom Bloor
13b594e64f Added footer with current git version shown 2017-09-29 16:21:24 +01:00
Tom Bloor
e667947c65 Fix uninitialised warning 2017-09-29 16:14:02 +01:00
Tom Bloor
a739b02eed Allow for day scale of transaction report 2017-09-29 16:04:41 +01:00
Tom Bloor
bf74fcc44f Redo all admin pages for bootstrap 4 beta 2017-09-29 15:52:34 +01:00
Tom Bloor
cf2c7dcc4b Initial pass of Transaction reports 2017-09-29 14:45:44 +01:00
Tom Bloor
2486c701a5 Improve feedback list some more 2017-09-28 17:10:13 +01:00
Tom Bloor
9cc1358283 improve layout of feedback reading 2017-09-28 17:03:26 +01:00
Tom Bloor
c3519fa4df Improved Feedback list page 2017-09-28 16:39:16 +01:00
Tom Bloor
4beb26a0a4 Added mobile view meta tag for admin view 2017-09-28 15:51:56 +01:00
Tom Bloor
90d1eeeb21 Merge pull request #64 from Pear-Trading/master
Release v0.9.3
2017-09-28 15:15:49 +01:00
Tom Bloor
0198d5364a Merge branch 'Release-v0.9.3' 2017-09-28 14:44:47 +01:00
Tom Bloor
2dcb591c19 Update Changelog with next version number 2017-09-28 14:44:18 +01:00
Tom Bloor
005c4694ff Merge pull request #63 from Pear-Trading/TBSliver/Postcode-Location
Added lat/long to customers and organisations
2017-09-28 14:42:57 +01:00
Tom Bloor
85a51b8955 modified Changelog 2017-09-28 14:30:19 +01:00
Tom Bloor
0dcb082786 Adding GIS::Distance dependency 2017-09-27 18:07:50 +01:00
Tom Bloor
5e078f8f8b Added supplier location endpoint 2017-09-27 18:01:06 +01:00
Tom Bloor
df90f62b7a Added full test data fixture setup 2017-09-26 17:49:16 +01:00
Tom Bloor
65d509943c Added postgres flags for testing and deps for cpanfile 2017-09-26 17:03:27 +01:00
Tom Bloor
5334d88a81 Move map css into admin main css 2017-09-22 17:35:24 +01:00
Tom Bloor
0486d910ee Added script for setting up lat/long in db and showing in admin panels 2017-09-22 12:35:12 +01:00
Tom Bloor
bfcacbee32 Added schema upgrade for transaction distance 2017-09-22 12:32:02 +01:00
Tom Bloor
1a0dbf7098 Added production config to gitignore 2017-09-22 12:31:43 +01:00
Tom Bloor
62cad2d70e Added distance to transaction column 2017-09-21 17:25:55 +01:00
Tom Bloor
e2e4e4769d Added upgrade DDL for lat-long on customers and orgs 2017-09-21 17:06:50 +01:00
Tom Bloor
a810795ca5 Added lat and long to customers and organisations 2017-09-21 17:01:10 +01:00
Tom Bloor
ad51891f51 Made script able to import into database 2017-09-21 16:57:14 +01:00
Tom Bloor
db1bbab10f Upgrade for postcode data table 2017-09-21 16:47:02 +01:00
Tom Bloor
3e87326265 Added otucode listings for codepoint_open command 2017-09-21 15:52:01 +01:00
Tom Bloor
f056199ec1 Merge branch 'development' into TBSliver/Postcode-Location 2017-09-21 15:49:27 +01:00
Tom Bloor
5a80d57ca2 Added table for storing postcodes 2017-09-21 15:48:25 +01:00
Tom Bloor
119774e434 First pass of codepoint_open importer 2017-09-21 15:17:45 +01:00
Tom Bloor
5b21fb669b Added codepoint open data to etc. dir with LICENCE file 2017-09-21 13:15:20 +01:00
Finn
d126233c8e Merge pull request #62 from Pear-Trading/finn/transactionlogdatefix
Added datetime format
2017-09-20 11:00:34 +01:00
Finn
22bdb625f0 Added datetime format 2017-09-19 17:40:17 +01:00
Finn
09b8efe655 Merge pull request #61 from Pear-Trading/finn/OrgDataAPI
Payroll info submit and read added
2017-09-19 17:31:37 +01:00
Finn
5f241bbd46 Tests fixed and API improved and DB upgraded 2017-09-19 17:23:30 +01:00
Finn
8e2cecfcf5 added read API 2017-09-19 15:50:58 +01:00
Finn
813f4af63f New routes added 2017-09-19 14:41:11 +01:00
Finn
78964d7297 Made endpoints org only 2017-09-18 17:31:29 +01:00
Finn
e388d360ac Changed endpoints in API 2017-09-18 17:26:35 +01:00
Finn
616181def3 Payroll sumission API improved and test added 2017-09-18 17:13:18 +01:00
Finn
b224182e61 Merge branch 'development' into finn/OrgDataAPI 2017-09-18 15:39:14 +01:00
Finn
7366c5035b Merge pull request #60 from Pear-Trading/finn/FeedbackErrors
Added error rewrites and added feedback test
2017-09-18 14:00:55 +01:00
Finn
9cb4f7c782 Added error rewrites and added feedback test 2017-09-18 13:31:30 +01:00
Finn
3b9383fc3d Merge pull request #59 from Pear-Trading/finn/betterErrors
Errors for API improved
2017-09-18 12:48:15 +01:00
Finn
5b3ef1bf27 Tests fixed 2017-09-18 12:05:30 +01:00
Finn
9e56383b46 User and Register API errors improved 2017-09-18 11:34:22 +01:00
Finn
dd36cd0c0c Feedback API error improved 2017-09-18 11:31:57 +01:00
Finn
414acd76fb Transaction API errors improved 2017-09-18 11:11:53 +01:00
Tom Bloor
f59cf88b09 Merge branch 'Release-v0.9.2' into development 2017-09-15 13:14:43 +01:00
Finn
26a37904d2 Naming sanitised 2017-09-08 13:00:42 +01:00
Finn
9cfd5536b9 Removed redundant code and added error messages 2017-09-08 12:15:11 +01:00
Finn
662219fc6e Initial API for submission added 2017-09-08 11:41:21 +01:00
405 changed files with 79110 additions and 570 deletions

8
.gitignore vendored
View file

@ -2,9 +2,17 @@
myapp.conf
hypnotoad.pid
*.db
*.db-wal
*.db-shm
*.db-journal
*~
/images
*.swp
/upload
cover_db/
schema.png
etc/code-point-open/codepo_gb/
pear-local_loop.production.conf

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,6 +2,141 @@
# 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
* Distance is now calculated when a transaction is submitted
## Bug Fixes
* Updated Geo::UK::Postcode::Regex dependency to latest version. Fixes postcode
validation errors
# v0.9.4
* **Admin Feature:** Report of transaction data graphs
* **Fix:** Mobile view meta tag for admin
* Upgrade all CSS to Bootstrap 4 beta
* **Admin Feature:** Added version number to admin console
# v0.9.3
* **Feature:** lat/long locations on customers and organisations
* **Feature:** Suppliers map co-ords
# v0.9.2
* **Fix:** Leaderboard total calculations not mapped correctly

143
README.md
View file

@ -5,3 +5,146 @@
*Master:* [![Build Status](https://travis-ci.org/Pear-Trading/Foodloop-Server.svg?branch=master)](https://travis-ci.org/Pear-Trading/Foodloop-Server)
*Development:* [![Build Status](https://travis-ci.org/Pear-Trading/Foodloop-Server.svg?branch=development)](https://travis-ci.org/Pear-Trading/Foodloop-Server)
# Testing
To run the main test framework, first install all the dependencies, then run the tests:
```
cpanm --installdeps .
prove -lr
```
To run the main framework against a PostgreSQL backend, assuming you have postgres installed, you will need some extra dependencies first:
```
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

@ -4,9 +4,8 @@ requires 'Data::UUID';
requires 'Devel::Dwarn';
requires 'Mojo::JSON';
requires 'Email::Valid';
requires 'Geo::UK::Postcode::Regex';
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,14 +14,39 @@ 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';
requires 'DBIx::Class::DeploymentHandler';
requires 'DBIx::Class::Fixtures';
requires 'GIS::Distance';
requires 'Text::CSV';
requires 'Try::Tiny';
requires 'Throwable::Error';
requires 'Minion';
on 'schema-graph' => sub {
on 'test' => sub {
requires 'Test::More';
requires 'Test::MockTime';
};
feature 'schema-graph', 'Draw diagrams of Schema' => sub {
requires 'GraphViz';
requires 'SQL::Translator';
};
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

@ -17,7 +17,7 @@
* email: test4@example.com
* password: abc123
* Test Org
* email: test5@example.com
* email: org@example.com
* password: abc123
* Test Admin
* email: admin@example.com

View file

@ -0,0 +1,10 @@
ORDNANCE SURVEY DATA LICENCE
Your use of data is subject to terms at www.ordnancesurvey.co.uk/opendata/licence.
Contains Ordnance Survey data © Crown copyright and database right 2017.
Contains Royal Mail data © Royal Mail copyright and database right 2017.
Contains National Statistics data © Crown copyright and database right 2017.
August 2017

Binary file not shown.

View file

@ -21,21 +21,36 @@ has schema => sub {
sub startup {
my $self = shift;
my $version = `git describe --tags`;
$self->plugin('Config', {
default => {
storage_path => tempdir,
upload_path => $self->home->child('upload'),
sessionTimeSeconds => 60 * 60 * 24 * 7,
sessionTokenJsonName => 'session_key',
sessionExpiresJsonName => 'sessionExpires',
version => $version,
},
});
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 {
@ -67,7 +82,7 @@ sub startup {
json => {
success => Mojo::JSON->false,
message => $c->error_messages->{$val}->{$check}->{message},
error => $check,
error => $c->error_messages->{$val}->{$check}->{error} || $check,
},
status => $c->error_messages->{$val}->{$check}->{status},
);
@ -138,22 +153,67 @@ 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');
$api_v1_org->post('/graphs')->to('api-v1-organisation-graphs#index');
$api_v1_org->post('/snippets')->to('api-v1-organisation-snippets#index');
$api_v1_org->post('/payroll')->to('api-organisation#post_payroll_read');
$api_v1_org->post('/payroll/add')->to('api-organisation#post_payroll_add');
$api_v1_org->post('/supplier')->to('api-organisation#post_supplier_read');
$api_v1_org->post('/supplier/add')->to('api-organisation#post_supplier_add');
$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');
@ -162,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');
@ -172,15 +238,37 @@ 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');
$admin_routes->get('/feedback/:id/actioned')->to('admin-feedback#actioned');
$admin_routes->get('/transactions')->to('admin-transactions#index');
$admin_routes->get('/transactions/:id')->to('admin-transactions#read');
$admin_routes->get('/transactions/:id/image')->to('admin-transactions#image');
$admin_routes->post('/transactions/:id/delete')->to('admin-transactions#delete');
$admin_routes->get('/reports/transactions')->to('admin-reports#transaction_data');
$admin_routes->get('/import')->to('admin-import#index');
$admin_routes->get('/import/add')->to('admin-import#get_add');
$admin_routes->post('/import/add')->to('admin-import#post_add');
$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->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');
@ -191,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

@ -0,0 +1,69 @@
package Pear::LocalLoop::Command::codepoint_open;
use Mojo::Base 'Mojolicious::Command';
use Mojo::Util 'getopt';
use Geo::UK::Postcode::CodePointOpen;
has description => 'Manage Codepoint Open Data';
has usage => sub { shift->extract_usage };
sub run {
my ( $self, @args ) = @_;
getopt \@args,
'o|outcodes=s' => \my @outcodes,
'q|quiet' => \my $quiet_mode;
my $cpo_dir = $self->app->home->child('etc')->child('code-point-open');
my $zip_file = $cpo_dir->child('codepo_gb.zip')->realpath->to_string;
my $output_dir = $cpo_dir->child('codepo_gb')->realpath->to_string;
unless ( -d $output_dir ) {
print "Unzipping code-point-open data\n" unless $quiet_mode;
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 );
printf( "Importing data for %s outcode(s)\n", @outcodes ? join( ' ', @outcodes ) : 'all' )
unless $quiet_mode;
my $iter = $cpo->read_iterator(
outcodes => \@outcodes,
include_lat_long => 1,
split_postcode => 1,
);
my $pc_rs = $self->app->schema->resultset('GbPostcode');
while ( my $pc = $iter->() ) {
$pc_rs->find_or_create(
{
outcode => $pc->{Outcode},
incode => $pc->{Incode},
latitude => $pc->{Latitude},
longitude => $pc->{Longitude},
},
{ key => 'primary' },
);
}
}
=head1 SYNOPSIS
Usage: APPLICATION codepoint_open [OPTIONS]
Options:
-o|--outcodes <outcode> : limit to specified outcodes (can be defined
multiple times)
=cut
1;

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,83 @@
package Pear::LocalLoop::Command::latlong_setup;
use Mojo::Base 'Mojolicious::Command';
use Mojo::Util 'getopt';
use Geo::UK::Postcode::Regex;
use GIS::Distance;
has description => 'Set lat/long data on customers and orgs';
has usage => sub { shift->extract_usage };
sub run {
my ( $self, @args ) = @_;
my $customer_rs = $self->app->schema->resultset('Customer');
my $org_rs = $self->app->schema->resultset('Organisation');
for my $result ( $customer_rs->all, $org_rs->all ) {
$self->_set_lat_long_for_result( $result );
}
my $transaction_rs = $self->app->schema->resultset('Transaction');
for my $result ( $transaction_rs->all ) {
my $distance = $self->_calculate_distance(
$result->buyer->${\$result->buyer->type},
$result->seller->${\$result->seller->type},
);
$result->update({ distance => $distance }) if defined $distance;
}
}
sub _set_lat_long_for_result {
my ( $self, $result ) = @_;
my $parsed_postcode = Geo::UK::Postcode::Regex->parse($result->postcode);
my $pc_rs = $self->app->schema->resultset('GbPostcode');
if ( $parsed_postcode->{valid} && !$parsed_postcode->{non_geographical} ) {
my $gb_pc = $pc_rs->find({
outcode => $parsed_postcode->{outcode},
incode => $parsed_postcode->{incode},
});
if ( $gb_pc ) {
$result->update({
latitude => $gb_pc->latitude,
longitude => $gb_pc->longitude,
});
}
}
}
sub _calculate_distance {
my ( $self, $buyer, $seller ) = @_;
my $gis = GIS::Distance->new();
my $buyer_lat = $buyer->latitude;
my $buyer_long = $buyer->longitude;
my $seller_lat = $seller->latitude;
my $seller_long = $seller->longitude;
if ( $buyer_lat && $buyer_long
&& $seller_lat && $seller_long ) {
return $gis->distance( $buyer_lat, $buyer_long => $seller_lat, $seller_long )->meters;
} else {
print STDERR "missing lat-long for: " . $buyer->name . " or " . $seller->name . "\n";
}
return;
}
=head1 SYNOPSIS
Usage: APPLICATION latlong_setup [OPTIONS]
Options:
none for now
=cut
1;

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

@ -7,7 +7,7 @@ sub under {
if ( $c->is_user_authenticated ) {
return 1 if $c->current_user->is_admin;
}
$c->redirect_to('/');
$c->redirect_to('/admin');
return 0;
}
@ -18,6 +18,8 @@ sub home {
my $token_rs = $c->schema->resultset('AccountToken');
my $pending_orgs_rs = $c->schema->resultset('Organisation')->search({ pending => 1 });
my $pending_transaction_rs = $pending_orgs_rs->entity->sales;
my $feedback_rs = $c->schema->resultset('Feedback');
my $pending_feedback_rs = $feedback_rs->search({ actioned => 0 });
$c->stash(
user_count => $user_rs->count,
tokens => {
@ -26,15 +28,22 @@ sub home {
},
pending_orgs => $pending_orgs_rs->count,
pending_trans => $pending_transaction_rs->count,
feedback => {
total => $feedback_rs->count,
pending => $pending_feedback_rs->count,
},
);
}
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

@ -9,8 +9,15 @@ has result_set => sub {
sub index {
my $c = shift;
my $feedback_rs = $c->result_set;
$c->stash( feedbacks => [ $feedback_rs->all ] );
my $feedback_rs = $c->result_set->search(
undef,
{
page => $c->param('page') || 1,
rows => 12,
order_by => { -desc => 'submitted_at' },
},
);
$c->stash( feedback_rs => $feedback_rs );
}
sub read {
@ -26,4 +33,19 @@ sub read {
}
}
sub actioned {
my $c = shift;
my $id = $c->param('id');
if ( my $feedback = $c->result_set->find($id) ) {
$feedback->actioned( ! $feedback->actioned );
$feedback->update;
$c->redirect_to( '/admin/feedback/' . $id );
} else {
$c->flash( error => 'No Feedback found' );
$c->redirect_to( '/admin/feedback' );
}
}
1;

View file

@ -0,0 +1,326 @@
package Pear::LocalLoop::Controller::Admin::Import;
use Mojo::Base 'Mojolicious::Controller';
use Text::CSV;
use Try::Tiny;
has result_set => sub {
my $c = shift;
return $c->schema->resultset('ImportSet');
};
sub index {
my $c = shift;
my $import_rs = $c->result_set->search(
undef,
{
page => $c->param('page') || 1,
rows => 10,
order_by => { -desc => 'date' },
},
);
$c->stash( import_rs => $import_rs );
}
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, $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(
import_set => $import_set,
import_value_rs => $import_value_rs,
import_users_rs => $import_users_rs,
import_org_rs => $import_org_rs,
import_lookup_rs => $import_lookup_rs,
);
}
sub get_add {
my $c = shift;
}
sub post_add {
my $c = shift;
my $csv_data = $c->param('csv');
my $date_format = $c->param('date_format');
my $csv = Text::CSV->new({
binary => 1,
allow_whitespace => 1,
});
open my $fh, '<', \$csv_data;
# List context returns the actual headers
my @csv_headers;
my $error;
try {
@csv_headers = $csv->header( $fh );
} catch {
$error = $_;
};
if ( defined $error ) {
$c->_csv_flash_error( $error );
$c->redirect_to( '/admin/import/add' );
return;
}
# Text::CSV Already errors on duplicate columns, so this is fine
my @required = grep {/^user$|^value$|^date$|^organisation$/} @csv_headers;
unless ( scalar( @required ) == 4 ) {
$c->_csv_flash_error( 'Required columns not available' );
$c->redirect_to( '/admin/import/add' );
return;
}
my $csv_output = $csv->getline_hr_all( $fh );
unless ( scalar( @$csv_output ) ) {
$c->_csv_flash_error( "No data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
for my $data ( @$csv_output ) {
for my $key ( qw/ user value organisation / ) {
unless ( defined $data->{$key} ) {
$c->_csv_flash_error( "Undefined [$key] data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
}
if ( defined $data->{date} ) {
my $dtp = DateTime::Format::Strptime->new( pattern => $date_format );
my $dt_obj = $dtp->parse_datetime($data->{date});
unless ( defined $dt_obj ) {
$c->_csv_flash_error( "Undefined or incorrect format for [date] data found" );
$c->redirect_to( '/admin/import/add' );
return;
}
$data->{date} = $dt_obj;
}
}
my $value_set;
$c->schema->txn_do(
sub {
$value_set = $c->result_set->create({});
$value_set->values->populate(
[
[ qw/ user_name purchase_value purchase_date org_name / ],
( map { [ @{$_}{qw/ user value date organisation /} ] } @$csv_output ),
]
);
}
);
unless ( defined $value_set ) {
$c->_csv_flash_error( 'Error creating new Value Set' );
$c->redirect_to( '/admin/import/add' );
return;
}
$c->flash( success => 'Created Value Set' );
$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');
my $user_name = $c->param('user');
my $values_rs = $c->result_set->find($set_id)->values->search(
{
user_name => $user_name,
ignore_value => 0,
}
);
unless ( $values_rs->count > 0 ) {
$c->flash( error => 'User 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 => $user_name },
);
my $entity_id = $c->param('entity');
my $users_rs = $c->schema->resultset('User');
if ( defined $entity_id && $users_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 => $user_name,
entity_id => $entity_id,
},
);
}
} elsif ( defined $entity_id ) {
$c->stash( error => "User does not exist" );
}
$c->stash(
users_rs => $users_rs,
lookup => $lookup_result,
user_name => $user_name,
);
}
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 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 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,15 +3,25 @@ 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;
my $valid_orgs_rs = $c->schema->resultset('Organisation')->search({ pending => 0 });
my $pending_orgs_rs = $c->schema->resultset('Organisation')->search({ pending => 1 });
my $orgs_rs = $c->schema->resultset('Organisation')->search(
undef,
{
page => $c->param('page') || 1,
rows => 10,
order_by => { -asc => 'name' },
},
);
$c->stash(
valid_orgs_rs => $valid_orgs_rs,
pending_orgs_rs => $pending_orgs_rs,
orgs_rs => $orgs_rs,
);
}
@ -30,6 +40,8 @@ sub add_org_submit {
$validation->optional('sector');
$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' );
@ -38,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 => {
@ -46,8 +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',
});
@ -66,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,
);
}
@ -84,11 +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' );
@ -97,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({
@ -105,17 +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

@ -0,0 +1,80 @@
package Pear::LocalLoop::Controller::Admin::Reports;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON qw/ encode_json /;
sub transaction_data {
my $c = shift;
my $quantised_column = 'quantised_hours';
if ( defined $c->param('scale') && $c->param('scale') eq 'days' ) {
$quantised_column = 'quantised_days';
}
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
my $transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
{},
{
columns => [
{
quantised => $quantised_column,
count => \"COUNT(*)",
sum_distance => $c->pg_or_sqlite(
'SUM("me"."distance")',
'SUM("me"."distance")',
),
average_distance => $c->pg_or_sqlite(
'AVG("me"."distance")',
'AVG("me"."distance")',
),
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_column,
order_by => { '-asc' => $quantised_column },
}
);
my $transaction_data = [
map{
my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised'));
{
sum_value => ($_->get_column('sum_value') || 0) * 1,
sum_distance => ($_->get_column('sum_distance') || 0) * 1,
average_value => ($_->get_column('average_value') || 0) * 1,
average_distance => ($_->get_column('average_distance') || 0) * 1,
count => $_->get_column('count'),
quantised => $c->format_iso_datetime($quantised),
}
} $transaction_rs->all
];
$c->respond_to(
json => { json => { data => $transaction_data } },
html => { transaction_rs => encode_json( $transaction_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

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

@ -4,8 +4,8 @@ use Mojo::Base 'Mojolicious::Controller';
has error_messages => sub {
return {
email => {
required => { message => 'Email is required', status => 400 },
in_resultset => { message => 'Change meeee', status => 400 },
required => { message => 'Email is required or not registered', status => 400 },
in_resultset => { message => 'Email is required or not registered', status => 400, error => "required" },
},
feedbacktext => {
required => { message => 'Feedback is required', status => 400 },

View file

@ -0,0 +1,239 @@
package Pear::LocalLoop::Controller::Api::Organisation;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::JSON;
has error_messages => sub {
return {
entry_period => {
required => { message => 'No entry period sent.', status => 400 },
},
employee_amount => {
required => { message => 'No employee amount sent.', status => 400 },
},
local_employee_amount => {
required => { message => 'No local employee amount sent.', status => 400 },
},
gross_payroll => {
required => { message => 'No gross payroll sent.', status => 400 },
},
payroll_income_tax => {
required => { message => 'No total income tax sent.', status => 400 },
},
payroll_employee_ni => {
required => { message => 'No total employee NI sent.', status => 400 },
},
payroll_employer_ni => {
required => { message => 'No total employer NI sent.', status => 400 },
},
payroll_total_pension => {
required => { message => 'No total total pension sent.', status => 400 },
},
payroll_other_benefit => {
required => { message => 'No total other benefits total sent.', status => 400 },
},
supplier_business_name => {
required => { message => 'No supplier business name sent.', status => 400 },
},
postcode => {
required => { message => 'No postcode sent.', status => 400 },
postcode => { message => 'postcode must be valid', status => 400 },
},
monthly_spend => {
required => { message => 'No monthly spend sent.', status => 400 },
},
employee_no => {
required => { message => 'No employee no sent.', status => 400 },
},
employee_income_tax => {
required => { message => 'No employee income tax sent.', status => 400 },
},
employee_gross_wage => {
required => { message => 'No employee gross wage sent.', status => 400 },
},
employee_ni => {
required => { message => 'No employee ni sent.', status => 400 },
},
employee_pension => {
required => { message => 'No employee pension sent.', status => 400 },
},
employee_other_benefit => {
required => { message => 'No employee other benefits sent.', status => 400 },
},
};
};
sub post_payroll_read {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
$validation->optional('page')->number;
return $c->api_validation_error if $validation->has_error;
my $payrolls = $user->entity->organisation->payroll->search(
undef, {
page => $validation->param('page') || 1,
rows => 10,
order_by => { -desc => 'submitted_at' },
},
);
# purchase_time needs timezone attached to it
my @payroll_list = (
map {{
entry_period => $_->entry_period,
employee_amount => $_->employee_amount,
local_employee_amount => $_->local_employee_amount,
gross_payroll => $_->gross_payroll / 100000,
payroll_income_tax => $_->payroll_income_tax / 100000,
payroll_employee_ni => $_->payroll_employee_ni / 100000,
payroll_employer_ni => $_->payroll_employer_ni / 100000,
payroll_total_pension => $_->payroll_total_pension / 100000,
payroll_other_benefit => $_->payroll_other_benefit / 100000,
}} $payrolls->all
);
return $c->render( json => {
success => Mojo::JSON->true,
payrolls => \@payroll_list,
page_no => $payrolls->pager->total_entries,
});
}
sub post_payroll_add {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
return $c->api_validation_error if $validation->has_error;
my $user_rs = $c->schema->resultset('User')->search({
id => { "!=" => $user->id },
});
$validation->required('entry_period');
$validation->required('employee_amount');
$validation->required('local_employee_amount');
$validation->required('gross_payroll');
$validation->required('payroll_income_tax');
$validation->required('payroll_employee_ni');
$validation->required('payroll_employer_ni');
$validation->required('payroll_total_pension');
$validation->required('payroll_other_benefit');
return $c->api_validation_error if $validation->has_error;
my $entry_period = $c->parse_iso_month($validation->param('entry_period'));
my $employee_amount = $validation->param('employee_amount');
my $local_employee_amount = $validation->param('local_employee_amount');
my $gross_payroll = $validation->param('gross_payroll');
my $payroll_income_tax = $validation->param('payroll_income_tax');
my $payroll_employee_ni = $validation->param('payroll_employee_ni');
my $payroll_employer_ni = $validation->param('payroll_employer_ni');
my $payroll_total_pension = $validation->param('payroll_total_pension');
my $payroll_other_benefit = $validation->param('payroll_other_benefit');
$c->schema->txn_do( sub {
$user->entity->organisation->payroll->create({
entry_period => $entry_period,
employee_amount => $employee_amount,
local_employee_amount => $local_employee_amount,
gross_payroll => $gross_payroll * 100000,
payroll_income_tax => $payroll_income_tax * 100000,
payroll_employee_ni => $payroll_employee_ni * 100000,
payroll_employer_ni => $payroll_employer_ni * 100000,
payroll_total_pension => $payroll_total_pension * 100000,
payroll_other_benefit => $payroll_other_benefit * 100000,
});
});
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Submitted Payroll Info Successfully',
});
}
sub post_supplier_read {
}
sub post_supplier_add {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
return $c->api_validation_error if $validation->has_error;
my $user_rs = $c->schema->resultset('User')->search({
id => { "!=" => $user->id },
});
$validation->required('entry_period');
$validation->required('postcode')->postcode;
$validation->required('supplier_business_name');
$validation->required('monthly_spend');
return $c->api_validation_error if $validation->has_error;
$c->schema->txn_do( sub {
$user->entity->organisation->update({
entry_period => $validation->param('entry_period'),
});
});
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Submitted Supplier Info Successfully',
});
}
sub post_employee_read {
}
sub post_employee_add {
my $c = shift;
my $user = $c->stash->{api_user};
my $validation = $c->validation;
$validation->input( $c->stash->{api_json} );
return $c->api_validation_error if $validation->has_error;
my $user_rs = $c->schema->resultset('User')->search({
id => { "!=" => $user->id },
});
$validation->required('entry_period');
$validation->required('employee_no');
$validation->required('employee_income_tax');
$validation->required('employee_gross_wage');
$validation->required('employee_ni');
$validation->required('employee_pension');
$validation->required('employee_other_benefit');
return $c->api_validation_error if $validation->has_error;
$c->schema->txn_do( sub {
$user->entity->organisation->update({
entry_period => $validation->param('entry_period'),
});
});
return $c->render( json => {
success => Mojo::JSON->true,
message => 'Submitted Employee Info Successfully',
});
}
1;

View file

@ -2,6 +2,8 @@ package Pear::LocalLoop::Controller::Api::Register;
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use Geo::UK::Postcode::Regex;
has error_messages => sub {
return {
token => {
@ -9,18 +11,18 @@ has error_messages => sub {
in_resultset => { message => 'Token invalid or has been used.', status => 401 },
},
name => {
required => { message => 'No name sent or was blank.', status => 400 },
required => { message => 'No organisation name sent or was blank.', status => 400 },
},
display_name => {
required => { message => 'No name sent or was blank.', status => 400 },
required => { message => 'No display name sent or was blank.', status => 400 },
},
full_name => {
required => { message => 'No name sent or was blank.', status => 400 },
required => { message => 'No full name sent or was blank.', status => 400 },
},
email => {
required => { message => 'No email sent.', status => 400 },
email => { message => 'Email is invalid.', status => 400 },
not_in_resultset => { message => 'Email exists.', status => 403 },
not_in_resultset => { message => 'Email already in use.', status => 403 },
},
postcode => {
required => { message => 'No postcode sent.', status => 400 },
@ -34,16 +36,16 @@ has error_messages => sub {
in => { message => '"usertype" is invalid.', status => 400 },
},
year_of_birth => {
required => { message => 'No year_of_birth sent.', status => 400 },
number => { message => 'year_of_birth is invalid', status => 400 },
gt_num => { message => 'year_of_birth must be within last 150 years', status => 400 },
lt_num => { message => 'year_of_birth must be atleast 10 years ago', status => 400 },
required => { message => 'No year of birth sent.', status => 400 },
number => { message => 'year of birth is invalid', status => 400 },
gt_num => { message => 'year of birth must be within last 150 years', status => 400 },
lt_num => { message => 'year of birth must be atleast 10 years ago', status => 400 },
},
street_name => {
required => { message => 'No street_name sent.', status => 400 },
required => { message => 'No street name sent.', status => 400 },
},
town => {
required => { message => 'No town sent.', status => 400 },
required => { message => 'No town/city sent.', status => 400 },
},
};
};
@ -80,6 +82,11 @@ sub post_register {
return $c->api_validation_error if $validation->has_error;
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
$usertype,
);
if ($usertype eq 'customer'){
$c->schema->txn_do( sub {
@ -94,6 +101,7 @@ sub post_register {
display_name => $validation->param('display_name'),
year_of_birth => $validation->param('year_of_birth'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : () ),
},
user => {
email => $validation->param('email'),
@ -118,6 +126,7 @@ sub post_register {
town => $validation->param('town'),
sector => $validation->param('sector'),
postcode => $validation->param('postcode'),
( defined $location ? ( %$location ) : () ),
},
user => {
email => $validation->param('email'),

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 => $_->purchase_time,
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

@ -50,17 +50,17 @@ The postcode of an organisation, optional key. Used when transaction_Type is 3.
has error_messages => sub {
return {
transaction_type => {
required => { message => 'transaction_type is missing.', status => 400 },
in => { message => 'transaction_type is not a valid value.', status => 400 },
required => { message => 'transaction type is missing.', status => 400 },
in => { message => 'transaction type is not a valid value.', status => 400 },
},
transaction_value => {
required => { message => 'transaction_value is missing', status => 400 },
number => { message => 'transaction_value does not look like a number', status => 400 },
gt_num => { message => 'transaction_value cannot be equal to or less than zero', status => 400 },
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 },
},
purchase_time => {
required => { message => 'purchase_time is missing', status => 400 },
is_full_iso_datetime => { message => 'purchase_time is in incorrect format', status => 400 },
required => { message => 'purchase time is missing', status => 400 },
is_full_iso_datetime => { message => 'purchase time is in incorrect format', status => 400 },
},
file => {
required => { message => 'No file uploaded', status => 400 },
@ -68,18 +68,23 @@ has error_messages => sub {
filetype => { message => 'File must be of type image/jpeg', status => 400 },
},
organisation_id => {
required => { message => 'organisation_id is missing', status => 400 },
number => { message => 'organisation_id is not a number', status => 400 },
in_resultset => { message => 'organisation_id does not exist in the database', status => 400 },
required => { message => 'existing organisation ID is missing', status => 400 },
number => { message => 'organisation ID is not a number', status => 400 },
in_resultset => { message => 'organisation ID does not exist in the database', status => 400 },
},
organisation_name => {
required => { message => 'organisation_name is missing', status => 400 },
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 },
},
search_name => {
required => { message => 'search_name is missing', status => 400 },
required => { message => 'search name is missing', status => 400 },
},
postcode => {
required => { message => 'postcode is missing', status => 400 },
postcode => { message => 'postcode must be valid', status => 400 },
},
};
@ -102,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;
@ -115,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;
@ -144,12 +152,18 @@ sub post_upload {
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;
@ -171,6 +185,10 @@ 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(
'sales',
@ -179,6 +197,8 @@ 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,
}
);
@ -193,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 {
@ -212,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;
@ -223,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

@ -28,10 +28,10 @@ has error_messages => sub {
required => { message => 'No password sent.', status => 400 },
},
street_name => {
required => { message => 'No street_name sent.', status => 400 },
required => { message => 'No street name sent.', status => 400 },
},
town => {
required => { message => 'No town sent.', status => 400 },
required => { message => 'No town/city sent.', status => 400 },
},
sector => {
required => { message => 'No sector sent.', status => 400 },
@ -49,22 +49,28 @@ sub post_account {
my $email = $user_result->email;
if ( $user_result->type eq 'customer' ) {
my $full_name = $user_result->entity->customer->full_name;
my $display_name = $user_result->entity->customer->display_name;
my $postcode = $user_result->entity->customer->postcode;
my $customer = $user_result->entity->customer;
my $full_name = $customer->full_name;
my $display_name = $customer->display_name;
my $postcode = $customer->postcode;
return $c->render( json => {
success => Mojo::JSON->true,
full_name => $full_name,
display_name => $display_name,
email => $email,
postcode => $postcode,
location => {
latitude => (defined $customer->latitude ? $customer->latitude * 1 : undef),
longitude => (defined $customer->longitude ? $customer->longitude * 1 : undef),
},
});
} elsif ( $user_result->type eq 'organisation' ) {
my $name = $user_result->entity->organisation->name;
my $postcode = $user_result->entity->organisation->postcode;
my $street_name = $user_result->entity->organisation->street_name;
my $town = $user_result->entity->organisation->town;
my $sector = $user_result->entity->organisation->sector;
my $organisation = $user_result->entity->organisation;
my $name = $organisation->name;
my $postcode = $organisation->postcode;
my $street_name = $organisation->street_name;
my $town = $organisation->town;
my $sector = $organisation->sector;
return $c->render( json => {
success => Mojo::JSON->true,
town => $town,
@ -73,6 +79,10 @@ sub post_account {
street_name => $street_name,
email => $email,
postcode => $postcode,
location => {
latitude => (defined $organisation->latitude ? $organisation->latitude * 1 : undef),
longitude => (defined $organisation->longitude ? $organisation->longitude * 1 : undef),
},
});
} else {
return $c->render(
@ -135,6 +145,11 @@ sub post_account_update {
return $c->api_validation_error if $validation->has_error;
my $location = $c->get_location_from_postcode(
$validation->param('postcode'),
$user->type,
);
if ( $user->type eq 'customer' ){
$c->schema->txn_do( sub {
@ -142,6 +157,7 @@ sub post_account_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'),
@ -159,6 +175,7 @@ sub post_account_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

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

@ -0,0 +1,193 @@
package Pear::LocalLoop::Controller::Api::V1::Supplier::Location;
use Mojo::Base 'Mojolicious::Controller';
has validation_data => sub {
my $children_errors = {
latitude => {
validation => [
{ required => {} },
{ number => { error_prefix => 'not_number' } },
{ in_range => { args => [ -90, 90 ], error_prefix => 'outside_range' } },
],
},
longitude => {
validation => [
{ required => {} },
{ number => { error_prefix => 'not_number' } },
{ in_range => { args => [ -180, 180 ], error_prefix => 'outside_range' } },
],
},
};
return {
index => {
north_east => {
validation => [
{ required => {} },
{ is_object => { error_prefix => 'not_object' } },
],
children => $children_errors,
},
south_west => {
validation => [
{ required => {} },
{ is_object => { error_prefix => 'not_object' } },
],
children => $children_errors,
},
}
}
};
sub index {
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;
# need: organisations only, with name, latitude, and longitude
my $org_rs = $entity->purchases->search_related('seller',
{
'seller.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 $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 ];
$c->render(
json => {
success => Mojo::JSON->true,
suppliers => $suppliers,
self => {
latitude => $entity_type_object->latitude,
longitude => $entity_type_object->longitude,
}
},
);
}
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

@ -6,6 +6,17 @@ use DateTime::Format::Strptime;
sub register {
my ( $plugin, $app, $conf ) = @_;
$app->helper( human_datetime_parser => sub {
return DateTime::Format::Strptime->new( pattern => '%x %X' );
});
$app->helper( format_human_datetime => sub {
my ( $c, $datetime_obj ) = @_;
return $c->human_datetime_parser->format_datetime(
$datetime_obj,
);
});
$app->helper( iso_datetime_parser => sub {
return DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S.%3N%z' );
});
@ -14,6 +25,10 @@ sub register {
return DateTime::Format::Strptime->new( pattern => '%Y-%m-%d' );
});
$app->helper( iso_month_parser => sub {
return DateTime::Format::Strptime->new( pattern => '%Y-%m' );
});
$app->helper( parse_iso_date => sub {
my ( $c, $date_string ) = @_;
return $c->iso_date_parser->parse_datetime(
@ -28,6 +43,20 @@ sub register {
);
});
$app->helper( parse_iso_month => sub {
my ( $c, $date_string ) = @_;
return $c->iso_month_parser->parse_datetime(
$date_string,
);
});
$app->helper( format_iso_month => sub {
my ( $c, $datetime_obj ) = @_;
return $c->iso_month_parser->format_datetime(
$datetime_obj,
);
});
$app->helper( parse_iso_datetime => sub {
my ( $c, $date_string ) = @_;
return $c->iso_datetime_parser->parse_datetime(
@ -37,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

@ -0,0 +1,58 @@
package Pear::LocalLoop::Plugin::Postcodes;
use Mojo::Base 'Mojolicious::Plugin';
use Geo::UK::Postcode::Regex;
use GIS::Distance;
sub register {
my ( $plugin, $app, $conf ) = @_;
$app->helper( get_location_from_postcode => sub {
my ( $c, $postcode, $usertype ) = @_;
my $postcode_obj = Geo::UK::Postcode::Regex->parse( $postcode );
my $location;
unless ( defined $postcode_obj && $postcode_obj->{non_geographical} ) {
my $pc_result = $c->schema->resultset('GbPostcode')->find({
incode => $postcode_obj->{incode},
outcode => $postcode_obj->{outcode},
});
if ( defined $pc_result ) {
# Force truncation here as SQLite is stupid
$location = {
latitude => (
$usertype eq 'customer'
? int($pc_result->latitude * 100 ) / 100
: $pc_result->latitude
),
longitude => (
$usertype eq 'customer'
? int($pc_result->longitude * 100 ) / 100
: $pc_result->longitude
),
};
}
}
return $location;
});
$app->helper( get_distance_from_coords => sub {
my ( $c, $buyer, $seller ) = @_;
my $gis = GIS::Distance->new();
my $buyer_lat = $buyer->latitude;
my $buyer_long = $buyer->longitude;
my $seller_lat = $seller->latitude;
my $seller_long = $seller->longitude;
if ( $buyer_lat && $buyer_long
&& $seller_lat && $seller_long ) {
return int( $gis->distance( $buyer_lat, $buyer_long => $seller_lat, $seller_long )->meters );
}
return;
});
}
1;

View file

@ -0,0 +1,18 @@
package Pear::LocalLoop::Plugin::TemplateHelpers;
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ( $plugin, $app, $conf ) = @_;
$app->helper( truncate_text => sub {
my ( $c, $string, $length ) = @_;
if ( length $string < $length ) {
return $string;
} else {
return substr( $string, 0, $length - 3 ) . '...';
}
});
}
1;

View file

@ -64,6 +64,115 @@ sub register {
$value = $app->parse_iso_datetime( $value );
return defined $value ? undef : 1;
});
$app->validator->add_check( is_object => sub {
my ( $validation, $name, $value ) = @_;
return ref ( $value ) eq 'HASH' ? undef : 1;
});
$app->validator->add_check( in_range => sub {
my ( $validation, $name, $value, $low, $high ) = @_;
return $low < $value && $value < $high ? undef : 1;
});
$app->helper( validation_error => sub { _validation_error(@_) } );
}
=head2 validation_error
Returns undef if there is no validation error, returns true otherwise - having
set the errors up as required. Renders out the errors as an array, with status
400
=cut
sub _validation_error {
my ( $c, $sub_name ) = @_;
my $val_data = $c->validation_data->{ $sub_name };
return unless defined $val_data;
my $data = $c->stash->{api_json};
my @errors = _validate_set( $c, $val_data, $data );
if ( scalar @errors ) {
my @sorted_errors = sort @errors;
$c->render(
json => {
success => Mojo::JSON->false,
errors => \@sorted_errors,
},
status => 400,
);
return \@errors;
}
return;
}
sub _validate_set {
my ( $c, $val_data, $data, $parent_name ) = @_;
my @errors;
# MUST get a raw validation object
my $validation = $c->app->validator->validation;
$validation->input( $data );
for my $val_data_key ( keys %$val_data ) {
$validation->topic( $val_data_key );
my $val_set = $val_data->{$val_data_key};
my $custom_check_prefix = {};
for my $val_error ( @{$val_set->{validation}} ) {
my ( $val_validator ) = keys %$val_error;
unless (
$validation->validator->checks->{$val_validator}
|| $val_validator =~ /required|optional/
) {
$c->app->log->warn( 'Unknown Validator [' . $val_validator . ']' );
next;
}
if ( my $custom_prefix = $val_error->{ $val_validator }->{ error_prefix } ) {
$custom_check_prefix->{ $val_validator } = $custom_prefix;
}
my $val_args = $val_error->{ $val_validator }->{ args };
$validation->$val_validator(
( $val_validator =~ /required|optional/ ? $val_data_key : () ),
( defined $val_args ? @$val_args : () )
);
# stop bothering checking if failed, validation stops after first failure
last if $validation->has_error( $val_data_key );
}
if ( $validation->has_error( $val_data_key ) ) {
my ( $check ) = @{ $validation->error( $val_data_key ) };
my $error_prefix = defined $custom_check_prefix->{ $check }
? $custom_check_prefix->{ $check }
: $check;
my $error_string = join ('_',
$error_prefix,
( defined $parent_name ? $parent_name : () ),
$val_data_key,
);
push @errors, $error_string;
} elsif ( defined $val_set->{ children } ) {
push @errors, _validate_set(
$c,
$val_set->{ children },
$data->{ $val_data_key },
$val_data_key );
}
}
return @errors;
}
1;

View file

@ -6,7 +6,7 @@ use warnings;
use base 'DBIx::Class::Schema';
our $VERSION = 7;
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,18 @@ __PACKAGE__->add_columns(
size => 16,
is_nullable => 0,
},
latitude => {
data_type => 'decimal',
size => [5,2],
is_nullable => 1,
default_value => undef,
},
longitude => {
data_type => 'decimal',
size => [5,2],
is_nullable => 1,
default_value => undef,
},
);
__PACKAGE__->set_primary_key("id");

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;
@ -63,4 +101,16 @@ sub name {
}
}
sub type_object {
my $self = shift;
if ( $self->type eq 'customer' ) {
return $self->customer;
} elsif ( $self->type eq 'organisation' ) {
return $self->organisation;
} else {
return;
}
}
1;

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

@ -10,6 +10,7 @@ __PACKAGE__->table("feedback");
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
FilterColumn
/);
__PACKAGE__->add_columns(
@ -53,6 +54,11 @@ __PACKAGE__->add_columns(
size => 255,
is_nullable => 0,
},
"actioned" => {
data_type => "boolean",
default_value => \"false",
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
@ -64,4 +70,18 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->filter_column( actioned => {
filter_to_storage => 'to_bool',
});
sub to_bool {
my ( $self, $val ) = @_;
my $driver_name = $self->result_source->schema->storage->dbh->{Driver}->{Name};
if ( $driver_name eq 'SQLite' ) {
return $val ? 1 : 0;
} else {
return $val ? 'true' : 'false';
}
}
1;

View file

@ -0,0 +1,49 @@
package Pear::LocalLoop::Schema::Result::GbPostcode;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table('gb_postcodes');
__PACKAGE__->add_columns(
outcode => {
data_type => 'char',
size => 4,
is_nullable => 0,
},
incode => {
data_type => 'char',
size => 3,
is_nullable => 0,
default_value => '',
},
latitude => {
data_type => 'decimal',
size => [7,5],
is_nullable => 1,
default_value => undef,
},
longitude => {
data_type => 'decimal',
size => [7,5],
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

@ -0,0 +1,57 @@
package Pear::LocalLoop::Schema::Result::ImportLookup;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("import_lookups");
__PACKAGE__->add_columns(
id => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
set_id => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
name => {
data_type => "varchar",
size => 255,
},
entity_id => {
data_type => "integer",
is_foreign_key => 1,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"import_set",
"Pear::LocalLoop::Schema::Result::ImportSet",
{ "foreign.id" => "self.set_id" },
{
is_deferrable => 0,
join_type => "LEFT",
on_delete => "NO ACTION",
on_update => "NO ACTION",
},
);
__PACKAGE__->belongs_to(
"entity",
"Pear::LocalLoop::Schema::Result::Entity",
{ "foreign.id" => "self.entity_id" },
{
join_type => "LEFT",
on_delete => "NO ACTION",
on_update => "NO ACTION",
},
);
1;

View file

@ -0,0 +1,44 @@
package Pear::LocalLoop::Schema::Result::ImportSet;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components( qw/
InflateColumn::DateTime
TimeStamp
/);
__PACKAGE__->table("import_sets");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"date" => {
data_type => "datetime",
set_on_create => 1,
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
"values",
"Pear::LocalLoop::Schema::Result::ImportValue",
{ "foreign.set_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"lookups",
"Pear::LocalLoop::Schema::Result::ImportLookup",
{ "foreign.set_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
1;

View file

@ -0,0 +1,78 @@
package Pear::LocalLoop::Schema::Result::ImportValue;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components( qw/
InflateColumn::DateTime
/);
__PACKAGE__->table("import_values");
__PACKAGE__->add_columns(
id => {
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0,
},
set_id => {
data_type => 'integer',
is_foreign_key => 1,
is_nullable => 0,
},
user_name => {
data_type => 'varchar',
size => 255,
},
purchase_date => {
data_type => "datetime",
is_nullable => 0,
},
purchase_value => {
data_type => 'varchar',
size => 255,
},
org_name => {
data_type => 'varchar',
size => 255,
},
transaction_id => {
data_type => 'integer',
is_foreign_key => 1,
is_nullable => 1,
},
ignore_value => {
data_type => 'boolean',
default_value => \'false',
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"import_set",
"Pear::LocalLoop::Schema::Result::ImportSet",
{ "foreign.id" => "self.set_id" },
{
is_deferrable => 0,
join_type => "LEFT",
on_delete => "NO ACTION",
on_update => "NO ACTION",
},
);
__PACKAGE__->belongs_to(
"transaction",
"Pear::LocalLoop::Schema::Result::Transaction",
{ "foreign.id" => "self.transaction_id" },
{
join_type => "LEFT",
on_delete => "NO ACTION",
on_update => "NO ACTION",
},
);
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,54 +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_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,
default_value => undef,
},
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');
@ -68,29 +106,71 @@ __PACKAGE__->belongs_to(
"entity_id",
);
__PACKAGE__->filter_column( pending => {
filter_to_storage => 'to_bool',
});
__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",
{ "foreign.org_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->filter_column(
pending => {
filter_to_storage => 'to_bool',
},
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 ) = @_;
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,83 @@
package Pear::LocalLoop::Schema::Result::OrganisationPayroll;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(qw/
InflateColumn::DateTime
TimeStamp
/);
__PACKAGE__->table("organisation_payroll");
__PACKAGE__->add_columns(
"id" => {
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
},
"org_id" => {
data_type => 'integer',
is_nullable => 0,
is_foreign_key => 1,
},
"submitted_at" => {
data_type => "datetime",
is_nullable => 0,
set_on_create => 1,
},
"entry_period" => {
data_type => "datetime",
is_nullable => 0,
},
"employee_amount" => {
data_type => "integer",
is_nullable => 0,
},
"local_employee_amount" => {
data_type => "integer",
is_nullable => 0,
},
"gross_payroll" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"payroll_income_tax" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"payroll_employee_ni" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"payroll_employer_ni" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"payroll_total_pension" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
"payroll_other_benefit" => {
data_type => "numeric",
size => [ 100, 0 ],
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
"organisation",
"Pear::LocalLoop::Schema::Result::Organisation",
"org_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,16 @@ __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],
is_nullable => 1,
},
);
__PACKAGE__->set_primary_key("id");
@ -66,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;

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