Compare commits
401 commits
v0.9.2
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
|
e495f744d9 | ||
|
48f8f20202 | ||
|
d9673f32e3 | ||
|
5149121e11 | ||
|
0d323c985f | ||
|
ff04f44232 | ||
|
fdbe86a464 | ||
|
acad46a9b5 | ||
|
9299c46fdf | ||
|
1efabeb45e | ||
|
103cf61ec6 | ||
|
653f495a70 | ||
|
eabc4e04fb | ||
|
f89572e3de | ||
|
d484b342df | ||
|
916e77a238 | ||
|
962cf972da | ||
|
3b8b5b97f4 | ||
|
af9069b17a | ||
|
c977cf3279 | ||
|
9b7d8530b6 | ||
|
bbb7edd269 | ||
|
0328bdc1f6 | ||
|
b1ab789455 | ||
|
51f0fb406e | ||
|
5accf45cde | ||
|
7dc7acb1a1 | ||
|
86d52e7bbc | ||
|
43808f5510 | ||
|
ed2b6970f4 | ||
|
9357405445 | ||
|
b02f8b7c5f | ||
|
4fdff21f50 | ||
|
546193f0cb | ||
|
314c3e4bd5 | ||
|
135fd890b2 | ||
|
06b08cff07 | ||
|
4d3aca2457 | ||
|
d661ab5996 | ||
|
00dbb77130 | ||
|
330a4a93ed | ||
|
39aa2d2083 | ||
|
7d862290d3 | ||
|
71189d18fc | ||
|
a45c354834 | ||
|
cc84dbb76e | ||
|
536d11f298 | ||
|
f6ed82a02f | ||
|
6ecc3d3c56 | ||
|
94aced117d | ||
|
ee4e5a868c | ||
|
275b9cef46 | ||
|
8f44e26a6d | ||
|
eeb7a852be | ||
|
5203df3d50 | ||
|
efbf8cbad7 | ||
|
aa2d429403 | ||
|
1444574d26 | ||
|
9c79e50eba | ||
|
1c942f0be7 | ||
|
d0506c2a95 | ||
|
22e6001362 | ||
|
bc74496738 | ||
|
afc635fdb4 | ||
|
d1cd30928e | ||
|
46b5496901 | ||
|
bf4b092a12 | ||
|
4b7550f569 | ||
|
c456428681 | ||
|
bea1301475 | ||
|
1d22da2bed | ||
|
8f3da8ae1b | ||
|
e974325d98 | ||
|
aabfc6505b | ||
|
adb19e84f7 | ||
|
8d4c9703a0 | ||
|
2b0abd2606 | ||
|
4323e49cfc | ||
|
969b529a66 | ||
|
644ccadbfc | ||
|
2296fb8d42 | ||
|
aabe98cc67 | ||
|
b8b06c06fe | ||
|
61f5d761da | ||
|
2fd0eed782 | ||
|
0f98f22404 | ||
|
25b6b3a9a4 | ||
|
ccdbff42e6 | ||
|
10ea8cfe92 | ||
|
e30717383c | ||
|
6741aba746 | ||
|
779a6e77ff | ||
|
174ce1f105 | ||
|
6d2cbecffa | ||
|
eeeaaaaddd | ||
|
eee925aa80 | ||
|
59180ed310 | ||
|
c3ac620a2d | ||
|
2ceecaa0a1 | ||
|
376db29a7c | ||
|
a4bcd9c6d7 | ||
|
78fdfc1e1d | ||
|
bc92d2eb11 | ||
|
9284218431 | ||
|
ec9fac293d | ||
|
b036d5494b | ||
|
4b4d50de07 | ||
|
b22b85e0f2 | ||
|
4b98de9075 | ||
|
4844174ead | ||
|
dc8f240243 | ||
|
e817dc29b1 | ||
|
8a2aa3a73a | ||
|
2286e54104 | ||
|
2b5bb9cd8c | ||
|
2d03d25916 | ||
|
b35e18f181 | ||
|
3514a9e0ed | ||
|
47bca8d39f | ||
|
055b95e2fc | ||
|
c5341af3e7 | ||
|
6527d1e36c | ||
|
a53479c6c8 | ||
|
1302f9e843 | ||
|
fb60ba4c0f | ||
|
047ec888fa | ||
|
4ff3f07f9a | ||
|
49e5e91860 | ||
|
73d44feace | ||
|
2cf0678126 | ||
|
bcb0cd642c | ||
|
97462df1a2 | ||
|
cc6ea41ce5 | ||
|
58ae5b5250 | ||
|
44f2a321b2 | ||
|
75ae16cd8d | ||
|
4fb85b9094 | ||
|
b157ba0843 | ||
|
b233acfd64 | ||
|
cea9e62073 | ||
|
3ceb926cd4 | ||
|
60438ea51c | ||
|
551a40a9a0 | ||
|
d5e03cc9e3 | ||
|
0cb3426825 | ||
|
29eaa291c5 | ||
|
fdea4be36f | ||
|
ca389a1788 | ||
|
371cf1ea42 | ||
|
af43a3ce9f | ||
|
2edc45a22e | ||
|
241fe26f26 | ||
|
0441128023 | ||
|
12bde771b2 | ||
|
ea18e467f4 | ||
|
a3dcd57fea | ||
|
d4d1b841d7 | ||
|
bdf23b2f3d | ||
|
813cbb82a5 | ||
|
98cd134d84 | ||
|
3cf73aca37 | ||
|
38dc29f4a1 | ||
|
d937e64663 | ||
|
79324ff5f7 | ||
|
786cd1618a | ||
|
7aa93bf380 | ||
|
ab88755b00 | ||
|
712959c37e | ||
|
23185985b8 | ||
|
1377742acd | ||
|
ebcc79fd36 | ||
|
af6d198c8f | ||
|
396cb3c8f6 | ||
|
28a33cf27f | ||
|
f519bb9f2f | ||
|
413479b94f | ||
|
940d96921c | ||
|
480c8ed78f | ||
|
156af841c2 | ||
|
26191d413d | ||
|
6879a8b3b8 | ||
|
613272f413 | ||
|
da86e52735 | ||
|
a4706ef05e | ||
|
1b74931248 | ||
|
9f3971815e | ||
|
3ea79ad8de | ||
|
ecbcd205fe | ||
|
6cd7df1259 | ||
|
6e970de92c | ||
|
41928e1ff6 | ||
|
7a44797a56 | ||
|
65a4447478 | ||
|
e64e4bcbc8 | ||
|
cab32fbf92 | ||
|
8c8da1f795 | ||
|
39549d257f | ||
|
b247183914 | ||
|
de9d70432f | ||
|
cd4f13bf8c | ||
|
3e9746ca22 | ||
|
6fe4cd9a31 | ||
|
b383284519 | ||
|
36947bfeab | ||
|
a9ca2efe9a | ||
|
7f9c53aa5a | ||
|
8e00135390 | ||
|
dddbb04023 | ||
|
ad30cf9cd9 | ||
|
8a0f933f07 | ||
|
1f70f9b40a | ||
|
c4f68332b9 | ||
|
175026e37e | ||
|
53b63aa655 | ||
|
4369a95054 | ||
|
4e74c871a2 | ||
|
c2a099c3e2 | ||
|
7ea96a74d0 | ||
|
4ee40b0732 | ||
|
40dfa1c102 | ||
|
bef55a85d5 | ||
|
cc2360ab22 | ||
|
500b61928b | ||
|
0f9a3eadd8 | ||
|
e0d3035929 | ||
|
8be16867bb | ||
|
d4ff855251 | ||
|
d6d4121cb3 | ||
|
67394ce8fa | ||
|
0214ad4558 | ||
|
3346dbd0d4 | ||
|
2673f79f98 | ||
|
2bbc1d508e | ||
|
bb254cb278 | ||
|
12364c2a17 | ||
|
cdde2f5ca8 | ||
|
38f8883ab1 | ||
|
feeba76523 | ||
|
9851ef5b6f | ||
|
8e34f34bd6 | ||
|
cc0ca06470 | ||
|
4653f713fa | ||
|
f1c1748b34 | ||
|
3914ec44a8 | ||
|
2dceb4067e | ||
|
5ce2f0beea | ||
|
a89a320685 | ||
|
5dabc7a28a | ||
|
74a1b3d238 | ||
|
229c2bb801 | ||
|
88aa5becff | ||
|
c4b7fa5102 | ||
|
b9a57c7dcb | ||
|
60615e9b4a | ||
|
b2627eea4f | ||
|
af95b31eb8 | ||
|
f50c5e685c | ||
|
8067d3cb96 | ||
|
8e1e9b2ec2 | ||
|
59dd053b45 | ||
|
3100fec233 | ||
|
b3f1a018b5 | ||
|
20ca523f1e | ||
|
5b0f8bffab | ||
|
da320d250f | ||
|
db95f239cd | ||
|
e6c236e1ff | ||
|
fb25cbe773 | ||
|
0b6d823145 | ||
|
1e02062ca3 | ||
|
8b91c3b3c8 | ||
|
4eb5a4fd3f | ||
|
3868721324 | ||
|
a95bfccdf9 | ||
|
a14277ee48 | ||
|
738a72b64e | ||
|
6e016641cd | ||
|
c8e876711c | ||
|
843037ea78 | ||
|
6c738993f4 | ||
|
65cadb5d24 | ||
|
9a71c1b7f5 | ||
|
9adf9668b1 | ||
|
b6397aedf4 | ||
|
0799d6eeb1 | ||
|
1bc56016e7 | ||
|
6d138af016 | ||
|
460c9e6049 | ||
|
8ada3f86b6 | ||
|
0072a97a3a | ||
|
8c166464cc | ||
|
d36091528f | ||
|
cb9c219b27 | ||
|
cf83137f9f | ||
|
fc0d2f6fa0 | ||
|
13cd5ed950 | ||
|
9c500ab24b | ||
|
951a0b233f | ||
|
9f4d39e029 | ||
|
addd5c640c | ||
|
6d508cc432 | ||
|
b0883b5522 | ||
|
9aaf3b4718 | ||
|
bccb292441 | ||
|
14410b475a | ||
|
a0cdaac370 | ||
|
e02638ac58 | ||
|
7155011841 | ||
|
f49fb93658 | ||
|
1015be7810 | ||
|
7df6fecfc4 | ||
|
37bc29829f | ||
|
cb7ab797f5 | ||
|
ead0dff76d | ||
|
747a834156 | ||
|
233df00c7b | ||
|
72929bf86b | ||
|
9096bef00d | ||
|
593efcedfa | ||
|
594fc865eb | ||
|
105c9093b8 | ||
|
049b4836c5 | ||
|
c4681ffc3a | ||
|
40dbd1eb50 | ||
|
e09974f0d9 | ||
|
eebac70276 | ||
|
3e3f667053 | ||
|
751c6e64f2 | ||
|
ef36cbc541 | ||
|
d881b3f95f | ||
|
dcd5896cea | ||
|
ed8607fd35 | ||
|
11d6785062 | ||
|
de90fe1f77 | ||
|
0302a3d299 | ||
|
3d31e94367 | ||
|
6c798d8d3f | ||
|
afac877d12 | ||
|
62881a0eda | ||
|
37e8f0b46a | ||
|
202deb9178 | ||
|
f1e3756075 | ||
|
5dafed5054 | ||
|
8f699c25c7 | ||
|
1a54bae4e9 | ||
|
ede5fe1cee | ||
|
9e127b8851 | ||
|
9631c78f57 | ||
|
13b594e64f | ||
|
e667947c65 | ||
|
a739b02eed | ||
|
bf74fcc44f | ||
|
cf2c7dcc4b | ||
|
2486c701a5 | ||
|
9cc1358283 | ||
|
c3519fa4df | ||
|
4beb26a0a4 | ||
|
90d1eeeb21 | ||
|
0198d5364a | ||
|
2dcb591c19 | ||
|
005c4694ff | ||
|
85a51b8955 | ||
|
0dcb082786 | ||
|
5e078f8f8b | ||
|
df90f62b7a | ||
|
65d509943c | ||
|
5334d88a81 | ||
|
0486d910ee | ||
|
bfcacbee32 | ||
|
1a0dbf7098 | ||
|
62cad2d70e | ||
|
e2e4e4769d | ||
|
a810795ca5 | ||
|
ad51891f51 | ||
|
db1bbab10f | ||
|
3e87326265 | ||
|
f056199ec1 | ||
|
5a80d57ca2 | ||
|
119774e434 | ||
|
5b21fb669b | ||
|
d126233c8e | ||
|
22bdb625f0 | ||
|
09b8efe655 | ||
|
5f241bbd46 | ||
|
8e2cecfcf5 | ||
|
813f4af63f | ||
|
78964d7297 | ||
|
e388d360ac | ||
|
616181def3 | ||
|
b224182e61 | ||
|
7366c5035b | ||
|
9cb4f7c782 | ||
|
3b9383fc3d | ||
|
5b3ef1bf27 | ||
|
9e56383b46 | ||
|
dd36cd0c0c | ||
|
414acd76fb | ||
|
f59cf88b09 | ||
|
26a37904d2 | ||
|
9cfd5536b9 | ||
|
662219fc6e |
405 changed files with 79110 additions and 570 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -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
7
.idea/.gitignore
generated
vendored
Normal 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
13
.idea/Foodloop-Server.iml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
20
.idea/dataSources.xml
generated
Normal 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
6
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
7
.idea/sqldialects.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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
|
||||
|
|
135
CHANGELOG.md
135
CHANGELOG.md
|
@ -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
143
README.md
|
@ -5,3 +5,146 @@
|
|||
*Master:* [](https://travis-ci.org/Pear-Trading/Foodloop-Server)
|
||||
|
||||
*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
|
||||
```
|
||||
|
|
32
cpanfile
32
cpanfile
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
10
etc/code-point-open/LICENCE
Normal file
10
etc/code-point-open/LICENCE
Normal 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
|
BIN
etc/code-point-open/codepo_gb.zip
Normal file
BIN
etc/code-point-open/codepo_gb.zip
Normal file
Binary file not shown.
|
@ -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 {
|
||||
|
|
69
lib/Pear/LocalLoop/Command/codepoint_open.pm
Normal file
69
lib/Pear/LocalLoop/Command/codepoint_open.pm
Normal 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;
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
83
lib/Pear/LocalLoop/Command/latlong_setup.pm
Normal file
83
lib/Pear/LocalLoop/Command/latlong_setup.pm
Normal 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;
|
140
lib/Pear/LocalLoop/Command/recur_transactions.pm
Normal file
140
lib/Pear/LocalLoop/Command/recur_transactions.pm
Normal 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;
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
100
lib/Pear/LocalLoop/Controller/Admin/Categories.pm
Normal file
100
lib/Pear/LocalLoop/Controller/Admin/Categories.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
326
lib/Pear/LocalLoop/Controller/Admin/Import.pm
Normal file
326
lib/Pear/LocalLoop/Controller/Admin/Import.pm
Normal 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;
|
127
lib/Pear/LocalLoop/Controller/Admin/ImportFrom.pm
Normal file
127
lib/Pear/LocalLoop/Controller/Admin/ImportFrom.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
80
lib/Pear/LocalLoop/Controller/Admin/Reports.pm
Normal file
80
lib/Pear/LocalLoop/Controller/Admin/Reports.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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(
|
||||
|
|
86
lib/Pear/LocalLoop/Controller/Api/Categories.pm
Normal file
86
lib/Pear/LocalLoop/Controller/Api/Categories.pm
Normal 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;
|
489
lib/Pear/LocalLoop/Controller/Api/External.pm
Normal file
489
lib/Pear/LocalLoop/Controller/Api/External.pm
Normal 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;
|
|
@ -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 },
|
||||
|
|
239
lib/Pear/LocalLoop/Controller/Api/Organisation.pm
Normal file
239
lib/Pear/LocalLoop/Controller/Api/Organisation.pm
Normal 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;
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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'),
|
||||
|
|
21
lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm
Normal file
21
lib/Pear/LocalLoop/Controller/Api/V1/Customer.pm
Normal 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;
|
167
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm
Normal file
167
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Graphs.pm
Normal 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;
|
63
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm
Normal file
63
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Pies.pm
Normal 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;
|
32
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm
Normal file
32
lib/Pear/LocalLoop/Controller/Api/V1/Customer/Snippets.pm
Normal 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;
|
|
@ -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 );
|
||||
}
|
||||
|
|
63
lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Pies.pm
Normal file
63
lib/Pear/LocalLoop/Controller/Api/V1/Organisation/Pies.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
193
lib/Pear/LocalLoop/Controller/Api/V1/Supplier/Location.pm
Normal file
193
lib/Pear/LocalLoop/Controller/Api/V1/Supplier/Location.pm
Normal 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;
|
48
lib/Pear/LocalLoop/Controller/Api/V1/User/Medals.pm
Normal file
48
lib/Pear/LocalLoop/Controller/Api/V1/User/Medals.pm
Normal 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;
|
39
lib/Pear/LocalLoop/Controller/Api/V1/User/Points.pm
Normal file
39
lib/Pear/LocalLoop/Controller/Api/V1/User/Points.pm
Normal 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;
|
10
lib/Pear/LocalLoop/Error.pm
Normal file
10
lib/Pear/LocalLoop/Error.pm
Normal 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;
|
23
lib/Pear/LocalLoop/Import/LCCCsv.pm
Normal file
23
lib/Pear/LocalLoop/Import/LCCCsv.pm
Normal 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;
|
43
lib/Pear/LocalLoop/Import/LCCCsv/Postcodes.pm
Normal file
43
lib/Pear/LocalLoop/Import/LCCCsv/Postcodes.pm
Normal 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;
|
48
lib/Pear/LocalLoop/Import/LCCCsv/Suppliers.pm
Normal file
48
lib/Pear/LocalLoop/Import/LCCCsv/Suppliers.pm
Normal 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;
|
128
lib/Pear/LocalLoop/Import/LCCCsv/Transactions.pm
Normal file
128
lib/Pear/LocalLoop/Import/LCCCsv/Transactions.pm
Normal 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;
|
88
lib/Pear/LocalLoop/Import/Role/CSV.pm
Normal file
88
lib/Pear/LocalLoop/Import/Role/CSV.pm
Normal 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;
|
19
lib/Pear/LocalLoop/Import/Role/ExternalName.pm
Normal file
19
lib/Pear/LocalLoop/Import/Role/ExternalName.pm
Normal 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;
|
11
lib/Pear/LocalLoop/Import/Role/Schema.pm
Normal file
11
lib/Pear/LocalLoop/Import/Role/Schema.pm
Normal 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;
|
24
lib/Pear/LocalLoop/Plugin/Currency.pm
Normal file
24
lib/Pear/LocalLoop/Plugin/Currency.pm
Normal 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;
|
|
@ -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,
|
||||
);
|
||||
|
|
40
lib/Pear/LocalLoop/Plugin/Minion.pm
Normal file
40
lib/Pear/LocalLoop/Plugin/Minion.pm
Normal 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;
|
12
lib/Pear/LocalLoop/Plugin/Minion/Job.pm
Normal file
12
lib/Pear/LocalLoop/Plugin/Minion/Job.pm
Normal 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;
|
15
lib/Pear/LocalLoop/Plugin/Minion/Job/csv_postcode_import.pm
Normal file
15
lib/Pear/LocalLoop/Plugin/Minion/Job/csv_postcode_import.pm
Normal 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;
|
15
lib/Pear/LocalLoop/Plugin/Minion/Job/csv_supplier_import.pm
Normal file
15
lib/Pear/LocalLoop/Plugin/Minion/Job/csv_supplier_import.pm
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
12
lib/Pear/LocalLoop/Plugin/Minion/Job/leaderboards_recalc.pm
Normal file
12
lib/Pear/LocalLoop/Plugin/Minion/Job/leaderboards_recalc.pm
Normal 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;
|
13
lib/Pear/LocalLoop/Plugin/Minion/Job/test.pm
Normal file
13
lib/Pear/LocalLoop/Plugin/Minion/Job/test.pm
Normal 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;
|
58
lib/Pear/LocalLoop/Plugin/Postcodes.pm
Normal file
58
lib/Pear/LocalLoop/Plugin/Postcodes.pm
Normal 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;
|
18
lib/Pear/LocalLoop/Plugin/TemplateHelpers.pm
Normal file
18
lib/Pear/LocalLoop/Plugin/TemplateHelpers.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,7 @@ use warnings;
|
|||
|
||||
use base 'DBIx::Class::Schema';
|
||||
|
||||
our $VERSION = 7;
|
||||
our $VERSION = 30;
|
||||
|
||||
__PACKAGE__->load_namespaces;
|
||||
|
||||
|
|
46
lib/Pear/LocalLoop/Schema/Result/Category.pm
Normal file
46
lib/Pear/LocalLoop/Schema/Result/Category.pm
Normal 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;
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
39
lib/Pear/LocalLoop/Schema/Result/EntityAssociation.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/EntityAssociation.pm
Normal 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",
|
||||
);
|
44
lib/Pear/LocalLoop/Schema/Result/EntityPostcode.pm
Normal file
44
lib/Pear/LocalLoop/Schema/Result/EntityPostcode.pm
Normal 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;
|
39
lib/Pear/LocalLoop/Schema/Result/ExternalReference.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/ExternalReference.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
49
lib/Pear/LocalLoop/Schema/Result/GbPostcode.pm
Normal file
49
lib/Pear/LocalLoop/Schema/Result/GbPostcode.pm
Normal 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;
|
32
lib/Pear/LocalLoop/Schema/Result/GbWard.pm
Normal file
32
lib/Pear/LocalLoop/Schema/Result/GbWard.pm
Normal 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;
|
34
lib/Pear/LocalLoop/Schema/Result/GlobalMedalGroup.pm
Normal file
34
lib/Pear/LocalLoop/Schema/Result/GlobalMedalGroup.pm
Normal 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;
|
39
lib/Pear/LocalLoop/Schema/Result/GlobalMedals.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/GlobalMedals.pm
Normal 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;
|
45
lib/Pear/LocalLoop/Schema/Result/GlobalUserMedalProgress.pm
Normal file
45
lib/Pear/LocalLoop/Schema/Result/GlobalUserMedalProgress.pm
Normal 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;
|
59
lib/Pear/LocalLoop/Schema/Result/GlobalUserMedals.pm
Normal file
59
lib/Pear/LocalLoop/Schema/Result/GlobalUserMedals.pm
Normal 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;
|
57
lib/Pear/LocalLoop/Schema/Result/ImportLookup.pm
Normal file
57
lib/Pear/LocalLoop/Schema/Result/ImportLookup.pm
Normal 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;
|
44
lib/Pear/LocalLoop/Schema/Result/ImportSet.pm
Normal file
44
lib/Pear/LocalLoop/Schema/Result/ImportSet.pm
Normal 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;
|
78
lib/Pear/LocalLoop/Schema/Result/ImportValue.pm
Normal file
78
lib/Pear/LocalLoop/Schema/Result/ImportValue.pm
Normal 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;
|
34
lib/Pear/LocalLoop/Schema/Result/OrgMedalGroup.pm
Normal file
34
lib/Pear/LocalLoop/Schema/Result/OrgMedalGroup.pm
Normal 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;
|
39
lib/Pear/LocalLoop/Schema/Result/OrgMedals.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/OrgMedals.pm
Normal 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;
|
45
lib/Pear/LocalLoop/Schema/Result/OrgUserMedalProgress.pm
Normal file
45
lib/Pear/LocalLoop/Schema/Result/OrgUserMedalProgress.pm
Normal 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;
|
59
lib/Pear/LocalLoop/Schema/Result/OrgUserMedals.pm
Normal file
59
lib/Pear/LocalLoop/Schema/Result/OrgUserMedals.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
49
lib/Pear/LocalLoop/Schema/Result/OrganisationExternal.pm
Normal file
49
lib/Pear/LocalLoop/Schema/Result/OrganisationExternal.pm
Normal 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;
|
83
lib/Pear/LocalLoop/Schema/Result/OrganisationPayroll.pm
Normal file
83
lib/Pear/LocalLoop/Schema/Result/OrganisationPayroll.pm
Normal 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;
|
39
lib/Pear/LocalLoop/Schema/Result/OrganisationSocialType.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/OrganisationSocialType.pm
Normal 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;
|
38
lib/Pear/LocalLoop/Schema/Result/OrganisationType.pm
Normal file
38
lib/Pear/LocalLoop/Schema/Result/OrganisationType.pm
Normal 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;
|
|
@ -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;
|
||||
|
|
39
lib/Pear/LocalLoop/Schema/Result/TransactionCategory.pm
Normal file
39
lib/Pear/LocalLoop/Schema/Result/TransactionCategory.pm
Normal 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;
|
49
lib/Pear/LocalLoop/Schema/Result/TransactionExternal.pm
Normal file
49
lib/Pear/LocalLoop/Schema/Result/TransactionExternal.pm
Normal 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
Reference in a new issue