From 14e163a1a585ef13e2e04a75a75252308475c9ea Mon Sep 17 00:00:00 2001 From: Mike Sutton Date: Sat, 12 Nov 2022 02:27:46 +0100 Subject: [PATCH] init push - laying out the project --- .drone.yml | 103 + .gitignore | 44 + consumer/Gemfile | 15 + notifications/Gemfile | 15 + portal/.dockerignore | 2 + portal/.eslintrc.json | 26 + portal/.gitignore | 45 + portal/.rspec | 1 + portal/.rubocop.yml | 150 + portal/.rubocop_todo.yml | 0 portal/.ruby-gemset | 1 + portal/.ruby-version | 1 + portal/Gemfile | 80 + portal/Gemfile.lock | 446 + portal/README.md | 9 + portal/Rakefile | 6 + portal/app/assets/config/manifest.js | 2 + portal/app/assets/images/.keep | 0 portal/app/assets/images/favicon.png | Bin 0 -> 53150 bytes portal/app/assets/images/logo-white.png | Bin 0 -> 3418 bytes portal/app/assets/images/logo.png | Bin 0 -> 3040 bytes portal/app/assets/stylesheets/application.css | 15 + .../app/channels/application_cable/channel.rb | 4 + .../channels/application_cable/connection.rb | 4 + .../app/channels/exchange_account_channel.rb | 9 + .../api/authenticated_api_controller.rb | 3 + .../controllers/api/v1/account_controller.rb | 11 + .../app/controllers/api/v1/app_controller.rb | 7 + .../app/controllers/api/v1/bets_controller.rb | 104 + .../api/v1/exchange_accounts_controller.rb | 14 + .../controllers/api/v1/users_controller.rb | 13 + .../app/controllers/application_controller.rb | 2 + portal/app/controllers/pages_controller.rb | 4 + .../users/omniauth_callbacks_controller.rb | 15 + portal/app/helpers/application_helper.rb | 25 + portal/app/helpers/mailer_style_helper.rb | 11 + .../account_sync_and_reconciliation_job.rb | 14 + portal/app/jobs/application_job.rb | 8 + portal/app/jobs/bet_placement_service.rb | 63 + portal/app/jobs/clear_old_pulls_job.rb | 8 + portal/app/jobs/process_subscription_job.rb | 10 + portal/app/jobs/pull_event_markets_job.rb | 11 + .../app/jobs/pull_latest_odds_prices_job.rb | 13 + portal/app/jobs/pull_runner_odds_job.rb | 9 + portal/app/jobs/pull_tips_job.rb | 16 + portal/app/jobs/pull_upcoming_events_job.rb | 13 + portal/app/lib/general_helper.rb | 6 + portal/app/lib/integrations/betburger.rb | 940 ++ .../integrations/betfair/account_manager.rb | 16 + portal/app/lib/integrations/betfair/base.rb | 40 + .../lib/integrations/betfair/bet_manager.rb | 173 + .../lib/integrations/betfair/connection.rb | 39 + .../betfair/opportunity_hunter.rb | 77 + .../app/lib/services/bet_outcome_service.rb | 6 + portal/app/mailers/application_mailer.rb | 44 + portal/app/mailers/project_mailer.rb | 76 + portal/app/mailers/user_mailer.rb | 21 + portal/app/models/account.rb | 15 + portal/app/models/application_record.rb | 3 + portal/app/models/bet.rb | 82 + portal/app/models/betfair_event.rb | 5 + portal/app/models/betfair_event_runner.rb | 6 + portal/app/models/betfair_runner_odd.rb | 4 + portal/app/models/concerns/latest.rb | 6 + portal/app/models/exchange_account.rb | 218 + portal/app/models/loggable.rb | 20 + portal/app/models/source_subscription.rb | 17 + portal/app/models/subscription_run.rb | 17 + portal/app/models/tip_source.rb | 22 + portal/app/models/tip_source_data.rb | 15 + portal/app/models/tipster_account.rb | 18 + portal/app/models/user.rb | 82 + portal/app/views/layouts/application.html.erb | 19 + portal/app/views/pages/index.html.erb | 0 .../notify_hourly_unread_activities.html.erb | 11 + portal/babel.config.js | 83 + portal/bin/bundle | 118 + portal/bin/rails | 9 + portal/bin/rake | 9 + portal/bin/setup | 35 + portal/bin/spring | 17 + portal/bin/webpack | 18 + portal/bin/webpack-dev-server | 18 + portal/bin/yarn | 9 + portal/client/packs/application.js | 24 + portal/client/src/assets/images.js | 5 + .../assets/images/iconmonstr-arrow-65-240.png | Bin 0 -> 5087 bytes portal/client/src/assets/images/linkedin.png | Bin 0 -> 9501 bytes portal/client/src/assets/images/logo.png | Bin 0 -> 8180 bytes portal/client/src/assets/images/logo.svg | 4 + portal/client/src/assets/images/main-bg.jpg | Bin 0 -> 111605 bytes portal/client/src/assets/style.js | 9 + .../client/src/assets/stylesheets/style.scss | 1210 ++ portal/client/src/channels/consumer.js | 6 + portal/client/src/data/axiosClient.js | 15 + portal/client/src/data/concerns/filterable.js | 19 + .../client/src/data/concerns/notfications.js | 8 + .../client/src/data/entities/base_entity.js | 67 + portal/client/src/data/entities/bet.js | 37 + .../src/data/entities/exchange_account.js | 48 + portal/client/src/data/entities/pagination.js | 58 + portal/client/src/data/entities/user.js | 51 + portal/client/src/data/index.js | 15 + portal/client/src/data/provider.js | 4 + portal/client/src/data/store.js | 7 + portal/client/src/data/stores/app_store.js | 126 + portal/client/src/data/stores/base_store.js | 86 + portal/client/src/data/stores/bet_store.js | 70 + portal/client/src/data/stores/user_store.js | 36 + portal/client/src/entry_point/index.js | 22 + portal/client/src/helpers/filter_helpers.js | 66 + portal/client/src/helpers/shared_helpers.js | 75 + portal/client/src/helpers/sidebar_helpers.js | 61 + .../client/src/routes/AuthenticatedRoute.jsx | 41 + portal/client/src/routes/index.jsx | 36 + portal/client/src/routes/routes.js | 5 + portal/client/src/t.js | 44 + portal/client/src/utils/Notifier.js | 18 + portal/client/src/utils/needfulMethods.js | 2 + portal/client/src/views/bets/_bets_list.js | 84 + portal/client/src/views/bets/index.js | 39 + portal/client/src/views/dashboard/index.js | 33 + .../src/views/layouts/authenticate/_header.js | 96 + .../views/layouts/authenticate/_sidebar.js | 151 + .../src/views/layouts/authenticate/index.js | 44 + .../client/src/views/layouts/public_layout.js | 40 + .../views/modules/countdown_refresh/index.js | 54 + .../src/views/modules/editable_hash/index.js | 67 + .../views/modules/filtered_list/_filters.js | 55 + .../src/views/modules/filtered_list/index.js | 111 + .../src/views/modules/toggler.js/index.js | 58 + portal/client/src/views/public/index.js | 47 + .../src/views/shared/form_components.js | 182 + portal/client/src/views/shared/info_point.js | 29 + portal/config.ru | 5 + portal/config/application.rb | 47 + portal/config/appsignal.yml | 40 + portal/config/boot.rb | 4 + portal/config/cable.yml | 12 + portal/config/credentials.yml.enc | 1 + portal/config/database.yml | 19 + portal/config/environment.rb | 5 + portal/config/environments/development.rb | 73 + portal/config/environments/production.rb | 126 + portal/config/environments/test.rb | 58 + .../application_controller_renderer.rb | 8 + portal/config/initializers/assets.rb | 14 + .../initializers/backtrace_silencers.rb | 7 + .../initializers/content_security_policy.rb | 30 + .../config/initializers/cookies_serializer.rb | 5 + portal/config/initializers/devise.rb | 301 + .../initializers/filter_parameter_logging.rb | 4 + portal/config/initializers/generators.rb | 4 + portal/config/initializers/inflections.rb | 16 + portal/config/initializers/keycloak.rb | 16 + portal/config/initializers/mime_types.rb | 4 + portal/config/initializers/pagy.rb | 182 + portal/config/initializers/sidekiq.rb | 9 + portal/config/initializers/stripe.rb | 6 + portal/config/initializers/wrap_parameters.rb | 14 + portal/config/locales/devise.en.yml | 66 + portal/config/locales/doorkeeper.en.yml | 124 + portal/config/locales/en.yml | 192 + portal/config/puma.rb | 38 + portal/config/routes.rb | 43 + portal/config/schedule.yml | 24 + portal/config/sidekiq.yml | 17 + portal/config/spring.rb | 6 + portal/config/storage.yml | 16 + portal/config/webpack/development.js | 5 + portal/config/webpack/environment.js | 19 + portal/config/webpack/production.js | 5 + portal/config/webpack/test.js | 5 + portal/config/webpacker.yml | 97 + portal/docker/Dockerfile | 59 + portal/docker/docker-compose.yml | 141 + portal/package.json | 101 + portal/postcss.config.js | 12 + portal/raq | 16 + portal/yarn.lock | 10757 ++++++++++++++++ proxy/Gemfile | 17 + proxy/bettermail_proxy.rb | 58 + stack.yml | 210 + 183 files changed, 20069 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 consumer/Gemfile create mode 100644 notifications/Gemfile create mode 100644 portal/.dockerignore create mode 100644 portal/.eslintrc.json create mode 100644 portal/.gitignore create mode 100644 portal/.rspec create mode 100644 portal/.rubocop.yml create mode 100644 portal/.rubocop_todo.yml create mode 100644 portal/.ruby-gemset create mode 100644 portal/.ruby-version create mode 100644 portal/Gemfile create mode 100644 portal/Gemfile.lock create mode 100644 portal/README.md create mode 100644 portal/Rakefile create mode 100644 portal/app/assets/config/manifest.js create mode 100644 portal/app/assets/images/.keep create mode 100644 portal/app/assets/images/favicon.png create mode 100644 portal/app/assets/images/logo-white.png create mode 100644 portal/app/assets/images/logo.png create mode 100644 portal/app/assets/stylesheets/application.css create mode 100644 portal/app/channels/application_cable/channel.rb create mode 100644 portal/app/channels/application_cable/connection.rb create mode 100644 portal/app/channels/exchange_account_channel.rb create mode 100644 portal/app/controllers/api/authenticated_api_controller.rb create mode 100644 portal/app/controllers/api/v1/account_controller.rb create mode 100644 portal/app/controllers/api/v1/app_controller.rb create mode 100644 portal/app/controllers/api/v1/bets_controller.rb create mode 100644 portal/app/controllers/api/v1/exchange_accounts_controller.rb create mode 100644 portal/app/controllers/api/v1/users_controller.rb create mode 100644 portal/app/controllers/application_controller.rb create mode 100644 portal/app/controllers/pages_controller.rb create mode 100644 portal/app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 portal/app/helpers/application_helper.rb create mode 100644 portal/app/helpers/mailer_style_helper.rb create mode 100644 portal/app/jobs/account_sync_and_reconciliation_job.rb create mode 100644 portal/app/jobs/application_job.rb create mode 100644 portal/app/jobs/bet_placement_service.rb create mode 100644 portal/app/jobs/clear_old_pulls_job.rb create mode 100644 portal/app/jobs/process_subscription_job.rb create mode 100644 portal/app/jobs/pull_event_markets_job.rb create mode 100644 portal/app/jobs/pull_latest_odds_prices_job.rb create mode 100644 portal/app/jobs/pull_runner_odds_job.rb create mode 100644 portal/app/jobs/pull_tips_job.rb create mode 100644 portal/app/jobs/pull_upcoming_events_job.rb create mode 100644 portal/app/lib/general_helper.rb create mode 100644 portal/app/lib/integrations/betburger.rb create mode 100644 portal/app/lib/integrations/betfair/account_manager.rb create mode 100644 portal/app/lib/integrations/betfair/base.rb create mode 100644 portal/app/lib/integrations/betfair/bet_manager.rb create mode 100644 portal/app/lib/integrations/betfair/connection.rb create mode 100644 portal/app/lib/integrations/betfair/opportunity_hunter.rb create mode 100644 portal/app/lib/services/bet_outcome_service.rb create mode 100644 portal/app/mailers/application_mailer.rb create mode 100644 portal/app/mailers/project_mailer.rb create mode 100644 portal/app/mailers/user_mailer.rb create mode 100644 portal/app/models/account.rb create mode 100644 portal/app/models/application_record.rb create mode 100644 portal/app/models/bet.rb create mode 100644 portal/app/models/betfair_event.rb create mode 100644 portal/app/models/betfair_event_runner.rb create mode 100644 portal/app/models/betfair_runner_odd.rb create mode 100644 portal/app/models/concerns/latest.rb create mode 100644 portal/app/models/exchange_account.rb create mode 100644 portal/app/models/loggable.rb create mode 100644 portal/app/models/source_subscription.rb create mode 100644 portal/app/models/subscription_run.rb create mode 100644 portal/app/models/tip_source.rb create mode 100644 portal/app/models/tip_source_data.rb create mode 100644 portal/app/models/tipster_account.rb create mode 100644 portal/app/models/user.rb create mode 100644 portal/app/views/layouts/application.html.erb create mode 100644 portal/app/views/pages/index.html.erb create mode 100644 portal/app/views/user_mailer/notify_hourly_unread_activities.html.erb create mode 100644 portal/babel.config.js create mode 100755 portal/bin/bundle create mode 100755 portal/bin/rails create mode 100755 portal/bin/rake create mode 100755 portal/bin/setup create mode 100755 portal/bin/spring create mode 100755 portal/bin/webpack create mode 100755 portal/bin/webpack-dev-server create mode 100755 portal/bin/yarn create mode 100644 portal/client/packs/application.js create mode 100644 portal/client/src/assets/images.js create mode 100644 portal/client/src/assets/images/iconmonstr-arrow-65-240.png create mode 100644 portal/client/src/assets/images/linkedin.png create mode 100644 portal/client/src/assets/images/logo.png create mode 100644 portal/client/src/assets/images/logo.svg create mode 100644 portal/client/src/assets/images/main-bg.jpg create mode 100644 portal/client/src/assets/style.js create mode 100644 portal/client/src/assets/stylesheets/style.scss create mode 100644 portal/client/src/channels/consumer.js create mode 100644 portal/client/src/data/axiosClient.js create mode 100644 portal/client/src/data/concerns/filterable.js create mode 100644 portal/client/src/data/concerns/notfications.js create mode 100644 portal/client/src/data/entities/base_entity.js create mode 100644 portal/client/src/data/entities/bet.js create mode 100644 portal/client/src/data/entities/exchange_account.js create mode 100644 portal/client/src/data/entities/pagination.js create mode 100644 portal/client/src/data/entities/user.js create mode 100644 portal/client/src/data/index.js create mode 100644 portal/client/src/data/provider.js create mode 100644 portal/client/src/data/store.js create mode 100644 portal/client/src/data/stores/app_store.js create mode 100644 portal/client/src/data/stores/base_store.js create mode 100644 portal/client/src/data/stores/bet_store.js create mode 100644 portal/client/src/data/stores/user_store.js create mode 100644 portal/client/src/entry_point/index.js create mode 100644 portal/client/src/helpers/filter_helpers.js create mode 100644 portal/client/src/helpers/shared_helpers.js create mode 100644 portal/client/src/helpers/sidebar_helpers.js create mode 100644 portal/client/src/routes/AuthenticatedRoute.jsx create mode 100644 portal/client/src/routes/index.jsx create mode 100644 portal/client/src/routes/routes.js create mode 100644 portal/client/src/t.js create mode 100644 portal/client/src/utils/Notifier.js create mode 100644 portal/client/src/utils/needfulMethods.js create mode 100644 portal/client/src/views/bets/_bets_list.js create mode 100644 portal/client/src/views/bets/index.js create mode 100644 portal/client/src/views/dashboard/index.js create mode 100644 portal/client/src/views/layouts/authenticate/_header.js create mode 100644 portal/client/src/views/layouts/authenticate/_sidebar.js create mode 100644 portal/client/src/views/layouts/authenticate/index.js create mode 100644 portal/client/src/views/layouts/public_layout.js create mode 100644 portal/client/src/views/modules/countdown_refresh/index.js create mode 100644 portal/client/src/views/modules/editable_hash/index.js create mode 100644 portal/client/src/views/modules/filtered_list/_filters.js create mode 100644 portal/client/src/views/modules/filtered_list/index.js create mode 100644 portal/client/src/views/modules/toggler.js/index.js create mode 100644 portal/client/src/views/public/index.js create mode 100644 portal/client/src/views/shared/form_components.js create mode 100644 portal/client/src/views/shared/info_point.js create mode 100644 portal/config.ru create mode 100644 portal/config/application.rb create mode 100644 portal/config/appsignal.yml create mode 100644 portal/config/boot.rb create mode 100644 portal/config/cable.yml create mode 100644 portal/config/credentials.yml.enc create mode 100644 portal/config/database.yml create mode 100644 portal/config/environment.rb create mode 100644 portal/config/environments/development.rb create mode 100644 portal/config/environments/production.rb create mode 100644 portal/config/environments/test.rb create mode 100644 portal/config/initializers/application_controller_renderer.rb create mode 100644 portal/config/initializers/assets.rb create mode 100644 portal/config/initializers/backtrace_silencers.rb create mode 100644 portal/config/initializers/content_security_policy.rb create mode 100644 portal/config/initializers/cookies_serializer.rb create mode 100644 portal/config/initializers/devise.rb create mode 100644 portal/config/initializers/filter_parameter_logging.rb create mode 100644 portal/config/initializers/generators.rb create mode 100644 portal/config/initializers/inflections.rb create mode 100644 portal/config/initializers/keycloak.rb create mode 100644 portal/config/initializers/mime_types.rb create mode 100644 portal/config/initializers/pagy.rb create mode 100644 portal/config/initializers/sidekiq.rb create mode 100644 portal/config/initializers/stripe.rb create mode 100644 portal/config/initializers/wrap_parameters.rb create mode 100644 portal/config/locales/devise.en.yml create mode 100644 portal/config/locales/doorkeeper.en.yml create mode 100644 portal/config/locales/en.yml create mode 100644 portal/config/puma.rb create mode 100644 portal/config/routes.rb create mode 100644 portal/config/schedule.yml create mode 100644 portal/config/sidekiq.yml create mode 100644 portal/config/spring.rb create mode 100644 portal/config/storage.yml create mode 100644 portal/config/webpack/development.js create mode 100644 portal/config/webpack/environment.js create mode 100644 portal/config/webpack/production.js create mode 100644 portal/config/webpack/test.js create mode 100644 portal/config/webpacker.yml create mode 100755 portal/docker/Dockerfile create mode 100755 portal/docker/docker-compose.yml create mode 100644 portal/package.json create mode 100644 portal/postcss.config.js create mode 100644 portal/raq create mode 100644 portal/yarn.lock create mode 100644 proxy/Gemfile create mode 100644 proxy/bettermail_proxy.rb create mode 100644 stack.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a628c44 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,103 @@ +--- +kind: pipeline +type: docker +name: build-test-deploy +image_pull_secrets: + - registryLogins + +platform: + os: linux + arch: amd64 + +volumes: + - name: dockersock + host: + path: /var/run/docker.sock + +steps: + - name: notify_start + image: plugins/slack + settings: + webhook: https://team.wizewerx.tech/hooks/5eq841fropn3prt4kzgoaoprgr + channel: builds + template: > + {{uppercase repo.name}}:{{uppercase build.branch}}:{{build.number}} CI build started + - name: consumer + image: wizewerx/wizewerx-docker-compose + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + DOCKER_AUTH_CONFIG: + from_secret: registryLogins + commands: + - mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json" + - export BM_COMPONENT=consumer + - export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH} + - docker pull $BUILD_TARGET + - docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET . + - docker push $BUILD_TARGET + - name: portal + image: wizewerx/wizewerx-docker-compose + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + DOCKER_AUTH_CONFIG: + from_secret: registryLogins + commands: + - mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json" + - export BM_COMPONENT=portal + - export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH} + - docker pull $BUILD_TARGET + - docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET . + - docker push $BUILD_TARGET + - name: proxy + image: wizewerx/wizewerx-docker-compose + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + DOCKER_AUTH_CONFIG: + from_secret: registryLogins + commands: + - mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json" + - export BM_COMPONENT=proxy + - export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH} + - docker pull $BUILD_TARGET + - docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET . + - docker push $BUILD_TARGET + - name: notifications + image: wizewerx/wizewerx-docker-compose + volumes: + - name: dockersock + path: /var/run/docker.sock + environment: + DOCKER_AUTH_CONFIG: + from_secret: registryLogins + commands: + - mkdir -p "/root/.docker" && echo $DOCKER_AUTH_CONFIG > "/root/.docker/config.json" + - export BM_COMPONENT=notifications + - export BUILD_TARGET=wizewerx/bettermail_${BM_COMPONENT}:${DRONE_COMMIT_BRANCH} + - docker pull $BUILD_TARGET + - docker build --cache-from $BUILD_TARGET --build-arg GEM_CACHE=$BUILD_TARGET -f ./consumer/docker/Dockerfile -t $BUILD_TARGET . + - docker push $BUILD_TARGET + - name: notify_end + image: plugins/slack + settings: + webhook: https://team.wizewerx.tech/hooks/5eq841fropn3prt4kzgoaoprgr + channel: builds + username: mike + template: > + {{uppercase repo.name}}:{{uppercase build.branch}}:{{build.number}}{{#success build.status}} succeeded. Good job. {{else}} failed. FIX THE BUILD!!!.{{/success}} + Build took {{since build.started}} + when: + status: [ success, failure ] + branch: + - master + - production + depends_on: + - consumer + - portal + - proxy + - notifications diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76a87c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/vendor + +# Ignore all logfiles and tempfiles. +*.xcf +log/* +tmp/* +!/log/.keep +!/tmp/.keep +.idea/* +*/.idea/* +/public/uploads/* +/node_modules +app/assets/images/avatars/* +# Ignore Byebug command history file. +.byebug_history +/.vscode/* +# Ignore master key for decrypting credentials and more. +config/master.key +.env +# allow database.yml and secrets for gitlab ci pipeline build +#/config/database.yml +#/config/secrets.yml + +#ignore avatars +public/assets/ + +public/packs + +dev.env +.env*.* +storage +*.dmp +package-lock.json +out/ +/spec/ +/samples/ diff --git a/consumer/Gemfile b/consumer/Gemfile new file mode 100644 index 0000000..db05c52 --- /dev/null +++ b/consumer/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' +ruby '2.6.5' +gem 'dotenv-rails' + +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', '>= 1.4.2', require: false + +group :development do + gem 'web-console', '>= 3.3.0' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +gem 'bunny' diff --git a/notifications/Gemfile b/notifications/Gemfile new file mode 100644 index 0000000..db05c52 --- /dev/null +++ b/notifications/Gemfile @@ -0,0 +1,15 @@ +source 'https://rubygems.org' +ruby '2.6.5' +gem 'dotenv-rails' + +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', '>= 1.4.2', require: false + +group :development do + gem 'web-console', '>= 3.3.0' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +gem 'bunny' diff --git a/portal/.dockerignore b/portal/.dockerignore new file mode 100644 index 0000000..2d4c264 --- /dev/null +++ b/portal/.dockerignore @@ -0,0 +1,2 @@ +.env.* +.env*.* diff --git a/portal/.eslintrc.json b/portal/.eslintrc.json new file mode 100644 index 0000000..7c6222d --- /dev/null +++ b/portal/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "parser": "babel-eslint", + "extends": "wesbos", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": ["react"], + "rules": { + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/img-has-alt": "off", + "import/prefer-default-export": "off", + "react-hooks/exhaustive-deps": "off" + } +} diff --git a/portal/.gitignore b/portal/.gitignore new file mode 100644 index 0000000..b6c0754 --- /dev/null +++ b/portal/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/vendor + +# Ignore all logfiles and tempfiles. +*.xcf +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep +/.idea/* +/public/uploads/* +/node_modules +app/assets/images/avatars/* +# Ignore Byebug command history file. +.byebug_history +/.vscode/* +# Ignore master key for decrypting credentials and more. +/config/master.key +/.env +# allow database.yml and secrets for gitlab ci pipeline build +#/config/database.yml +#/config/secrets.yml + +#ignore avatars +/public/assets/ + +/public/packs + +/dev.env +/.env*.* +/storage +*.dmp +/brakeman.html +package-lock.json +/out/ +/ssl/ +/spec/ +/samples/ diff --git a/portal/.rspec b/portal/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/portal/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/portal/.rubocop.yml b/portal/.rubocop.yml new file mode 100644 index 0000000..ecb56a5 --- /dev/null +++ b/portal/.rubocop.yml @@ -0,0 +1,150 @@ +inherit_from: .rubocop_todo.yml +require: + - rubocop-performance + +AllCops: + NewCops: enable + Exclude: + - 'tmp/**/*' + - 'vendor/**/*' + - config/**/* + - db/**/* + - bin/**/* + - node_modules/**/* + - yarn-cache/**/* + - Rakefile + - app/javascript + - 'client/**/*' +Style/OptionalBooleanParameter: + Enabled: false +Style/Documentation: + Enabled: false + +Lint/SuppressedException: + Enabled: false + +Layout/EmptyLinesAroundBlockBody: + Enabled: false + +Layout/EmptyLinesAroundModuleBody: + Enabled: false + +Layout/EmptyLinesAroundClassBody: + Enabled: false + +Layout/EmptyLinesAroundMethodBody: + Enabled: false + +Metrics/AbcSize: + Enabled: false + Max: 20 +Metrics/ModuleLength: + Enabled: false +Metrics/BlockLength: + Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false +Metrics/ClassLength: + Enabled: false +Style/StructInheritance: + Enabled: false +Metrics/ParameterLists: + Enabled: false +Naming/MethodParameterName: + Enabled: false +Lint/RequireParentheses: + Enabled: false +Metrics/MethodLength: + Enabled: false + Max: 30 +Lint/ShadowedException: + Enabled: false +Layout/LineLength: + Max: 200 + IgnoredPatterns: [ + '# index_*' + ] + +Style/RaiseArgs: + Enabled: +Style/ClassAndModuleChildren: + Enabled: false +Style/EmptyMethod: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/BlockDelimiters: + Exclude: + - 'spec/**/*' + +Style/IfUnlessModifier: + Enabled: false + +Style/RescueStandardError: + Enabled: false +Layout/SpaceAroundMethodCallOperator: + Enabled: false +Lint/RaiseException: + Enabled: false +Lint/StructNewOverride: + Enabled: false +Style/ExponentialNotation: + Enabled: false +Style/HashEachMethods: + Enabled: false +Style/HashTransformKeys: + Enabled: false +Style/HashTransformValues: + Enabled: false +Performance/StringInclude: + Enabled: false +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: false +Lint/DeprecatedOpenSSLConstant: + Enabled: false +Lint/DuplicateElsifCondition: + Enabled: false +Lint/MixedRegexpCaptureTypes: + Enabled: false +Style/AccessorGrouping: + Enabled: false +Style/ArrayCoercion: + Enabled: false +Style/BisectedAttrAccessor: + Enabled: false +Style/CaseLikeIf: + Enabled: false +Style/HashAsLastArrayItem: + Enabled: false +Style/HashLikeCase: + Enabled: false +Style/RedundantAssignment: + Enabled: true +Style/RedundantFetchBlock: + Enabled: true +Style/RedundantFileExtensionInRequire: + Enabled: false +Style/RedundantRegexpCharacterClass: + Enabled: false +Style/RedundantRegexpEscape: + Enabled: false +Style/SlicingWithRange: + Enabled: true +Performance/AncestorsInclude: + Enabled: false +Performance/BigDecimalWithNumericArgument: + Enabled: false +Performance/RedundantSortBlock: + Enabled: false +Performance/RedundantStringChars: + Enabled: true +Performance/ReverseFirst: + Enabled: false +Performance/SortReverse: + Enabled: false +Performance/Squeeze: + Enabled: false \ No newline at end of file diff --git a/portal/.rubocop_todo.yml b/portal/.rubocop_todo.yml new file mode 100644 index 0000000..e69de29 diff --git a/portal/.ruby-gemset b/portal/.ruby-gemset new file mode 100644 index 0000000..22696ba --- /dev/null +++ b/portal/.ruby-gemset @@ -0,0 +1 @@ +betbeast diff --git a/portal/.ruby-version b/portal/.ruby-version new file mode 100644 index 0000000..d7edb56 --- /dev/null +++ b/portal/.ruby-version @@ -0,0 +1 @@ +ruby-2.6.5 diff --git a/portal/Gemfile b/portal/Gemfile new file mode 100644 index 0000000..3475c0b --- /dev/null +++ b/portal/Gemfile @@ -0,0 +1,80 @@ +source 'https://rubygems.org' +git_source(:techgit) { |repo| "https://git.wizewerx.tech/#{repo}.git" } +# gem 'wizewerx_utils', techgit: 'wizewerx/wizewerx_utils', branch: 'master' + +ruby '2.6.5' +gem 'dotenv-rails' + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 6.0.3', '>= 6.0.3.4' +# Use postgresql as the database for Active Record +gem 'pg', '>= 0.18', '< 2.0' +# Use Puma as the app server +gem 'puma', '~> 4.1' +# Use SCSS for stylesheets +gem 'sass-rails', '>= 6' +# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker +gem 'webpacker', '~> 4.0' +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 5' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.7' +# Use Redis adapter to run Action Cable in production +# gem 'redis', '~> 4.0' +# Use Active Model has_secure_password +# gem 'bcrypt', '~> 3.1.7' +gem 'devise', techgit: 'foss/devise', tag: 'v4.8.1' +# Use Active Storage variant +# gem 'image_processing', '~> 1.2' +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', '>= 1.4.2', require: false +# this is used for asset precompile for docker image build to avoid database failure, wrap in production group +# --------------------------------------------------- +gem 'activerecord-nulldb-adapter' +# --------------------------------------------------- +# +# +gem 'public_activity' + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platforms: %i[mri mingw x64_mingw] + gem 'rubocop', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails' + gem 'rubocop-rspec' + # testing support + gem 'rspec-rails', '~> 4.0.1' + gem 'factory_bot_rails' + gem 'capybara' + gem 'database_cleaner' + gem 'faker' +end + +group :development do + gem 'annotate' + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. + gem 'listen', '~> 3.2' + gem 'web-console', '>= 3.3.0' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'pry' +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] +gem 'stripe' +gem 'feature' +gem 'omniauth', '~> 1.9.1' +gem 'omniauth-keycloak', '~> 1.2.1' +gem 'keycloak', '3.2.1' +gem 'mini_magick' +gem "aws-sdk-s3", require: false +gem 'sidekiq' +gem "sidekiq-cron", "1.8.0" +gem 'ledermann-rails-settings' +gem 'appsignal' +gem 'httparty' +gem 'pagy', '~> 3.5' +gem 'activerecord-import' diff --git a/portal/Gemfile.lock b/portal/Gemfile.lock new file mode 100644 index 0000000..15cd5cb --- /dev/null +++ b/portal/Gemfile.lock @@ -0,0 +1,446 @@ +GIT + remote: https://git.wizewerx.tech/foss/devise.git + revision: ffecd4caaca700883a042561538f84507ac1edfc + tag: v4.8.1 + specs: + devise (4.8.1) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.0.6) + actionpack (= 6.0.6) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.0.6) + actionpack (= 6.0.6) + activejob (= 6.0.6) + activerecord (= 6.0.6) + activestorage (= 6.0.6) + activesupport (= 6.0.6) + mail (>= 2.7.1) + actionmailer (6.0.6) + actionpack (= 6.0.6) + actionview (= 6.0.6) + activejob (= 6.0.6) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.0.6) + actionview (= 6.0.6) + activesupport (= 6.0.6) + rack (~> 2.0, >= 2.0.8) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.0.6) + actionpack (= 6.0.6) + activerecord (= 6.0.6) + activestorage (= 6.0.6) + activesupport (= 6.0.6) + nokogiri (>= 1.8.5) + actionview (6.0.6) + activesupport (= 6.0.6) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.0.6) + activesupport (= 6.0.6) + globalid (>= 0.3.6) + activemodel (6.0.6) + activesupport (= 6.0.6) + activerecord (6.0.6) + activemodel (= 6.0.6) + activesupport (= 6.0.6) + activerecord-import (1.0.8) + activerecord (>= 3.2) + activerecord-nulldb-adapter (0.8.0) + activerecord (>= 5.2.0, < 7.1) + activestorage (6.0.6) + actionpack (= 6.0.6) + activejob (= 6.0.6) + activerecord (= 6.0.6) + marcel (~> 1.0) + activesupport (6.0.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + aes_key_wrap (1.1.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) + rake (>= 10.4, < 14.0) + appsignal (3.1.5) + rack + ast (2.4.2) + aws-eventstream (1.2.0) + aws-partitions (1.650.0) + aws-sdk-core (3.164.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.58.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.116.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + bcrypt (3.1.18) + bindata (2.4.13) + bindex (0.8.1) + bootsnap (1.13.0) + msgpack (~> 1.2) + builder (3.2.4) + byebug (11.1.3) + capybara (3.36.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + coderay (1.1.3) + concurrent-ruby (1.1.10) + connection_pool (2.3.0) + crass (1.0.6) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) + diff-lcs (1.5.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) + erubi (1.11.0) + et-orbi (1.2.7) + tzinfo + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) + faker (2.22.0) + i18n (>= 1.8.11, < 2) + faraday (2.6.0) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-net_http (3.0.1) + feature (1.4.0) + ffi (1.15.5) + fugit (1.7.1) + et-orbi (~> 1, >= 1.2.7) + raabro (~> 1.4) + globalid (1.0.0) + activesupport (>= 5.0) + hashie (5.0.0) + http-accept (1.7.0) + http-cookie (1.0.5) + domain_name (~> 0.5) + httparty (0.20.0) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jmespath (1.6.1) + json (2.3.0) + json-jwt (1.16.1) + activesupport (>= 4.2) + aes_key_wrap + bindata + faraday (~> 2.0) + faraday-follow_redirects + jwt (2.2.1) + keycloak (3.2.1) + json (= 2.3.0) + jwt (= 2.2.1) + rest-client (= 2.1.0) + ledermann-rails-settings (2.5.0) + activerecord (>= 4.2) + listen (3.7.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.19.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.2) + matrix (0.4.2) + method_source (1.0.0) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) + mini_magick (4.11.0) + mini_mime (1.1.2) + mini_portile2 (2.8.0) + minitest (5.16.3) + msgpack (1.6.0) + multi_json (1.15.0) + multi_xml (0.6.0) + netrc (0.11.0) + nio4r (2.5.8) + nokogiri (1.13.9) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + nokogiri (1.13.9-x86_64-linux) + racc (~> 1.4) + oauth2 (1.4.11) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + omniauth (1.9.2) + hashie (>= 3.4.6) + rack (>= 1.6.2, < 3) + omniauth-keycloak (1.2.1) + json-jwt (~> 1.12) + omniauth (~> 1.9.0) + omniauth-oauth2 (~> 1.6.0) + omniauth-oauth2 (1.6.0) + oauth2 (~> 1.1) + omniauth (~> 1.9) + orm_adapter (0.5.0) + pagy (3.14.0) + parallel (1.22.1) + parser (3.1.2.1) + ast (~> 2.4.1) + pg (1.4.4) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + public_activity (2.0.2) + actionpack (>= 5.0.0) + activerecord (>= 5.0) + i18n (>= 0.5.0) + railties (>= 5.0.0) + public_suffix (5.0.0) + puma (4.3.12) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.6.0) + rack (2.2.4) + rack-proxy (0.7.4) + rack + rack-test (2.0.2) + rack (>= 1.3) + rails (6.0.6) + actioncable (= 6.0.6) + actionmailbox (= 6.0.6) + actionmailer (= 6.0.6) + actionpack (= 6.0.6) + actiontext (= 6.0.6) + actionview (= 6.0.6) + activejob (= 6.0.6) + activemodel (= 6.0.6) + activerecord (= 6.0.6) + activestorage (= 6.0.6) + activesupport (= 6.0.6) + bundler (>= 1.3.0) + railties (= 6.0.6) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.3) + loofah (~> 2.3) + railties (6.0.6) + actionpack (= 6.0.6) + activesupport (= 6.0.6) + method_source + rake (>= 0.8.7) + thor (>= 0.20.3, < 2.0) + rainbow (3.1.1) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + redis (4.8.0) + regexp_parser (2.6.0) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rexml (3.2.5) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-rails (4.0.2) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.11.1) + rubocop (1.37.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.23.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.23.0) + parser (>= 3.1.1.0) + rubocop-performance (1.15.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.17.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.14.1) + rubocop (~> 1.33) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + sidekiq (6.5.7) + connection_pool (>= 2.2.5) + rack (~> 2.0) + redis (>= 4.5.0, < 5) + sidekiq-cron (1.8.0) + fugit (~> 1) + sidekiq (>= 4.2.1) + spring (2.1.1) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (4.1.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stripe (7.1.0) + thor (1.2.1) + thread_safe (0.3.6) + tilt (2.0.11) + turbolinks (5.2.1) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) + tzinfo (1.2.10) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unicode-display_width (2.3.0) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webpacker (4.3.0) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.1) + +PLATFORMS + -darwin-21 + ruby + x86_64-linux + +DEPENDENCIES + activerecord-import + activerecord-nulldb-adapter + annotate + appsignal + aws-sdk-s3 + bootsnap (>= 1.4.2) + byebug + capybara + database_cleaner + devise! + dotenv-rails + factory_bot_rails + faker + feature + httparty + jbuilder (~> 2.7) + keycloak (= 3.2.1) + ledermann-rails-settings + listen (~> 3.2) + mini_magick + omniauth (~> 1.9.1) + omniauth-keycloak (~> 1.2.1) + pagy (~> 3.5) + pg (>= 0.18, < 2.0) + pry + public_activity + puma (~> 4.1) + rails (~> 6.0.3, >= 6.0.3.4) + rspec-rails (~> 4.0.1) + rubocop + rubocop-performance + rubocop-rails + rubocop-rspec + sass-rails (>= 6) + sidekiq + sidekiq-cron (= 1.8.0) + spring + spring-watcher-listen (~> 2.0.0) + stripe + turbolinks (~> 5) + tzinfo-data + web-console (>= 3.3.0) + webpacker (~> 4.0) + +RUBY VERSION + ruby 2.6.5p114 + +BUNDLED WITH + 2.3.10 diff --git a/portal/README.md b/portal/README.md new file mode 100644 index 0000000..72ab306 --- /dev/null +++ b/portal/README.md @@ -0,0 +1,9 @@ +# Betbeast + +Betbeast is a private bets arbitration service. + +## Setting up development + +`$ bundle exec rails db:setup` + +`$ bundle exec rails s` diff --git a/portal/Rakefile b/portal/Rakefile new file mode 100644 index 0000000..e85f913 --- /dev/null +++ b/portal/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/portal/app/assets/config/manifest.js b/portal/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/portal/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/portal/app/assets/images/.keep b/portal/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/portal/app/assets/images/favicon.png b/portal/app/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b034e25fe2df0fb73425fa5824538b1aaf75a597 GIT binary patch literal 53150 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%0A~0D>ZqzsaNT~abn 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%0AnN$T_c@TBvV% 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 DHKA<0 literal 0 HcmV?d00001 diff --git a/portal/app/assets/images/logo.png b/portal/app/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39a79a34bb940b543a043125802885304aaf6e68 GIT binary patch literal 3040 zcmV<63m^1}P)Px=nn^@KRCr$PT}^CVM-@KDsau6grfsCe5{_CU5wHVtThb_9B-CNyn4-2uP@5H0 zl_+9EyQs*Hc30E|Wm8e4s_hE9NCXhY$)XKSBpRg-#Ddr*NVrvu6! z+{fH`bMMUkdGFn4&q$V@eCM8-Idi@_bADznlch|YnBRHJjowk};e$fFpGbBSQNOKj zh5uV2;!8|&mNw+{$J^FV9vd6^utNHQRNz2IIlyM?Q?m;b#QeArJ=n>fG0__&^78cf z@Uc$zBeWejP!$IfV4Iv>JW4E26Y(8YHBgR@tdj8aGvh8}Ot)(vwN`q<0?o;b z z0npw#^CqpW{X3%9+j}$Jf8cPXfSWloR4E7o*xXa!4PXI109#QZ+7w{Bw7f*~^Jg-7P}Np4fGvP+OM~s=#lO_KYQFc_Wn<@J{8JIA3~#TS%aYW^;|N z?XLg1xzn_Ac{!6!H{H}rN5;Ncnw9O;Y>Lr8CZg{e9Y-MfRU-PCPmos$_zx8wxTy7l zATeTBpp_cK@&TJ#5PoXVN5J(-5qX4&Fcx_2^zs{zw&Pf26i?Fl0-Cz#4`Ts6#IiDj z?IQ95I)T3V3We((5kb84HD<=#G!*x=H?vic!?YWuCUefzBhO%)f! z*Or#vOVw`uy9Q`@@4eBxaRI&S*FTDa-#hNunV^@&{C0tjh#n&%Fx=`!R(-@u4gpNf zst^$X)kP;B@(axTREnTDgW4%ELuA4%Sr!blnXI-OU?U=o6+%6)791T^05ZUXOqZw9 zgdPp6#RFgiZLi{b#|0+WbJZ`50pmkF7of%BfiXi4uux(?k#Q7_uX3=Z#n1Y8eTD`H zKi>#i13>T2ouW@ZS#K~8StO`~f?)U3(%)(2^1o<(J(;}kyUlDP*t`TlX$9-*tbq24 zFCkU$Rb~dz6U9@rS+iksv~SI>&_IlZ0NODXNJ=X}Kp~J=h^$*n7jQBboK%3iz~u2f z4vc?5L=U7bqKY3FBQzg?SzQLn@89@k=6u1+I6VC2C~yrx{pp=IBf!NT$T)=yj+CtO z^#{N0l#3dI4SgOow@qqGhahWOxn{|AjO#*}qit4P+61<=vBV(83ZJR5ly-m?0LYe@ zd9(;{ioD|irp$CL5y2H4=8FKCpo?*dp%RcQDd&0Nk~;tC6c8zhTp1 zyXY!40~@wUPsVnXTCo9u?^wq1n7L&V5dh7!j{vYoG@6(wO*t!@>>x@k6Lhe8{8=T3?U_=&lz4s54R&(hkOU$VCzPxA>0 z#Dd$`cN-l#IPStDwNbM*X)7hDuu^j(YuF@{BF|#${Sww6Cnpp9b*9_iAy}&DIYPuLm@kHzO;hk*Qf@Q##F&5Yy zw%g0@(QU1wiO7mqciIwzYpB^GPq44%)&PgCX?0-p$q><{1nF5QrV}I!NQVzUl<-!j z)vW>8u)kYYZi!}#8}rG7Y!%SDT(cpR z;OEj+3~ZCl)=JG5wvieb3SEm(1>p94?u)c%&zF30*{Ihgn#~y=$UE>BYBrmFX@(tA zw}V;Z5RqO6pJ z1=upOToN(H65dFJNsx`v6!NH{%~lWBf0|Gr4eP?fIa+q4Va4y`Oxs?S)|E%l^txRF zwrKlg8Dmo#mfz?SMxSFy;4%}g1Gri||x zu_iCO?5?cmMS|MsYfEi58#Cakyu7kZmzMw8lxc#@klh2HrCt34-X6y+hIa$l)Fjwk z>Ue~dF9fWkY0fzlE0cxTA|YMtHLXp=3T<1aL@-^|kGGLc&(h{f zOV>&S$m9BXg@-{6% z!}b(+H{zH|>gjDQkv9{-B7*Jp1nz`{M>hk&uB?97a{vczK*Ob;Q&fu&#`vttr}P9{ zxPCm>jmUYxrUVrl4uWYWFQ)+@?Mzu2umL2t(+YYYeA3CH<0r}QX4AH}7z6qQV4^+zj{AiE!XAg6 z8}Tcj&9=oqtf968iL+y1e8%btW}DX}oOij5m6pSM<`v>-4u=&nsn2BGd5AumUOl*~ z8_txh+nv?52yn%lsad~;P&uw-K)YxkcHenrKYNrf`DVr65icfg! zRlpaF5BF8ao-1LEogn~f zu3&eDrf_O$CEO8ctGKscEmjatZ10=`m&HzvRwaF_V(PTHDm}jNF~llnq$V`F<+}wn zVOQI`p?&6;UnHWMe7L@8v$4ud;WNJu@d#jRJAQIC7(QU1i(U1u9KcrZ`AM8}nw(uc zN-R&;=R@|gO2W_2j1N6mZ#LGWcL8kmn1{4pG#x6Onq8P6=EpnzL3@0#oA;v$!;R*_oP|0+6ob0Vv@78A*Vmy zwtn*1*hunYZ$&9@k@o`F>OIk3U-A6jOLM)ycw*oGZ(eP|nA*`s3%Xq^lFChO#sJ|) iZ~3QyYsH9~cIJOg1C^$OWAj@80000= #{v_symbolize[:min]} AND expected_value <= #{v_symbolize[:max]}") + end + # add additional filters later + # + summary = summary_dashboard(exchange_account, bets) + + pagy, bets = pagy(bets) + + pagy_json = pagy_metadata(pagy) + + + render json: { summary: summary, pagy: pagy_json, bets: bets.order(created_at: :desc).map(&:json_payload) } + end + + def add_placed_bet + # push a placed bet from an authenticated client + wpb = 'WEB_PLACED_BETS' + + ex = ExchangeAccount.find_by(id: wpb) + stake = (params['stake'] || 0).to_f + executed_odds = (params['odds_accepted'] || 0).to_f + record = { + tip_provider_id: 'betburger', + tip_provider_bet_id: params['betburger_bet_id'], + tip_provider_percent: (params['value_bet_ev_percent'] || 0).to_f, + tip_provider_odds: (params['original_odds'] || 0).to_f, + team1: "Not Set", + team2: "Not Set", + exchange_id: params['bookmaker'], + exchange_event_name: params['match'], + exchange_market_details: {market: params['market'], selection: params['selection']}, + stake: stake, + executed_odds: executed_odds, + expected_value: Bet.calculate_expected_value(stake, executed_odds), + original_json: params.to_json, + outcome: {'Bet placed': 'open', 'Odds changed': 'expired'}[params['placer_result']] || 'skipped', + log: [params['placer_result']], + } + ex.my_bets.create(record) + render json: { success: true } + end + + def tips + id = params[:id] + resp = { success: false } + ts = id.nil? ? nil : TipSource.find_by(id: id) + if ts + resp[:success] = true + latest_source_data = ts.tip_source_data.latest + resp[:data] = latest_source_data.blank? ? {} : latest_source_data.data + else + resp[:message] = 'Unknown tip source requested' + end + render json: resp + end + + def summary_dashboard(exchange_account, bets) + { + exchange_account: exchange_account.json_payload, + total_win_amount: exchange_account.total_won(bets), + total_lost_amount: exchange_account.total_lost(bets), + total_risked_amount: exchange_account.total_open(bets), + total_tips: bets.count, + total_tips_skipped: bets.skipped.count, + total_tips_processing: bets.processing.count, + total_tips_expired: bets.expired.count, + total_tips_ignored: bets.ignored.count, + total_tips_voided: bets.voided.count, + total_placed_bets: bets.placed_bets.count, + total_placed_bets_won: bets.won.count, + total_placed_bets_lost: bets.lost.count, + total_placed_bets_open: bets.open.count, + average_odds_won: odd_averages(bets.won), + average_odds_lost: odd_averages(bets.lost), + average_odds: odd_averages(bets), + running_since: exchange_account.my_bets.minimum(:created_at) || exchange_account.created_at, + } + end + + def odd_averages(bets) + return 0 if bets.count.zero? + + total_odds = 0 + bets.map { |b| total_odds += b.tip_provider_odds.to_f } + (total_odds / bets.count).round(2) + end +end diff --git a/portal/app/controllers/api/v1/exchange_accounts_controller.rb b/portal/app/controllers/api/v1/exchange_accounts_controller.rb new file mode 100644 index 0000000..c13bb9e --- /dev/null +++ b/portal/app/controllers/api/v1/exchange_accounts_controller.rb @@ -0,0 +1,14 @@ +class Api::V1::ExchangeAccountsController < Api::AuthenticatedApiController + def update + exchange_account = ExchangeAccount.find(params[:exchange_account_id]) + render json: { success: false} and return unless exchange_account + + payload = {} + payload[:betting_enabled] = params['canBet'] unless params['canBet'].nil? + payload[:status] = params['isActive'] == true ? 'active' : 'inactive' unless params['isActive'].nil? + + exchange_account.update(payload) unless payload.empty? + render json: { success: true} and return + end + +end diff --git a/portal/app/controllers/api/v1/users_controller.rb b/portal/app/controllers/api/v1/users_controller.rb new file mode 100644 index 0000000..6be5dbe --- /dev/null +++ b/portal/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::UsersController < Api::AuthenticatedApiController + skip_before_action :authenticate_user!, only: [ :auth] + + def auth + unless user_signed_in? + render json: { success: false } + return + end + + render json: { success: true, user: current_user.json_payload} + end + +end diff --git a/portal/app/controllers/application_controller.rb b/portal/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/portal/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/portal/app/controllers/pages_controller.rb b/portal/app/controllers/pages_controller.rb new file mode 100644 index 0000000..39848c6 --- /dev/null +++ b/portal/app/controllers/pages_controller.rb @@ -0,0 +1,4 @@ +class PagesController < ApplicationController + def index + end +end diff --git a/portal/app/controllers/users/omniauth_callbacks_controller.rb b/portal/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 0000000..bb67faf --- /dev/null +++ b/portal/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Users + class OmniauthCallbacksController < Devise::OmniauthCallbacksController + def keycloakopenid + Rails.logger.debug(request.env['omniauth.auth']) + @user = User.find_or_create_from_auth_hash(request.env['omniauth.auth']) + sign_in_and_redirect @user, event: :authentication + end + + def failure + redirect_to root_path + end + end +end diff --git a/portal/app/helpers/application_helper.rb b/portal/app/helpers/application_helper.rb new file mode 100644 index 0000000..ca1a4fd --- /dev/null +++ b/portal/app/helpers/application_helper.rb @@ -0,0 +1,25 @@ +module ApplicationHelper + def render_activities(_activities) + 'Activities' + end + + def filter_by_date_range(records, field_string) + return records if field_string.blank? + + field = params[field_string.to_sym] + + return records unless field + + ea_symbolize = JSON.parse(field).symbolize_keys + + parsed_date_from = nil + parsed_date_to = nil + + parsed_date_from = DateTime.parse(ea_symbolize[:from]) unless ea_symbolize[:from].nil? + parsed_date_to = DateTime.parse(ea_symbolize[:to]) unless ea_symbolize[:to].nil? + + records = records.where("#{field_string} >= ? AND #{field_string} <= ?", parsed_date_from, parsed_date_to + 1.day) unless parsed_date_from.nil? + + records + end +end diff --git a/portal/app/helpers/mailer_style_helper.rb b/portal/app/helpers/mailer_style_helper.rb new file mode 100644 index 0000000..db4a210 --- /dev/null +++ b/portal/app/helpers/mailer_style_helper.rb @@ -0,0 +1,11 @@ +module MailerStyleHelper + def mail_logo_style + "background: #222C36; + width: 100%; + display: inline-block; + padding: 15px; + box-sizing: border-box; + border-radius: 10px 10px 0 0" + end +end +# reload!; ProjectMailer.notify_helper_for_join_request(ProjectHelper.first).deliver_now diff --git a/portal/app/jobs/account_sync_and_reconciliation_job.rb b/portal/app/jobs/account_sync_and_reconciliation_job.rb new file mode 100644 index 0000000..d483e77 --- /dev/null +++ b/portal/app/jobs/account_sync_and_reconciliation_job.rb @@ -0,0 +1,14 @@ +class AccountSyncAndReconciliationJob < ApplicationJob + queue_as :high + + def perform(args = {}) + ExchangeAccount.active.each do | ea| + am = Integrations::Betfair::AccountManager.new(ea) + bm = Integrations::Betfair::BetManager.new(ea) + puts "Refreshing account balance on '#{ea.id}'" + am.refresh_account_balance + puts "Reconcile open bets on '#{ea.id}'" + bm.check_qualified_bet_outcome(ea.my_bets.open) + end + end +end diff --git a/portal/app/jobs/application_job.rb b/portal/app/jobs/application_job.rb new file mode 100644 index 0000000..f332cde --- /dev/null +++ b/portal/app/jobs/application_job.rb @@ -0,0 +1,8 @@ +class ApplicationJob < ActiveJob::Base + sidekiq_options retry: false + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/portal/app/jobs/bet_placement_service.rb b/portal/app/jobs/bet_placement_service.rb new file mode 100644 index 0000000..2e1dd6a --- /dev/null +++ b/portal/app/jobs/bet_placement_service.rb @@ -0,0 +1,63 @@ +class BetPlacementService < ApplicationJob + queue_as :high + + def perform(args = {}) + # do not proceed if we have already procssed a bet for this tip + bet = args[:bet] + # get a valid betfair instance for the account we are using, halt if not valid. + exchange = Integrations::Betfair::BetManager.new(bet.exchange_account) + return unless exchange + + last_step = 'Checking event id' + begin + # check this bet with the exchange and populate the event id if it is missing + + unless bet.exchange_event_id + bet.exchange_event_id = exchange.bet_event(bet) + end + bet.placement_attempts += 1 + + # check the odds are still good at the exchange for that event + last_step = 'Checking odds' + prices_and_stakes = exchange.bet_odds(bet) + prices = prices_and_stakes[:prices] + + bet.exchange_odds = prices_and_stakes + bet.outcome = 'expired' + tip_odds = bet.tip_provider_odds.to_f + + # use tip odds or get closest match to tip odds offered by the exchange + last_step = 'Determining if odds optimal' + margin = tip_odds * bet.exchange_account.current_stake_strategy[:odds_margin] + max_tip_odds = tip_odds + margin + odds_to_execute_at = prices.min_by { |num| (max_tip_odds - num).abs } + raise 'Cannot determine optimal odds' unless odds_to_execute_at + if odds_to_execute_at.between?(tip_odds, tip_odds + margin) + last_step = 'Determining stake and placing bet' + odds_stake = prices_and_stakes[:stakes][odds_to_execute_at.to_s] + + bet.executed_odds = odds_to_execute_at.to_s + # stake will always fill the max available or the limits of our stake management - whichever is the lower. + stake = bet.exchange_account.optimal_stake(bet, exchange.minimum_stake, odds_stake) + + raise 'Optimal stake is zero. Not taking bet' if stake.zero? + + bet.outcome = 'open' + bet.stake = stake + bet.expected_value = Bet.calculate_expected_value(stake, odds_to_execute_at) + if bet.exchange_account.can_bet? + bet.exchange_bet_id = exchange.place_bet(bet, stake) + end + else + bet.log << '[Bet placement] Tip odds not found in latest prices, nudge too far out' + end + rescue Exception => e + x = "Placement #{bet.placement_attempts}--->Last step: #{last_step}. Error: #{e.message}. Skipping" + bet.log ||= [] + bet.log << x + bet.outcome = e.message.include?('PERMISSION_DENIED') ? 'errored' : 'skipped' + ensure + bet.save! + end + end +end diff --git a/portal/app/jobs/clear_old_pulls_job.rb b/portal/app/jobs/clear_old_pulls_job.rb new file mode 100644 index 0000000..49b56e7 --- /dev/null +++ b/portal/app/jobs/clear_old_pulls_job.rb @@ -0,0 +1,8 @@ +class ClearOldPullsJob < ApplicationJob + queue_as :high + + def perform(args = {}) + max_hours_to_keep = ENV['MAX_HISTORY_HOURS']&.to_i || 2 + TipSourceData.where("created_at < ?", max_hours_to_keep.hours.ago).delete_all + end +end diff --git a/portal/app/jobs/process_subscription_job.rb b/portal/app/jobs/process_subscription_job.rb new file mode 100644 index 0000000..a9e99b4 --- /dev/null +++ b/portal/app/jobs/process_subscription_job.rb @@ -0,0 +1,10 @@ +class ProcessSubscriptionJob < ApplicationJob + queue_as :high + + def perform(args = {}) + subscription = args[:subscription] + tsd = args[:tsd] + bb = Integrations::Betburger.new() + bb.process_subscription_tips(subscription: subscription, data: tsd) + end +end diff --git a/portal/app/jobs/pull_event_markets_job.rb b/portal/app/jobs/pull_event_markets_job.rb new file mode 100644 index 0000000..f22366b --- /dev/null +++ b/portal/app/jobs/pull_event_markets_job.rb @@ -0,0 +1,11 @@ +class PullEventMarketsJob < ApplicationJob + queue_as :medium + + def perform(args = {}) + ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT']) + raise 'No Betfair hunter account' unless ea + + hunter = Integrations::Betfair::OpportunityHunter.new(ea) + hunter.event_markets_and_selections #refresh the markets to include + end +end diff --git a/portal/app/jobs/pull_latest_odds_prices_job.rb b/portal/app/jobs/pull_latest_odds_prices_job.rb new file mode 100644 index 0000000..0b67386 --- /dev/null +++ b/portal/app/jobs/pull_latest_odds_prices_job.rb @@ -0,0 +1,13 @@ +class PullLatestOddsPricesJob < ApplicationJob + queue_as :medium + + def perform(args = {}) + ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT']) + raise 'No Betfair hunter account' unless ea + + hunter = Integrations::Betfair::OpportunityHunter.new(ea) + BetfairEventRunner.runners_for_open_events.each do | runner | + PullRunnerOddsJob.perform_later(hunter: hunter, runner: runner) + end + end +end diff --git a/portal/app/jobs/pull_runner_odds_job.rb b/portal/app/jobs/pull_runner_odds_job.rb new file mode 100644 index 0000000..1c4a182 --- /dev/null +++ b/portal/app/jobs/pull_runner_odds_job.rb @@ -0,0 +1,9 @@ +class PullRunnerOddsJob < ApplicationJob + queue_as :medium + + def perform(args = {}) + hunter = params[:oh] + runner = params[:runner] + hunter.runner_odds(runner) + end +end diff --git a/portal/app/jobs/pull_tips_job.rb b/portal/app/jobs/pull_tips_job.rb new file mode 100644 index 0000000..075d66d --- /dev/null +++ b/portal/app/jobs/pull_tips_job.rb @@ -0,0 +1,16 @@ +class PullTipsJob < ApplicationJob + queue_as :high + + def perform(args = {}) + ta = TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT']) + bb = Integrations::Betburger.new(ta) + ta.tip_sources.active_sources.each do |ts| + tsd = bb.pull_and_save_tips(ts) + ts.source_subscriptions.each do |ss| + next unless ss.exchange_account.active? + + ProcessSubscriptionJob.perform_later(subscription: ss, tsd: tsd) + end + end + end +end diff --git a/portal/app/jobs/pull_upcoming_events_job.rb b/portal/app/jobs/pull_upcoming_events_job.rb new file mode 100644 index 0000000..a374439 --- /dev/null +++ b/portal/app/jobs/pull_upcoming_events_job.rb @@ -0,0 +1,13 @@ +class PullUpcomingEventsJob < ApplicationJob + queue_as :medium + + def perform(args = {}) + ea = ExchangeAccount.find_by(id: ENV['BETFAIR_HUNTER_ACCOUNT']) + raise 'No Betfair hunter account' unless ea + + hunter = Integrations::Betfair::OpportunityHunter.new(ea) + top_of_hour = Time.now.beginning_of_hour + hunter.events_in_timeframe(from: top_of_hour, to: top_of_hour + 1.hour) #pull events + hunter.event_markets_and_selections #refresh the markets to include + end +end diff --git a/portal/app/lib/general_helper.rb b/portal/app/lib/general_helper.rb new file mode 100644 index 0000000..652d2b6 --- /dev/null +++ b/portal/app/lib/general_helper.rb @@ -0,0 +1,6 @@ +class GeneralHelper + + def self.broadcast_to_account_channel(account_id, data) + ExchangeAccountChannel.broadcast_to(account_id, data) + end +end diff --git a/portal/app/lib/integrations/betburger.rb b/portal/app/lib/integrations/betburger.rb new file mode 100644 index 0000000..e74a926 --- /dev/null +++ b/portal/app/lib/integrations/betburger.rb @@ -0,0 +1,940 @@ +module Integrations + class Betburger + include HTTParty + attr_reader :access_token + + URIS = { + live: 'https://rest-api-lv.betburger.com/api/v1/valuebets/bot_pro_search', + prematch: 'https://rest-api-pr.betburger.com/api/v1/valuebets/bot_pro_search', + }.freeze.with_indifferent_access + + def initialize(tipster_account=nil) + super() + tipster_account ||= TipsterAccount.find_by(id: ENV['TIPSTER_ACCOUNT']) + @access_token = tipster_account.apikey + end + + def pull_and_save_tips(source) + url = URIS[source.source_type] + return unless url + + response = self.class.post(url, { body: { per_page: 32, search_filter: source.filters, access_token: @access_token } }) + source.tip_source_data.create(data: response) + end + + def process_subscription_tips(subscription:, data:,place_bets: true) + + return unless subscription + + tsd = data || subscription.tip_source_data.last + + # tips already processed + return if subscription.subscription_runs.where(tip_source_data_id: tsd.id).exists? + + json = tsd.data + return unless json['total_by_filter'].to_i.positive? + + percentages = {} + vb_refs = json['source']['value_bets'] + vb_refs.each do |vb_ref| + percentages[vb_ref['bet_id']] = { percent: vb_ref['percent'] } + end + bet_refs = json['bets'] + stats = { non_valuebet: 0, unsupported_exchange: 0, duplicate: 0, processed: 0 } + bet_refs.each do |bf| + place_this_bet = place_bets + unless bf['is_value_bet'] == true + stats[:non_valuebet] += 1 + next + end + + exchange = exchanges[(bf['bookmaker_id']).to_s] + unless exchange + stats[:unsupported_exchange] += 1 + next + end + + tipster_event_id = bf['event_id'] + + log = [] + bet_id = bf['id'] + + vb = populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id) + # on live betting, strictly adhere to max_odds rule. Otherwise bet on all odds to generate learning data. + odds_within_range = vb[:tip_provider_odds].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_odds_to_bet], subscription.exchange_account.current_stake_strategy[:max_odds_to_bet]) + ev_within_range = vb[:tip_provider_percent].to_f.between?( subscription.exchange_account.current_stake_strategy[:min_ev], subscription.exchange_account.current_stake_strategy[:max_ev]) + unless odds_within_range && ev_within_range + log << "[Parsing] Tip provider odds not within #{subscription.exchange_account.current_stake_strategy[:min_odds_to_bet]} and #{subscription.exchange_account.current_stake_strategy[:max_odds_to_bet]}" unless odds_within_range + log << "[Parsing] Tip provider EV not within #{subscription.exchange_account.current_stake_strategy[:min_ev]} and #{subscription.exchange_account.current_stake_strategy[:max_ev]}" unless ev_within_range + vb[:outcome] = 'ignored' + place_this_bet = false + end + + vb[:log] = log + skip_duplicate_event_check = false + duplicates = subscription.exchange_account.my_bets.where(tip_provider_bet_id: bet_id) + if duplicates.exists? + stats[:duplicate] += 1 + if duplicates.retryable_bets.count.positive? + bet = duplicates.retryable_bets.last #only supporting 1 duplicate! + bet.log = [] + bet.update(vb) + skip_duplicate_event_check = true + else + next + end + end + + unless skip_duplicate_event_check + next if subscription.exchange_account.my_bets.where(tip_provider_event_id: tipster_event_id).exists? && !subscription.exchange_account.allow_multiple_bets_per_event + end + + begin + bet ||= Bet.unscoped.create(vb) + + if bet && place_this_bet && vb[:exchange_market_details] + BetPlacementService.perform_later(bet: bet) + end + rescue Exception => e + puts "Something weird happened: #{e.message}" + end + stats[:processed] += 1 + end + subscription.subscription_runs.create(tip_source_data_id: tsd.id, log: ["Payload: #{bet_refs.size}. #{stats}"]) + end + + def populate_bet_hash(subscription, bet_id, bf, exchange, log, percentages, tipster_event_id) + vb = { tip_provider_id: 'betburger', tip_provider_bet_id: bet_id } + vb[:exchange_id] = exchange + vb[:exchange_account_id] = subscription.exchange_account.id + vb[:tip_provider_event_id] = tipster_event_id + vb[:tip_provider_odds] = bf['koef'] # what the tip provider says the odds are from the exchange + vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank + vb[:team1] = bf['team1_name'] + vb[:team2] = bf['team2_name'] + vb[:period] = periods[bf['period_id'].to_s.to_sym] unless bf['period_id'].blank? + vb[:exchange_event_name] = vb[:team1] + vb[:exchange_event_name] += " v #{vb[:team2]}" unless vb[:team2].blank? + vb[:exchange_event_id] = bf['bookmaker_event_direct_link'] # may be blank + vb[:tip_provider_percent] = percentages[bet_id][:percent] # dunno what this is used for + tip_market_details = {} + tip_market_details[:market] = variations[bf['market_and_bet_type']&.to_s] + tip_market_details[:params] = bf['market_and_bet_type_param']&.to_s + vb[:tip_provider_market_details] = tip_market_details + exchange_market_details = map_exchange_market_details(exchange, tip_market_details, vb[:team1], vb[:team2]) + vb[:original_json] = bf + if exchange_market_details + vb[:exchange_market_details] = exchange_market_details + else + vb[:outcome] = 'ignored' + log << '[Parsing] Unsupported market/selection' + end + vb + end + + def exchanges + @exchanges ||= { + '11' => 'betfair' + } + end + + def map_exchange_market_details(exchange, tip_market_details, team1, team2) + market_mapping = mapped_markets(exchange) + return unless market_mapping + + rtn = market_mapping[tip_market_details[:market]]&.clone + return unless rtn + + swaps = { 'Team1' => team1, 'Team2' => team2, '%s' => tip_market_details[:params]} + if tip_market_details[:params] + rounded = tip_market_details[:params].to_f.round + swaps['%rounded_s'] = "#{rounded.positive? ? '+':''}#{rounded}" if tip_market_details[:params] + swaps['%rounded_positive_s'] = "#{rounded.abs}" + swaps['%nearest_half_over_s'] = determine_nearest_half(tip_market_details[:params], 'over') if tip_market_details[:params] + swaps['%nearest_half_under_s'] = determine_nearest_half(tip_market_details[:params], 'under') if tip_market_details[:params] + end + + swaps.keys.each do |k| + rtn[:market] = rtn[:market].sub(k, swaps[k]) + rtn[:selection] = rtn[:selection].sub(k, swaps[k]) + end + if rtn.key?(:handicap) + rtn[:handicap] = tip_market_details[:params].to_f + end + rtn + end + + def determine_nearest_half(line, direction ) + line_f = line.to_f - line.to_i + return line if line_f == 0.5 + + line_f = line.to_i + 0.5 + line_f -= 1 if direction == 'over' + line_f += 1 if direction == 'under' + + line_f.to_s + end + + + def mapped_markets(key) + # this is ready for other exchanges + @mapped_markets ||= { 'betfair' => betfair_markets } + @mapped_markets[key] + end + + def betfair_markets + { + 'Team1 Win' => { market: 'Match Odds', selection: 'Team1' }, + 'Team2 Win' => { market: 'Match Odds', selection: 'Team2' }, + 'X' => { market: 'Match Odds', selection: 'The Draw' }, + '1' => { market: 'Match Odds', selection: 'Team1' }, + '2' => { market: 'Match Odds', selection: 'Team2' }, + '1X' => { market: 'Double Chance', selection: 'Home or Draw' }, + '12' => { market: 'Double Chance', selection: 'Home or Away' }, + 'X2' => { market: 'Double Chance', selection: 'Draw or Away' }, + 'Total Over(%s)' => { market: 'Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' }, + 'Total Over(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' }, + 'Total Over(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_over_s Goals', selection: 'Over %nearest_half_over_s Goals' }, + 'Total Under(%s)' => { market: 'Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' }, + 'Total Under(%s) for Team1' => { market: 'Team1 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' }, + 'Total Under(%s) for Team2' => { market: 'Team2 Over/Under %nearest_half_under_s Goals', selection: 'Under %nearest_half_under_s Goals' }, + 'Asian Handicap1(%s)' => { market: 'Asian Handicap', selection: 'Team1' }, + 'Asian Handicap1(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team1' }, + 'Asian Handicap2(0.0)/Draw No Bet' => { market: 'Draw no Bet', selection: 'Team2' }, + 'Asian Handicap2(%s)' => { market: 'Asian Handicap', selection: 'Team2' }, + 'European Handicap1(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team1 %rounded_s' }, + 'European Handicap2(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Team2 %rounded_s' }, + 'European HandicapX(%s)' => { market: 'Team1 +%rounded_positive_s', selection: 'Draw' }, + 'Both to score' => { market: 'Both teams to score?', selection: 'Yes' }, + 'One scoreless' => { market: 'Both teams to score?', selection: 'No' }, + 'Only one to score' => { market: 'Both teams to score?', selection: 'No' }, + 'Odd' => { market: 'Total Goals Odd/Even', selection: 'Odd' }, + 'Even' => { market: 'Total Goals Odd/Even', selection: 'Even' }, + 'Mat' => { market: 'Total Goals Odd/Even', selection: 'Even' }, + } + end + + def variations + @variations ||= + { '1' => 'Team1 Win', + '2' => 'Team2 Win', + '3' => 'Asian Handicap1(0.0)/Draw No Bet', + '4' => 'Asian Handicap2(0.0)/Draw No Bet', + '5' => 'European Handicap1(%s)', + '6' => 'European HandicapX(%s)', + '7' => 'European Handicap2(%s)', + '8' => 'Both to score', + '9' => 'One scoreless', + '10' => 'Only one to score', + '11' => '1', + '12' => 'X', + '13' => '2', + '14' => '1X', + '15' => 'X2', + '16' => '12', + '17' => 'Asian Handicap1(%s)', + '18' => 'Asian Handicap2(%s)', + '19' => 'Total Over(%s)', + '20' => 'Total Under(%s)', + '21' => 'Total Over(%s) for Team1', + '22' => 'Total Under(%s) for Team1', + '23' => 'Total Over(%s) for Team2', + '24' => 'Total Under(%s) for Team2', + '25' => 'Odd', + '26' => 'Even', + '27' => '1 - Yellow Cards', + '28' => 'X - Yellow Cards', + '29' => '2 - Yellow Cards', + '30' => '1X - Yellow Cards', + '31' => '12 - Yellow Cards', + '32' => 'X2 - Yellow Cards', + '33' => 'Asian Handicap1(%s) - Yellow Cards', + '34' => 'Asian Handicap2(%s) - Yellow Cards', + '35' => 'Total Over(%s) - Yellow Cards', + '36' => 'Total Under(%s) - Yellow Cards', + '37' => 'Total Under(%s) for Team1 - Yellow Cards', + '38' => 'Total Over(%s) for Team1 - Yellow Cards', + '39' => 'Total Under(%s) for Team2 - Yellow Cards', + '40' => 'Total Over(%s) for Team2 - Yellow Cards', + '41' => 'Even - Yellow Cards', + '42' => 'Odd - Yellow Cards', + '43' => '1 - Corners', + '44' => 'X - Corners', + '45' => '2 - Corners', + '46' => '1X - Corners', + '47' => '12 - Corners', + '48' => 'X2 - Corners', + '49' => 'Asian Handicap1(%s) - Corners', + '50' => 'Asian Handicap2(%s) - Corners', + '51' => 'Total Over(%s) - Corners', + '52' => 'Total Under(%s) - Corners', + '53' => 'Total Under(%s) for Team1 - Corners', + '54' => 'Total Over(%s) for Team1 - Corners', + '55' => 'Total Under(%s) for Team2 - Corners', + '56' => 'Total Over(%s) for Team2 - Corners', + '57' => 'Odd - Corners', + '58' => 'Even - Corners', + '63' => 'Red card - yes', + '64' => 'No red card', + '65' => 'Penalty - yes', + '66' => 'No penalty', + '67' => 'Score (%s)', + '68' => 'Total Over(%s) - Tie Break', + '69' => 'Total Under(%s) - Tie Break', + '70' => 'Score (%s) - not', + '71' => 'Any substitute to score a goal - yes', + '72' => 'Any substitute to score a goal - no', + '73' => 'Team1 to win by exactly 1 goal - yes', + '74' => 'Team1 to win by exactly 1 goal - no', + '75' => 'Team2 to win by exactly 1 goal - yes', + '76' => 'Team2 to win by exactly 1 goal - no', + '77' => 'Team1 to win by exactly 2 goals - yes', + '78' => 'Team1 to win by exactly 2 goals - no', + '79' => 'Team1 to win by exactly 3 goals - yes', + '80' => 'Team1 to win by exactly 3 goals - no', + '81' => 'Team2 to win by exactly 2 goals - yes', + '82' => 'Team2 to win by exactly 2 goals - no', + '83' => 'Team2 to win by exactly 3 goals - yes', + '84' => 'Team2 to win by exactly 3 goals - no', + '85' => 'Team1 to win to Nil - yes', + '86' => 'Team1 to win to Nil - no', + '87' => 'Team2 to win to Nil - yes', + '88' => 'Team2 to win to Nil - no', + '89' => 'Team1 to win either halves - yes', + '90' => 'Team1 to win either halves - no', + '91' => 'Team2 to win either halves - yes', + '92' => 'Team2 to win either halves - no', + '93' => 'Draw in either half - yes', + '94' => 'Draw in either half - no', + '95' => 'Team1 to win in both halves - yes', + '96' => 'Team1 to win in both halves - no', + '97' => 'Team2 to win in both halves - yes', + '98' => 'Team2 to win in both halves - no', + '99' => 'Team1 to win and Total Over 2.5 - yes', + '100' => 'Team1 to win and Total Over 2.5 - no', + '101' => 'Team1 to win and Total Under 2.5 - yes', + '102' => 'Team1 to win and Total Under 2.5 - no', + '103' => 'Team2 to win and Total Over 2.5 - yes', + '104' => 'Team2 to win and Total Over 2.5 - no', + '105' => 'Team2 to win and Total Under 2.5 - yes', + '106' => 'Team2 to win and Total Under 2.5 - no', + '107' => 'Draw in both half - yes', + '108' => 'Draw in both half - no', + '109' => 'Draw and Total Over 2.5 - yes', + '110' => 'Draw and Total Over 2.5 - no', + '111' => 'Draw and Total Under 2.5 - yes', + '112' => 'Draw and Total Under 2.5 - no', + '113' => 'Goals in both halves - yes', + '114' => 'Goals in both halves - no', + '115' => 'Team1 to score in both halves - yes', + '116' => 'Team1 to score in both halves - no', + '117' => 'Team2 to score in both halves - yes', + '118' => 'Team2 to score in both halves - no', + '119' => 'Double - yes', + '120' => 'Double - no', + '121' => 'Hattrick - yes', + '122' => 'Hattrick - no', + '123' => 'Own goal - yes', + '124' => 'Own goal - no', + '125' => 'Both halves > 1.5 goals - yes', + '126' => 'Both halves > 1.5 goals - no', + '127' => 'Both halves < 1.5 goals - yes', + '128' => 'Both halves < 1.5 goals - no', + '129' => 'Sets (%s)', + '130' => 'Sets (%s) - not', + '131' => 'Asian Handicap1(%s) - Sets', + '132' => 'Asian Handicap2(%s) - Sets', + '133' => 'Total Over(%s) - Sets', + '134' => 'Total Under(%s) - Sets', + '135' => 'Team1/Team1', + '136' => 'Team1/Team1 - no', + '137' => 'Team1/Draw', + '138' => 'Team1/Draw - no', + '139' => 'Team1/Team2', + '140' => 'Team1/Team2 - no', + '141' => 'Draw/Team1', + '142' => 'Draw/Team1 - no', + '143' => 'Draw/Draw', + '144' => 'Draw/Draw - no', + '145' => 'Draw/Team2', + '146' => 'Draw/Team2 - no', + '147' => 'Team2/Team1', + '148' => 'Team2/Team1 - no', + '149' => 'Team2/Draw', + '150' => 'Team2/Draw - no', + '151' => 'Team2/Team2', + '152' => 'Team2/Team2 - no', + '153' => 'Exact (%s)', + '154' => 'Exact (%s) - no', + '155' => 'Exact (%s) for Team1', + '156' => 'Exact (%s) for Team1 - no', + '157' => 'Exact (%s) for Team2', + '158' => 'Exact (%s) for Team2 - no', + '159' => 'More goals in the 1st half', + '160' => 'Equal goals in halves', + '161' => 'More goals in the 2nd half', + '162' => '1 half most goals (draw no bet)', + '163' => '2 half most goals (draw no bet)', + '164' => 'Team1 - 1st goal', + '165' => 'No goal', + '166' => 'Team2 - 1st goal', + '167' => 'Team1 - 1st goal (draw no bet)', + '168' => 'Team2 - 1st goal (draw no bet)', + '169' => 'Team1 - Last goal', + '170' => 'No goal', + '171' => 'Team2 - Last goal', + '172' => 'Team1 - Last goal (draw no bet)', + '173' => 'Team2 - Last goal (draw no bet)', + '174' => 'Total Over(%s) - Aces', + '175' => 'Total Under(%s) - Aces', + '176' => 'Total Over(%s) for Team1 - Aces', + '177' => 'Total Under(%s) for Team1 - Aces', + '178' => 'Total Over(%s) for Team2 - Aces', + '179' => 'Total Under(%s) for Team2 - Aces', + '180' => 'Total Over(%s) - Double Faults', + '181' => 'Total Under(%s) - Double Faults', + '182' => 'Total Over(%s) for Team1 - Double Faults', + '183' => 'Total Under(%s) for Team1 - Double Faults', + '184' => 'Total Over(%s) for Team2 - Double Faults', + '185' => 'Total Under(%s) for Team2 - Double Faults', + '186' => 'Total Over(%s) for Team1 - 1st Serve', + '187' => 'Total Under(%s) for Team1 - 1st Serve', + '188' => 'Total Over(%s) for Team2 - 1st Serve', + '189' => 'Total Under(%s) for Team2 - 1st Serve', + '190' => 'Asian Handicap1(%s) - Aces', + '191' => 'Asian Handicap2(%s) - Aces', + '192' => 'Asian Handicap1(%s) - Double Faults', + '193' => 'Asian Handicap2(%s) - Double Faults', + '194' => 'Asian Handicap1(%s) - 1st Serve', + '195' => 'Asian Handicap2(%s) - 1st Serve', + '196' => 'Player1 - 1st Ace', + '197' => 'Player2 - 1st Ace', + '198' => 'Player1 - 1st Double Fault', + '199' => 'Player2 - 1st Double Fault', + '200' => 'Player1 - 1st Break', + '201' => 'Player2 - 1st Break', + '202' => 'Player1 - 1st Break', + '203' => 'No break - 1st Break', + '204' => 'Player2 - 1st Break', + '205' => '6-0 Set - yes', + '206' => '6-0 Set - no', + '207' => 'Win From Behind - yes', + '208' => 'Win From Behind - no', + '209' => 'Exact (%s) - Sets', + '210' => 'Exact (%s) - Sets - no', + '211' => 'Team1 - 1st corner', + '212' => 'No corners', + '213' => 'Team2 - 1st corner', + '214' => 'Team1 - Last corner', + '215' => 'No corners', + '216' => 'Team2 - Last corner', + '217' => 'Team1 - 1st Yellow Card', + '218' => 'No Yellow Card', + '219' => 'Team2 - 1st Yellow Card', + '220' => 'Team1 - Last Yellow Card', + '221' => 'No Yellow Card', + '222' => 'Team2 - Last Yellow Card', + '223' => 'Team1 - 1st offside', + '224' => 'No offsides', + '225' => 'Team2 - 1st offside', + '226' => 'Team1 - Last offside', + '227' => 'No offsides', + '228' => 'Team2 - Last offside', + '229' => '1st substitution - 1 half', + '230' => '1st substitution - intermission', + '231' => '1st substitution - 2 half', + '232' => '1st goal - 1 half', + '233' => 'No goal', + '234' => '1st goal - 2 half', + '235' => 'Team1 - 1st subs', + '236' => 'Team2 - 1st subs', + '237' => 'Team1 - Last subs', + '238' => 'Team2 - Last subs', + '239' => '1 - Shots on goal', + '240' => 'X - Shots on goal', + '241' => '2 - Shots on goal', + '242' => '1X - Shots on goal', + '243' => '12 - Shots on goal', + '244' => 'X2 - Shots on goal', + '245' => 'Asian Handicap1(%s) - Shots on goal', + '246' => 'Asian Handicap2(%s) - Shots on goal', + '247' => 'Total Over(%s) - Shots on goal', + '248' => 'Total Under(%s) - Shots on goal', + '249' => 'Total Over(%s) for Team1 - Shots on goal', + '250' => 'Total Under(%s) for Team1 - Shots on goal', + '251' => 'Total Over(%s) for Team2 - Shots on goal', + '252' => 'Total Under(%s) for Team2 - Shots on goal', + '253' => 'Odd - Shots on goal', + '254' => 'Even - Shots on goal', + '255' => '1 - Fouls', + '256' => 'X - Fouls', + '257' => '2 - Fouls', + '258' => '1X - Fouls', + '259' => '12 - Fouls', + '260' => 'X2 - Fouls', + '261' => 'Asian Handicap1(%s) - Fouls', + '262' => 'Asian Handicap2(%s) - Fouls', + '263' => 'Total Over(%s) - Fouls', + '264' => 'Total Under(%s) - Fouls', + '265' => 'Total Over(%s) for Team1 - Fouls', + '266' => 'Total Under(%s) for Team1 - Fouls', + '267' => 'Total Over(%s) for Team2 - Fouls', + '268' => 'Total Under(%s) for Team2 - Fouls', + '269' => 'Odd - Fouls', + '270' => 'Even - Fouls', + '271' => '1 - Offsides', + '272' => 'X - Offsides', + '273' => '2 - Offsides', + '274' => '1X - Offsides', + '275' => '12 - Offsides', + '276' => 'X2 - Offsides', + '277' => 'Asian Handicap1(%s) - Offsides', + '278' => 'Asian Handicap2(%s) - Offsides', + '279' => 'Total Over(%s) - Offsides', + '280' => 'Total Under(%s) - Offsides', + '281' => 'Total Over(%s) for Team1 - Offsides', + '282' => 'Total Under(%s) for Team1 - Offsides', + '283' => 'Total Over(%s) for Team2 - Offsides', + '284' => 'Total Under(%s) for Team2 - Offsides', + '285' => 'Odd - Offsides', + '286' => 'Even - Offsides', + '287' => 'Team1 to Win From Behind - yes', + '288' => 'Team1 to Win From Behind - no', + '289' => 'Team2 to Win From Behind - yes', + '290' => 'Team2 to Win From Behind - no', + '291' => 'Both To Score and W1 - yes', + '292' => 'Both To Score and W1 - no', + '293' => 'Both To Score and W2 - yes', + '294' => 'Both To Score and W2 - no', + '295' => 'Both To Score and Draw - yes', + '296' => 'Both To Score and Draw - no', + '297' => 'Exact (%s) - Added time', + '298' => 'Exact (%s) - Added time - no', + '299' => 'Total Over(%s) - Added time', + '300' => 'Total Under(%s) - Added time', + '301' => 'Home No Bet - W2', + '302' => 'Home No Bet - Draw', + '303' => 'Home No Bet - No Draw', + '304' => 'Away No Bet - W1', + '305' => 'Away No Bet - Draw', + '306' => 'Away No Bet - No Draw', + '307' => 'Total Over(%s) - Subs', + '308' => 'Total Under(%s) - Subs', + '309' => 'Total Over(%s) for Team1 - Subs', + '310' => 'Total Under(%s) for Team1 - Subs', + '311' => 'Total Over(%s) for Team2 - Subs', + '312' => 'Total Under(%s) for Team1 - Subs', + '313' => '1 - Ball possession', + '314' => 'X - Ball possession', + '315' => '2 - Ball possession', + '316' => '1X - Ball possession', + '317' => '12 - Ball possession', + '318' => 'X2 - Ball possession', + '319' => 'Asian Handicap1(%s) - Ball possession', + '320' => 'Asian Handicap2(%s) - Ball possession', + '321' => 'Total Over(%s) for Team1 - Ball possession', + '322' => 'Total Under(%s) for Team1 - Ball possession', + '323' => 'Total Over(%s) for Team2 - Ball possession', + '324' => 'Total Under(%s) for Team2 - Ball possession', + '325' => 'Team1 - 1st corner', + '326' => 'Team2 - 1st corner', + '327' => 'Team1 - Last corner', + '328' => 'Team2 - Last corner', + '329' => 'Team1 - 1st Yellow Card', + '330' => 'Team2 - 1st Yellow Card', + '331' => 'Team1 - Last Yellow Card', + '332' => 'Team2 - Last Yellow Card', + '333' => 'Team1 - 1st offside', + '334' => 'Team2 - 1st offside', + '335' => 'Team1 - Last offside', + '336' => 'Team2 - Last offside', + '337' => 'Home No Bet - No W2', + '338' => 'Away No Bet - No W1', + '339' => '1X and Total Over 2.5 - yes', + '340' => '1X and Total Over 2.5 - no', + '341' => '1X and Total Under 2.5 - yes', + '342' => '1X and Total Under 2.5 - no', + '343' => 'X2 and Total Over 2.5 - yes', + '344' => 'X2 and Total Over 2.5 - no', + '345' => 'X2 and Total Under 2.5 - yes', + '346' => 'X2 and Total Under 2.5 - no', + '348' => 'Overtime - yes', + '351' => 'Overtime - no', + '354' => 'Score Draw - yes', + '357' => 'Score Draw - no', + '360' => 'Race to 2 - Team1', + '363' => 'Race to 2 - Team2', + '366' => 'Race to 2 - Team1', + '369' => 'Race to 2 - neither', + '372' => 'Race to 2 - Team2', + '375' => 'Race to 3 - Team1', + '378' => 'Race to 3 - Team2', + '381' => 'Race to 3 - Team1', + '384' => 'Race to 3 - neither', + '387' => 'Race to 3 - Team2', + '390' => 'Race to 4 - Team1', + '393' => 'Race to 4 - Team2', + '396' => 'Race to 4 - Team1', + '399' => 'Race to 4 - neither', + '402' => 'Race to 4 - Team2', + '405' => 'Race to 5 - Team1', + '408' => 'Race to 5 - Team2', + '411' => 'Race to 5 - Team1', + '414' => 'Race to 5 - neither', + '417' => 'Race to 5 - Team2', + '420' => 'Race to 10 - Team1', + '423' => 'Race to 10 - Team2', + '426' => 'Race to 10 - Team1', + '429' => 'Race to 10 - neither', + '432' => 'Race to 10 - Team2', + '435' => 'Race to 15 - Team1', + '438' => 'Race to 15 - Team2', + '441' => 'Race to 15 - Team1', + '444' => 'Race to 15 - neither', + '447' => 'Race to 15 - Team2', + '450' => 'Race to 20 - Team1', + '453' => 'Race to 20 - Team2', + '456' => 'Race to 20 - Team1', + '459' => 'Race to 20 - neither', + '462' => 'Race to 20 - Team2', + '467' => 'Team1 - Next goal (draw no bet)', + '470' => 'Team2 - Next goal (draw no bet)', + '473' => 'Team1 - Next goal', + '476' => 'No next goal', + '479' => 'Team2 - Next goal', + '481' => '1st - yes', + '484' => '1st - no', + '487' => '1st-3rd - yes', + '490' => '1st-3rd - no', + '493' => 'W1', + '496' => 'W2', + '499' => 'TO(%s) - Hits', + '502' => 'TU(%s) - Hits', + '505' => 'TO(%s) for Team1 - Hits', + '508' => 'TU(%s) for Team1 - Hits', + '511' => 'TO(%s) for Team2 - Hits', + '514' => 'TU(%s) for Team2 - Hits', + '517' => 'TO(%s) - Errors', + '520' => 'TU(%s) - Errors', + '523' => 'TO(%s) - Hits+Errors+Runs', + '526' => 'TU(%s) - Hits+Errors+Runs', + '529' => 'AH1(%s) - Hits', + '532' => 'AH2(%s) - Hits', + '535' => '1 - Hits', + '538' => 'X - Hits', + '541' => '2 - Hits', + '546' => 'Team1 Win - Kills', + '549' => 'Team2 Win - Kills', + '552' => 'Asian Handicap1(%s) - Kills', + '555' => 'Asian Handicap2(%s) - Kills', + '558' => 'Total Over(%s) - Kills', + '561' => 'Total Under(%s) - Kills', + '564' => 'Total Over(%s) for Team1 - Kills', + '567' => 'Total Under(%s) for Team1 - Kills', + '570' => 'Total Over(%s) for Team2 - Kills', + '573' => 'Total Under(%s) for Team2 - Kills', + '574' => 'W1 - 1st blood', + '575' => 'W2 - 1st blood', + '576' => 'W1 - 1st tower', + '577' => 'W2 - 1st tower', + '578' => 'W1 - 1st dragon', + '579' => 'W2 - 1st dragon', + '580' => 'W1 - 1st baron', + '581' => 'W2 - 1st baron', + '582' => 'W1 - 1st inhibitor', + '583' => 'W2 - 1st inhibitor', + '584' => 'W1 - 1st roshan', + '585' => 'W2 - 1st roshan', + '586' => 'Win pistol rounds - Yes', + '587' => 'Win pistol rounds - No', + '588' => 'TO(%s) - Duration', + '589' => 'TU(%s) - Duration', + '590' => 'TO(%s) - Barons', + '591' => 'TU(%s) - Barons', + '592' => 'TO(%s) - Inhibitors', + '593' => 'TU(%s) - Inhibitors', + '594' => 'TO(%s) - Towers', + '595' => 'TU(%s) - Towers', + '596' => 'TO(%s) - Dragons', + '597' => 'TU(%s) - Dragons', + '598' => 'TO(%s) - Roshans', + '599' => 'TU(%s) - Roshans', + '600' => 'TO(%s) for Team1 - Sets', + '601' => 'TO(%s) for Team1 - Sets', + '602' => 'TO(%s) for Team2 - Sets', + '603' => 'TO(%s) for Team2 - Sets', + '604' => 'W1 - Longest TD', + '605' => 'W2 - Longest TD', + '606' => 'W1 - Longest FG', + '607' => 'W2 - Longest FG', + '608' => 'Touchdown - Yes', + '609' => 'Touchdown - No', + '610' => 'Safety - Yes', + '611' => 'Safety - No', + '612' => 'First score TD - Yes', + '613' => 'First score TD - No', + '614' => 'Both teams 10 pts - Yes', + '615' => 'Both teams 10 pts - No', + '616' => 'Both teams 15 pts - Yes', + '617' => 'Both teams 15 pts - No', + '618' => 'Both teams 20 pts - Yes', + '619' => 'Both teams 20 pts - No', + '620' => 'Both teams 25 pts - Yes', + '621' => 'Both teams 25 pts - No', + '622' => 'Both teams 30 pts - Yes', + '623' => 'Both teams 30 pts - No', + '624' => 'Both teams 35 pts - Yes', + '625' => 'Both teams 35 pts - No', + '626' => 'Both teams 40 pts - Yes', + '627' => 'Both teams 40 pts - No', + '628' => 'Both teams 45 pts - Yes', + '629' => 'Both teams 45 pts - No', + '630' => 'Both teams 50 pts - Yes', + '631' => 'Both teams 50 pts - No', + '632' => 'Highest Scoring Quarter - 1st', + '633' => 'Highest Scoring Quarter - 2nd', + '634' => 'Highest Scoring Quarter - 3rd', + '635' => 'Highest Scoring Quarter - 4th', + '636' => 'Highest Scoring Quarter - Tie', + '637' => 'TO(%s) - Field Goals', + '638' => 'TU(%s) - Field Goals', + '639' => 'TO(%s) - Touchdowns', + '640' => 'TU(%s) - Touchdowns', + '641' => 'TO(%s) - Longest TD, distance', + '642' => 'TU(%s) - Longest TD, distance', + '643' => 'TO(%s) - Longest FG, distance', + '644' => 'TU(%s) - Longest FG, distance', + '645' => 'TO(%s) for Team1 - Field Goals', + '646' => 'TU(%s) for Team1 - Field Goals', + '647' => 'TO(%s) for Team2 - Field Goals', + '648' => 'TU(%s) for Team2 - Field Goals', + '649' => 'TO(%s) for Team1 - Touchdowns', + '650' => 'TU(%s) for Team1 - Touchdowns', + '651' => 'TO(%s) for Team2 - Touchdowns', + '652' => 'TU(%s) for Team2 - Touchdowns', + '653' => 'AH1(%s) - Maps', + '654' => 'AH2(%s) - Maps', + '655' => 'TO(%s) - Maps', + '656' => 'TU(%s) - Maps', + '657' => 'TO(%s) for Team1 - Maps', + '658' => 'TU(%s) for Team1 - Maps', + '659' => 'TO(%s) for Team2 - Maps', + '660' => 'TU(%s) for Team2 - Maps', + '661' => 'Maps (%s)', + '662' => 'Maps (%s) - not', + '663' => 'Exact (%s) - Maps', + '664' => 'Exact (%s) - Maps - no', + '665' => 'Win both pistol rounds - Yes', + '666' => 'Win both pistol rounds - No', + '667' => 'Team1 to win both pistol rounds - Yes', + '668' => 'Team1 to win both pistol rounds - No', + '669' => 'Team2 to win both pistol rounds - Yes', + '670' => 'Team1 to win both pistol rounds - No', + '671' => 'Team1 to win at least 1 set - Yes', + '672' => 'Team1 to win at least 1 set - No', + '673' => 'Team2 to win at least 1 set - Yes', + '674' => 'Team1 to win at least 1 set - No', + '675' => 'Team1 to win at least 1 map - Yes', + '676' => 'Team1 to win at least 1 map - No', + '677' => 'Team2 to win at least 1 map - Yes', + '678' => 'Team1 to win at least 1 map - No', + '679' => 'Both teams kill a dragon - Yes', + '680' => 'Both teams kill a dragon - No', + '681' => 'Both teams kill a baron - Yes', + '682' => 'Both teams kill a baron - No', + '683' => 'W1 - 1st Barracks', + '684' => 'W2 - 1st Barracks', + '685' => 'W1 - 1st Double Kill', + '686' => 'W2 - 1st Double Kill', + '687' => 'TO(%s) - Barracks', + '688' => 'TU(%s) - Barracks', + '689' => 'TO(%s) - Double Kills', + '690' => 'TU(%s) - Double Kills', + '691' => 'AH1(%s) - Barons', + '692' => 'AH2(%s) - Barons', + '693' => 'AH1(%s) - Dragons', + '694' => 'AH2(%s) - Dragons', + '695' => 'AH1(%s) - Towers/Turrets', + '696' => 'AH2(%s) - Towers/Turrets', + '697' => '1 - 3-points', + '698' => 'X - 3-points', + '699' => '2 - 3-points', + '700' => '1 - Rebounds', + '701' => 'X - Rebounds', + '702' => '2 - Rebounds', + '703' => '1 - Assists', + '704' => 'X - Assists', + '705' => '2 - Assists', + '706' => '1X - 3-points', + '707' => 'X2 - 3-points', + '708' => '12 - 3-points', + '709' => '1X - Rebounds', + '710' => 'X2 - Rebounds', + '711' => '12 - Rebounds', + '712' => '1X - Assists', + '713' => 'X2 - Assists', + '714' => '12 - Assists', + '715' => 'AH1(%s) - 3-points', + '716' => 'AH2(%s) - 3-points', + '717' => 'AH1(%s) - Rebounds', + '718' => 'AH2(%s) - Rebounds', + '719' => 'AH1(%s) - Assists', + '720' => 'AH2(%s) - Assists', + '721' => 'TO(%s) - 3-points', + '722' => 'TU(%s) - 3-points', + '723' => 'TO(%s) - Rebounds', + '724' => 'TU(%s) - Rebounds', + '725' => 'TO(%s) - Assists', + '726' => 'TU(%s) - Assists', + '727' => 'TO(%s) for Team1 - 3-points', + '728' => 'TU(%s) for Team1 - 3-points', + '729' => 'TO(%s) for Team1 - Rebounds', + '730' => 'TU(%s) for Team1 - Rebounds', + '731' => 'TO(%s) for Team1 - Assists', + '732' => 'TU(%s) for Team1 - Assists', + '733' => 'TO(%s) for Team2 - 3-points', + '734' => 'TU(%s) for Team2 - 3-points', + '735' => 'TO(%s) for Team2 - Rebounds', + '736' => 'TU(%s) for Team2 - Rebounds', + '737' => 'TO(%s) for Team2 - Assists', + '738' => 'TU(%s) for Team2 - Assists', + '739' => 'W1 - 180s', + '740' => 'W2 - 180s', + '741' => 'AH1(%s) - 180s', + '742' => 'AH2(%s) - 180s', + '743' => 'TO(%s) - 180s', + '744' => 'TU(%s) - 180s', + '745' => '1 - Cards', + '746' => 'X - Cards', + '747' => '2 - Cards', + '748' => '1 - Booking points', + '749' => 'X - Booking points', + '750' => '2 - Booking points', + '751' => '1X - Cards', + '752' => 'X2 - Cards', + '753' => '12 - Cards', + '754' => '1X - Booking points', + '755' => 'X2 - Booking points', + '756' => '12 - Booking points', + '757' => 'AH1(%s) - Cards', + '758' => 'AH2(%s) - Cards', + '759' => 'AH1(%s) - Booking points', + '760' => 'AH2(%s) - Booking points', + '761' => 'TO(%s) - Cards', + '762' => 'TU(%s) - Cards', + '763' => 'TO(%s) - Booking points', + '764' => 'TU(%s) - Booking points', + '765' => 'TO(%s) for Team1 - Cards', + '766' => 'TU(%s) for Team1 - Cards', + '767' => 'TO(%s) for Team1 - Booking points', + '768' => 'TU(%s) for Team1 - Booking points', + '769' => 'TO(%s) for Team2 - Cards', + '770' => 'TU(%s) for Team2 - Cards', + '771' => 'TO(%s) for Team2 - Booking points', + '772' => 'TU(%s) for Team2 - Booking points', + '773' => 'Odd - Cards', + '774' => 'Even - Cards', + '775' => 'Odd - Booking points', + '776' => 'Even - Booking points' } + + end + + def periods + @periods ||= + { + '1': 'match (pairs)', + '2': 'with OT and SO', + '3': 'with OT', + '4': 'regular time', #match odds + '5': '1st', + '6': '2nd', + '7': '3rd', + '8': '4th', + '9': '5th', + '10': '1st half', #first + '13': '2nd half', + '14': '1 set, 01 game', + '15': '1 set, 02 game', + '16': '1 set, 03 game', + '17': '1 set, 04 game', + '18': '1 set, 05 game', + '19': '1 set, 06 game', + '20': '1 set, 07 game', + '21': '1 set, 08 game', + '22': '1 set, 09 game', + '23': '1 set, 10 game', + '24': '1 set, 11 game', + '25': '1 set, 12 game', + '26': '1 set, 13 game', + '44': '2 set, 01 game', + '45': '2 set, 02 game', + '46': '2 set, 03 game', + '47': '2 set, 04 game', + '48': '2 set, 05 game', + '49': '2 set, 06 game', + '50': '2 set, 07 game', + '51': '2 set, 08 game', + '52': '2 set, 09 game', + '53': '2 set, 10 game', + '54': '2 set, 11 game', + '55': '2 set, 12 game', + '56': '2 set, 13 game', + '57': '3 set, 01 game', + '58': '3 set, 02 game', + '59': '3 set, 03 game', + '60': '3 set, 04 game', + '61': '3 set, 05 game', + '62': '3 set, 06 game', + '63': '3 set, 07 game', + '64': '3 set, 08 game', + '65': '3 set, 09 game', + '66': '3 set, 10 game', + '67': '3 set, 11 game', + '68': '3 set, 12 game', + '71': '3 set, 13 game', + '76': '6th', + '78': '7th', + '86': 'regular time', + '92': 'regular time', + '93': '1st half', + '95': 'to qualify', + '96': '8th', + '97': '9th', + '113': '4 set, 01 game', + '114': '4 set, 02 game', + '115': '4 set, 03 game', + '116': '4 set, 04 game', + '117': '4 set, 05 game', + '118': '4 set, 06 game', + '119': '4 set, 07 game', + '120': '4 set, 08 game', + '121': '4 set, 09 game', + '122': '4 set, 10 game', + '123': '4 set, 11 game', + '124': '4 set, 12 game', + '125': '5 set, 01 game', + '126': '5 set, 02 game', + '127': '5 set, 03 game', + '128': '5 set, 04 game', + '129': '5 set, 05 game', + '130': '5 set, 06 game', + '131': '5 set, 07 game', + '132': '5 set, 08 game', + '133': '5 set, 09 game', + '134': '5 set, 10 game', + '156': '4 set, 13 game', + '159': '5 set, 11 game', + '161': '5 set, 12 game', + '169': '5 set, 13 game', + '223': 'regular time', + '243': '1st half', + '245': '2nd half', + '246': '2nd half', + '317': '1st half', + '318': '2nd half', + '4091': 'regular time', #match odds + '4094': '1st half' + } + end + end +end diff --git a/portal/app/lib/integrations/betfair/account_manager.rb b/portal/app/lib/integrations/betfair/account_manager.rb new file mode 100644 index 0000000..4923af2 --- /dev/null +++ b/portal/app/lib/integrations/betfair/account_manager.rb @@ -0,0 +1,16 @@ +module Integrations + module Betfair + class AccountManager < Base + def refresh_account_balance + # go check the account balance and update the ExchangeAccount for this account + res = self.class.post("#{API_ACCOUNT_ENDPOINT}/getAccountFunds/", { headers: @connection.api_headers, data: {} }) + if res['availableToBetBalance'] + acc_bal = res['availableToBetBalance'] + @account.update(account_balance: acc_bal, account_balance_last_checked: Time.now) + end + @account.account_balance + end + end + end +end + diff --git a/portal/app/lib/integrations/betfair/base.rb b/portal/app/lib/integrations/betfair/base.rb new file mode 100644 index 0000000..7a1cc5e --- /dev/null +++ b/portal/app/lib/integrations/betfair/base.rb @@ -0,0 +1,40 @@ +module Integrations + module Betfair + class Base + include HTTParty + + API_BETTING_ENDPOINT = 'https://api.betfair.com/exchange/betting/rest/v1.0'.freeze + API_ACCOUNT_ENDPOINT = 'https://api.betfair.com/exchange/account/rest/v1.0'.freeze + + def initialize(account_friendly_id) + @account = ExchangeAccount.find_by_id(account_friendly_id) + unless @account + puts "No exchange account for '#{account_friendly_id}'. Stopping" + return + end + self.class.pem @account.ssl_pem + @connection = Integrations::Betfair::Connection.new(@account) + end + + def minimum_stake + 1 + end + + def list_events(filter) + body = { filter: filter } + self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json }) + end + + def debug_list_market_types + body = { filter: {} } + marketTypes = self.class.post("#{API_BETTING_ENDPOINT}/listMarketTypes/", { headers: @connection.api_headers, body: body.to_json }) + results = [] + marketTypes.each do |market| + results << market['marketType'] + end + File.write(Rails.root.join('samples/bf_market_types.json'), results.join("\n")) + end + end + end +end + diff --git a/portal/app/lib/integrations/betfair/bet_manager.rb b/portal/app/lib/integrations/betfair/bet_manager.rb new file mode 100644 index 0000000..8656190 --- /dev/null +++ b/portal/app/lib/integrations/betfair/bet_manager.rb @@ -0,0 +1,173 @@ +module Integrations + module Betfair + class BetManager < Base + def check_qualified_bet_outcome(bets) + bets_lookup = {} + market_ids = [] + bets_by_bet_ids = [] + reconciliation_limit = 20 + bet_count = 0 + bets.each do |bet| + bet_count += 1 + next unless bet.exchange_market_details['market_id'] && bet.exchange_market_details['selection_id'] + + market_ids << bet.exchange_market_details['market_id'] + bets_lookup[bet.exchange_market_details['market_id'].to_s] = { selection_id: bet.exchange_market_details['selection_id'], bet_id: bet.id } + bets_by_bet_ids << bet unless bet.exchange_bet_id.blank? + next unless (reconciliation_limit == market_ids.size) || (bet_count == bets.count) + + reconcile_bets_by_markets(market_ids, bets_lookup) unless market_ids.empty? + market_ids = [] + bets_lookup = {} + end + reconcile_bets_by_bet_ids(bets_by_bet_ids) unless bets_by_bet_ids.empty? + end + + def reconcile_bets_by_markets(market_ids, bets_lookup) + outcomes = { LOSER: [], WINNER: [] } + body = { marketIds: market_ids } + markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketBook/", { headers: @connection.api_headers, body: body.to_json }) + markets.each do |market| + next unless market['status'] == 'CLOSED' + + bet = bets_lookup[market['marketId']] + next unless bet + + runners = market['runners'] + runners.each do |runner| + next unless runner['selectionId'] == bet[:selection_id] + + x_status = runner['status'].to_sym + if outcomes[x_status] + outcomes[x_status] << bet[:bet_id] + end + break + end + end + ActiveRecord::Base.connection.execute("update bets set outcome = 'lost', outcome_value=stake where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:LOSER]])}") if outcomes[:LOSER].size.positive? + ActiveRecord::Base.connection.execute("update bets set outcome = 'won', outcome_value=expected_value where #{ActiveRecord::Base.sanitize_sql(['id in (?)', outcomes[:WINNER]])}") if outcomes[:WINNER].size.positive? + end + + def reconcile_bets_by_bet_ids(bets) + return unless bets.count.positive? + + results = {} + open_bet_ids = bets.pluck(:exchange_bet_id) + %w[SETTLED VOIDED CANCELLED].each do |status| + body = { betStatus: status, betIds: open_bet_ids } + r = self.class.post("#{API_BETTING_ENDPOINT}/listClearedOrders/", { headers: @connection.api_headers, body: body.to_json }) + orders = r['clearedOrders'] + if status == 'SETTLED' + # the status updates are by betOutcome + orders.each do |o| + results[o['betOutcome']] ||= [] + results[o['betOutcome']] << o['betId'] + end + else + results[status] ||= [] + bet_ids = orders.map { |o| o['betId'] } + results[status] = bet_ids + end + end + results.keys.each do |k| + next if results[k].blank? + + Bet.unscoped.open.where(exchange_bet_id: results[k]).update(outcome: k.downcase) + end + end + + def place_bet(bet, stake) + body = { marketId: bet.exchange_market_details['market_id'], customerRef: bet.tip_provider_bet_id } + body[:instructions] = + [{ orderType: 'LIMIT', side: 'BACK', selectionId: bet.exchange_market_details['selection_id'].to_s, + limitOrder: { timeInForce: 'FILL_OR_KILL', size: stake.to_s, price: bet.tip_provider_odds.to_s, persistenceType: 'LAPSE' } }] + r = self.class.post("#{API_BETTING_ENDPOINT}/placeOrders/", { headers: @connection.api_headers, body: body.to_json }) + + success = r['status'] == 'SUCCESS' + if success + bet_id = r['instructionReports'][0]['betId'] + return bet_id + end + error_code = r['errorCode'] + raise "[Place bet] Placing bet failed: #{error_code}" + end + + def bet_event(bet, update_bet = false) + # return the event for this bet. + # easy if we have the event_id, else we have to search. + event_id = [] + event_id << bet.exchange_event_id unless bet.exchange_event_id.blank? + events = list_events({ eventIds: event_id, textQuery: bet.exchange_event_name }) + e_id = events[0]['event']['id'] if events.length.positive? + if e_id + bet.update(exchange_event_id: e_id) if update_bet + return e_id + end + raise '[bet_event] Error getting event id' + end + + def bet_odds(bet) + event_market_selection_hash = event_market_selection(bet) + raise '[bet odds] - market not available' unless event_market_selection_hash['market_id'] + raise '[bet odds] - selection not available' unless event_market_selection_hash['selection_id'] + + body = { marketId: event_market_selection_hash['market_id'], selectionId: event_market_selection_hash['selection_id'] } + body[:priceProjection] = { priceData: ['EX_ALL_OFFERS'], virtualise: true } + r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json }) + runners = r[0]['runners'] + raise '[Bet odds] - cannot identify prices' unless runners + + rs = runners.first + raise '[Bet odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack'] + + prices = [] + stakes = {} + rs['ex']['availableToBack'].each do |ex| + prices << ex['price'] + stakes[(ex['price']).to_s] = ex['size'] + end + { prices: prices, stakes: stakes, liquidity: r[0]["totalAvailable"] } + end + + def event_market_selection(bet) + if bet.exchange_event_id.blank? + bet_event(bet, true) + raise '[No event id]' if bet.exchange_event_id.blank? + end + body = { maxResults: 500, filter: { eventIds: [bet.exchange_event_id] }, marketProjection: %w[MARKET_DESCRIPTION RUNNER_DESCRIPTION RUNNER_METADATA] } + markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json }) + m_details = bet.exchange_market_details + returned_markets = [] + returned_selections = [] + markets.each do |market| + returned_markets << market['marketName'] + next unless m_details['market'].casecmp(market['marketName']).zero? + + m_details['market_id'] = market['marketId'] + fuzzy_match_runners = market['marketName'] == 'Match Odds' + runners = market['runners'] + m_selection = m_details['selection'] + runners.each do |runner| + returned_selections << runner['runnerName'] + runner_name_matched = runner['runnerName'].casecmp(m_selection).zero? + if !runner_name_matched && fuzzy_match_runners + runner_name_matched = (runner['runnerName'].downcase.split & m_selection.downcase.split).length.positive? + end + next unless runner_name_matched + + m_details['selection_id'] = runner['selectionId'] + m_details['selection'] = runner['runnerName'] + break + end + end + + m_details['returned_markets'] = returned_markets unless m_details.key?('market_id') + m_details['returned_selections'] = returned_selections unless m_details.key?('selection_id') + + bet.update(exchange_market_details: m_details) + m_details + end + end + end +end + diff --git a/portal/app/lib/integrations/betfair/connection.rb b/portal/app/lib/integrations/betfair/connection.rb new file mode 100644 index 0000000..dee3698 --- /dev/null +++ b/portal/app/lib/integrations/betfair/connection.rb @@ -0,0 +1,39 @@ +module Integrations + module Betfair + class Connection + + include HTTParty + def initialize(account) + @account = account + self.class.pem @account.ssl_pem + end + + def api_headers + { 'X-Application' => @account.apikey, 'X-Authentication' => session_token, 'content-type' => 'application/json', 'accept' => 'application/json' } + end + + private + + def session_token + # if + puts 'Checking if session still fresh' + if @account.last_session_token_saved_at && @account.last_session_token_saved_at > 10.hours.ago + puts 'Returning cached session token' + return @account.last_session_token + end + + puts 'Cache is stale or non-existent - getting fresh session key' + url = 'https://identitysso-cert.betfair.com/api/certlogin' + r = self.class.post(url, headers: { 'X-Application' => @account.apikey }, body: { username: @account.login_uid, password: @account.login_pass }) + resp = JSON.parse(r) + if resp['loginStatus'] == 'SUCCESS' + @account.update(last_session_token: resp['sessionToken'], last_session_token_saved_at: Time.now) + return resp['sessionToken'] + end + + raise '[Betfair Session token] Cannot get session to Betfair' + end + end + end +end + diff --git a/portal/app/lib/integrations/betfair/opportunity_hunter.rb b/portal/app/lib/integrations/betfair/opportunity_hunter.rb new file mode 100644 index 0000000..27012f7 --- /dev/null +++ b/portal/app/lib/integrations/betfair/opportunity_hunter.rb @@ -0,0 +1,77 @@ +module Integrations + module Betfair + class OpportunityHunter < Base + def events_in_timeframe(from:, to:) + raise "Timeframe not set " unless from.present? && to.present? + + timeframe = { from: from.iso8601, to: to.iso8601 } + filter = { marketStartTime: timeframe } + body = { filter: filter } + + r = self.class.post("#{API_BETTING_ENDPOINT}/listEvents/", { headers: @connection.api_headers, body: body.to_json }) + events = [] + r.each do |e| + ev = e['event'] + events << BetfairEvent.new(event_id: ev['id'], event_name: ev['name'], event_start: DateTime.parse(ev['openDate'])) + end + import_result = BetfairEvent.import(events, on_duplicate_key_ignore: true) + puts "#{import_result.ids.size} events added" + end + + def event_markets_and_selections + batches = [] + batch = 0 + limit = 10 + BetfairEvent.open.order(created_at: :desc).pluck(:event_id).each do |eid| + batches[batch] ||= [] + if batches[batch].size < limit + batches[batch] << eid + else + batch += 1 + end + end + batches.each { |b| batch_event_runners b } + end + + def runner_odds(runner) + body = { marketId: runner.market_id, selectionId: runner.selection_id } + body[:priceProjection] = { priceData: ['EX_BEST_OFFERS'], virtualise: true } + r = self.class.post("#{API_BETTING_ENDPOINT}/listRunnerBook/", { headers: @connection.api_headers, body: body.to_json }) + runners = r[0]['runners'] + raise '[Odds] - cannot identify prices' unless runners + + rs = runners.first + raise '[Odds] - cannot identify prices' unless rs && rs['ex'] && rs['ex']['availableToBack'] + + imports = [] + rs['ex']['availableToBack'].each do |ex| + imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'back') + end + rs['ex']['availableToLay'].each do |ex| + imports << BetfairRunnerOdd.new(betfair_event_runner_id: runner.id, odds: ex['price'], total_matched: 0, total_available: ex['size'], bet_type: 'lay') + end + + BetfairRunnerOdd.import(imports, on_duplicate_key_ignore: true) + end + + private + + def batch_event_runners(batch) + body = { maxResults: 1000 - (10 * (batch.size - 1)), filter: { eventIds: batch }, marketProjection: ['EVENT', 'RUNNER_DESCRIPTION'] } + markets = self.class.post("#{API_BETTING_ENDPOINT}/listMarketCatalogue/", { headers: @connection.api_headers, body: body.to_json }) + import = [] + markets.each do |market| + market_fragment = { event_id: market['event']['id'], market_id: market['marketId'], market_name: market['marketName'] } + runners = market['runners'] + runners&.each do |runner| + rec = { selection_id: runner['selectionId'] || runner['runnerName'], selection_name: runner['runnerName'] }.merge(market_fragment) + import << BetfairEventRunner.new(rec) + end + end + import_result = BetfairEventRunner.import(import, on_duplicate_key_ignore: true) + puts "#{import_result.ids.size} runners added" + end + end + end +end + diff --git a/portal/app/lib/services/bet_outcome_service.rb b/portal/app/lib/services/bet_outcome_service.rb new file mode 100644 index 0000000..2457424 --- /dev/null +++ b/portal/app/lib/services/bet_outcome_service.rb @@ -0,0 +1,6 @@ +module Services + class BetOutcomeService + def bet_outcome(bet_id) + end + end +end diff --git a/portal/app/mailers/application_mailer.rb b/portal/app/mailers/application_mailer.rb new file mode 100644 index 0000000..a163f53 --- /dev/null +++ b/portal/app/mailers/application_mailer.rb @@ -0,0 +1,44 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'info@helpbuild.co' + helper MailerStyleHelper + layout 'mailer' + + def try_delivering(_options = {}) + yield + true + rescue EOFError, + IOError, + Errno::ECONNRESET, + Errno::ECONNABORTED, + Errno::EPIPE, + Errno::ETIMEDOUT, + Net::SMTPAuthenticationError, + Net::SMTPServerBusy, + Net::SMTPSyntaxError, + Net::SMTPUnknownError, + OpenSSL::SSL::SSLError => e + ExceptionHub.notify(e) + false + end + + private + + def deliver_mail(email, subject, reply_to = nil, to_name = '') + addresses = set_addresses_and_user(email: email, to_name: to_name) + reply_to = addresses[:from] if reply_to.nil? + try_delivering do + I18n.with_locale('en') do + mail(reply_to: reply_to, from: addresses[:from], to: addresses[:to], subject: "[Helpbuild] | #{subject}") + end + end + end + + def set_addresses_and_user(email: '', from_name: 'Helpbuild', to_name: '', from_email: nil) + addresses = {} + from_email ||= Rails.configuration.action_mailer[:default_options][:from] + addresses[:from] = %(#{from_name} <#{from_email}>) + addresses[:reply_to] = %(#{from_name} <#{from_email}>) + addresses[:to] = %("#{to_name}" <#{email}>) + addresses + end +end diff --git a/portal/app/mailers/project_mailer.rb b/portal/app/mailers/project_mailer.rb new file mode 100644 index 0000000..e3da75a --- /dev/null +++ b/portal/app/mailers/project_mailer.rb @@ -0,0 +1,76 @@ +class ProjectMailer < ApplicationMailer + layout 'project_mailer' + layout false, only: :send_project_invitation + + def helper_join_request(builders, helper) + subject = I18n.t('project_join_request', project: helper.project.title) + @project_helper = helper + @project = helper.project + builders.each do |b| + @user = b.user + deliver_mail(@user.email, subject, reply_to = nil) + end + end + + def notify_helper_for_join_request(helper) + return if helper.builder? + subject = I18n.t('project_join_request_response', project: helper.project.title) + + @user = helper.user + @project = helper.project + @status = helper.status.titlecase + + deliver_mail(@user.email, subject, reply_to = nil) + end + + def ask_request_to_helpers(helper, ask) + @user = helper.user + @project_helper = helper + @ask = ask + subject = I18n.t('ask_request_to_helpers', project: helper.project.title) + @project = helper.project + deliver_mail(helper.user.email, subject, reply_to = nil) + end + + def notify_builder_ask_status(ask, status, project_helper) + @builder = ask.project_helper.user + @ask = ask + @status = status + @project_helper = project_helper + @project = ask.project + subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status) + deliver_mail(@builder.email, subject, reply_to = nil) + end + + def notify_helper_ask_status(ask, status, project_helper) + # @builder = ask.project_helper.user + # @ask = ask + # @status = status + # @project_helper = project_helper + # @project = ask.project + # subject = I18n.t('ask_status_to_builder_subject', project: @project.title, ask: ask.title, status: status) + # deliver_mail(project_helper.user.email, subject, reply_to = nil) + end + + def notify_announcement(project_helper, announcement) + @user = project_helper.user + @announcement = announcement + @project = project_helper.project + subject = I18n.t('announcement_notification', project: announcement.project.title) + deliver_mail(project_helper.user.email, subject, reply_to = nil) + end + + def notify_monthly_announcement(project) + emails = project.project_builders.map {|pb| pb.user.email } + subject = I18n.t('monthly_announcement_check', project: project.title) + @project = project + + deliver_mail(emails.join(','), subject, reply_to = nil) + end + + def send_project_invitation(email, project) + @project = project + subject = I18n.t('project_invitation_subject', project: project.title, builder_name: project.user.first_name) + deliver_mail(email, subject, reply_to = nil) + end +end diff --git a/portal/app/mailers/user_mailer.rb b/portal/app/mailers/user_mailer.rb new file mode 100644 index 0000000..8768340 --- /dev/null +++ b/portal/app/mailers/user_mailer.rb @@ -0,0 +1,21 @@ +class UserMailer < ApplicationMailer + def welcome_email user + @user = user + subject = I18n.t('welcome_email_subject') + deliver_mail(user.email, subject, reply_to = nil, user.fullname) + end + + def notify_token_purchase(user, tokens, amount, balance) + @user = user + subject = I18n.t('token_purchase_confirmation_subject') + @body = I18n.t('token_purchase_confirmation_body', tokens: tokens, amount: amount, balance: balance.to_i) + deliver_mail(user.email, subject, reply_to = nil, user.fullname) + end + + def notify_hourly_unread_activities(user_id, activities_hash) + @user = User.find_by(id: user_id) + @activities_hash = activities_hash + + deliver_mail(@user.email, '[Helpbuild] Activity Digest Mail', reply_to = nil, @user.fullname) + end +end diff --git a/portal/app/models/account.rb b/portal/app/models/account.rb new file mode 100644 index 0000000..5c89805 --- /dev/null +++ b/portal/app/models/account.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: accounts +# +# id :uuid not null, primary key +# name :string +# contact_id :uuid not null +# subdomain :string not null +# style :text +# size :bigint default(10000) +# created_at :datetime not null +# updated_at :datetime not null +# +class Account < ApplicationRecord +end diff --git a/portal/app/models/application_record.rb b/portal/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/portal/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/portal/app/models/bet.rb b/portal/app/models/bet.rb new file mode 100644 index 0000000..68beeb9 --- /dev/null +++ b/portal/app/models/bet.rb @@ -0,0 +1,82 @@ +# == Schema Information +# +# Table name: bets +# +# id :uuid not null, primary key +# tip_provider_id :string not null +# tip_provider_bet_id :string not null +# tip_provider_percent :float not null +# exchange_id :string not null +# exchange_event_name :string not null +# exchange_event_id :string +# tip_provider_odds :string +# expected_value :float +# exchange_bet_id :string +# stake :float default(0.0), not null +# outcome :string default("processing"), not null +# created_at :datetime not null +# updated_at :datetime not null +# exchange_account_id :string +# tip_provider_event_id :string +# exchange_odds :string +# team1 :string not null +# team2 :string +# tip_provider_market_details :jsonb +# exchange_market_details :jsonb +# log :jsonb +# original_json :jsonb +# executed_odds :string +# placement_attempts :integer default(0) +# period :string +# +class Bet < ApplicationRecord + enum outcome: { processing: 'processing', won: 'won', lost: 'lost', open: 'open', expired: 'expired', skipped: 'skipped', ignored: 'ignored', errored: 'errored', voided: 'voided', cancelled: 'cancelled'} + scope :placed_bets, -> { where('outcome in (?)', %w[won lost open voided cancelled]) } + scope :retryable_bets, -> { where("outcome in ('expired', 'skipped')") } + + default_scope { where(exchange_account_id: ENV['EXCHANGE_ACCOUNT']) } + + belongs_to :exchange_account + include Loggable + def json_payload + { + id: id, + exchange: exchange_id, + exchange_event_name: exchange_event_name, + created_at: updated_at, + exchange_odds: exchange_odds, + tip_provider_odds: tip_provider_odds, + expected_value: expected_value&.round(1), + stake: stake, + outcome: outcome, + market_identifier: tip_provider_market_details.to_s, + exchange_market_details: exchange_market_details.to_s, + bet_placement_type: bet_placement_type, + executed_odds: executed_odds, + ev_percent: tip_provider_percent.round(2), + event_scheduled_time: original_json['started_at'] ? Time.at(original_json['started_at']) : "", + log: show_log + } + end + + def self.calculate_expected_value(stake, odds) + (stake * odds) - stake + end + + private + + def placed_bet? + open? || lost? || won? || voided? + end + + def bet_placement_type + if placed_bet? + return 'Simulated' unless exchange_bet_id + + return "Placed [#{exchange_bet_id}]" + end + '' + end + + +end diff --git a/portal/app/models/betfair_event.rb b/portal/app/models/betfair_event.rb new file mode 100644 index 0000000..88c10aa --- /dev/null +++ b/portal/app/models/betfair_event.rb @@ -0,0 +1,5 @@ +class BetfairEvent < ApplicationRecord + has_many :betfair_event_runners, dependent: :delete_all, primary_key: :event_id + + enum status: { open: 'open', closed: 'closed' } +end diff --git a/portal/app/models/betfair_event_runner.rb b/portal/app/models/betfair_event_runner.rb new file mode 100644 index 0000000..ac2f8c4 --- /dev/null +++ b/portal/app/models/betfair_event_runner.rb @@ -0,0 +1,6 @@ +class BetfairEventRunner < ApplicationRecord + belongs_to :betfair_event, foreign_key: :event_id + has_many :betfair_runner_odds, dependent: :delete_all + + scope :runners_for_open_events, -> { joins("INNER JOIN betfair_events ON betfair_event_runners.event_id = betfair_events.event_id").where(betfair_events: { status: 'open' }) } +end diff --git a/portal/app/models/betfair_runner_odd.rb b/portal/app/models/betfair_runner_odd.rb new file mode 100644 index 0000000..8e06eb9 --- /dev/null +++ b/portal/app/models/betfair_runner_odd.rb @@ -0,0 +1,4 @@ +class BetfairRunnerOdd < ApplicationRecord + belongs_to :betfair_event_runner + enum bet_type: {back: 'back', lay: 'lay'} +end diff --git a/portal/app/models/concerns/latest.rb b/portal/app/models/concerns/latest.rb new file mode 100644 index 0000000..ad44179 --- /dev/null +++ b/portal/app/models/concerns/latest.rb @@ -0,0 +1,6 @@ +module Latest + extend ActiveSupport::Concern + included do + scope :latest, -> { order(created_at: :desc).first } + end +end diff --git a/portal/app/models/exchange_account.rb b/portal/app/models/exchange_account.rb new file mode 100644 index 0000000..986cbb4 --- /dev/null +++ b/portal/app/models/exchange_account.rb @@ -0,0 +1,218 @@ +# == Schema Information +# +# Table name: exchange_accounts +# +# id :string not null, primary key +# exchange_id :string not null +# exchange_name :string not null +# login_uid :string not null +# login_pass :string not null +# apikey :string not null +# contact_name :string not null +# contact_email :string not null +# account_balance :float default(10000.0) +# account_balance_last_checked :datetime +# ssl_pem :text +# allow_multiple_bets_per_event :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# log :jsonb +# betting_enabled :boolean default(FALSE) +# last_session_token :string +# last_session_token_saved_at :datetime +# status :string default("inactive"), not null +# stake_strategy :jsonb +# last_log_time :datetime +# +class ExchangeAccount < ApplicationRecord + has_many :bets + has_many :source_subscriptions, dependent: :delete_all + has_many :tip_sources, through: :source_subscriptions + has_many :subscription_runs, through: :source_subscriptions + enum status: { active: 'active', inactive: 'inactive'} + + DEFAULT_STRATEGY = + { + max_bankroll_per_bet: 0.2, + stake_sizing: 'MINIMUM', #[MINIMUM,FIXED, HYBRID, PROPORTIONAL] + min_odds_to_bet: 1.1, + max_odds_to_bet: 3.0, + min_ev: 2, + max_ev: 7, + fixed_stake_size: 3.0, + odds_margin: 0.1, + kelly_multiplier: 0.3, + }.freeze + + DEFAULT_HYBRID_STRATEGY = + { + max_bets: 10, + action_on_max_losses: 'REBASE', #[REBASE] + pause_in_minutes: 0, + rebase_status: :complete, + current_stake: 0.0, + last_account_balance: nil, + account_balance_checked_at: nil, + max_losses: 0.1 + }.freeze + + def json_payload + { + id: id, + exchange_name: exchange_name, + exchange_id: exchange_id, + contact_name: contact_name, + contact_email: contact_email, + account_balance: account_balance, + is_active: active?, + can_bet: can_bet?, + stake_strategy_name: stake_strategy_description, + log: subscription_runs.empty? ? nil : last_run.log, + last_log_time: subscription_runs.empty? ? updated_at : last_run.created_at, + stake_strategy_config: current_stake_strategy, + source_types: tip_sources.empty? ? 'None' : tip_sources.pluck(:source_type).join(' , ') + } + end + + def last_run + subscription_runs&.latest + end + + def self.mounted_account + find_by_id(ENV['EXCHANGE_ACCOUNT']) + end + + def self.mounted_account_payload + return {} unless mounted_account + + mounted_account.json_payload + end + + def optimal_stake(bet, exchange_minimum, stake_available) + return exchange_minimum if minimum_stake? + + x = [stake_by_strategy(bet).round(1), stake_available].min + + [x, exchange_minimum].max + end + + # # account_balance will ultimately come from accounts model + def actual_account_balance + return account_balance if betting_enabled + + simulated_account_balance + end + + def simulated_account_balance + account_balance + total_won - (total_lost + total_open) + end + + def total_won(subset = my_bets) + subset.won.sum(:expected_value).round(1) + end + + def total_lost(subset = my_bets) + subset.lost.sum(:stake).round(1) + end + + def total_open(subset = my_bets) + subset.open.sum(:stake).round(1) + end + + def can_bet? + return false unless betting_enabled + + actual_account_balance > total_open + end + + def my_bets + Bet.unscoped.where(exchange_account_id: id) + end + + def current_stake_strategy + if stake_strategy.blank? + update(stake_strategy: DEFAULT_STRATEGY) + end + @current_stake_strategy ||= stake_strategy.with_indifferent_access + end + + def minimum_stake? + stake_strategy_description == 'MINIMUM' + end + + def stake_strategy_description + return current_stake_strategy[:stake_sizing] unless current_stake_strategy[:stake_sizing].blank? + + 'MINIMUM' + end + + def stake_by_strategy(bet) + return classic_kelly_stake_sizing(bet) if current_stake_strategy[:stake_sizing] == 'KELLY' + return proportional_stake if current_stake_strategy[:stake_sizing] == 'PROPORTIONAL' + return current_stake_strategy[:fixed_stake_size] if current_stake_strategy[:stake_sizing] == 'FIXED' + return hybrid_stake_sizing if current_stake_strategy[:stake_sizing] == 'HYBRID' + + 0 + end + + # def kelly_stake_sizing(bet) + # proportional = proportional_stake + # multiplier = current_stake_strategy[:kelly_multiplier] || 0.3 + # kelly_factor = bet.tip_provider_percent * multiplier + # stake = kelly_factor * proportional + # + # return 0 if stake.negative? + # + # [stake, proportional].min + # end + + def classic_kelly_stake_sizing(bet) + multiplier = current_stake_strategy[:kelly_multiplier] || 0.3 + max_per_bet = proportional_stake + kelly(odds: bet.executed_odds.to_f, probs: bet.tip_provider_percent, balance: actual_account_balance, multiplier: multiplier, per_bet_max: max_per_bet) + end + + def kelly(odds:, probs:, balance: , multiplier:, per_bet_max:) + kelly_stake_percent = (((odds * probs) -1) / (odds - 1 ) * multiplier)/100 + stake = kelly_stake_percent * balance + + return 0 if stake.negative? + + [per_bet_max, stake].min + + end + + private + + def hybrid_stake_sizing + hybrid_strategy = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone + last_based_account_balance = hybrid_strategy[:last_account_balance] + last_based_account_balance ||= rebase_hybrid_strategy + + max_losses = hybrid_strategy[:max_losses] + max_allowable_losses = last_based_account_balance * max_losses + + difference = actual_account_balance - last_based_account_balance + if difference.abs >= max_allowable_losses + rebase_hybrid_strategy + end + current_stake_strategy[:hybrid][:current_stake] + # raise "[Bet sizing] Betting on '#{id}' paused due to stake management rules" + end + + def rebase_hybrid_strategy + puts 'Rebasing hybrid stake because account balances have gained/loss at least the max' + hybrid = current_stake_strategy[:hybrid] || DEFAULT_HYBRID_STRATEGY.clone(freeze: false) + hybrid[:last_account_balance] = actual_account_balance + hybrid[:current_stake] = ((actual_account_balance * hybrid[:max_losses]) / hybrid[:max_bets]).round(2) + hybrid[:account_balance_checked_at] = Time.now + current_stake_strategy[:hybrid] = hybrid + update(stake_strategy: current_stake_strategy) + hybrid[:current_stake] + end + + def proportional_stake + (current_stake_strategy[:max_bankroll_per_bet] * actual_account_balance) + end + +end diff --git a/portal/app/models/loggable.rb b/portal/app/models/loggable.rb new file mode 100644 index 0000000..5fb4034 --- /dev/null +++ b/portal/app/models/loggable.rb @@ -0,0 +1,20 @@ +module Loggable + extend ActiveSupport::Concern + + def set_log(s) + log_this(s, false) + end + + def log_this(s, append = true) + x = append ? log || [] : [] + x << "[#{Time.now}]: #{s}" + update(log: x) + end + + def show_log + x = log || [] + return 'Nothing logged' if x.length.zero? + + x.join(',') + end +end diff --git a/portal/app/models/source_subscription.rb b/portal/app/models/source_subscription.rb new file mode 100644 index 0000000..96af948 --- /dev/null +++ b/portal/app/models/source_subscription.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: source_subscriptions +# +# id :uuid not null, primary key +# exchange_account_id :string +# tip_source_id :uuid +# created_at :datetime not null +# updated_at :datetime not null +# +class SourceSubscription < ApplicationRecord + belongs_to :tip_source, dependent: :delete + belongs_to :exchange_account + + has_many :subscription_runs + has_many :tip_source_data, through: :tip_source +end diff --git a/portal/app/models/subscription_run.rb b/portal/app/models/subscription_run.rb new file mode 100644 index 0000000..c0b5024 --- /dev/null +++ b/portal/app/models/subscription_run.rb @@ -0,0 +1,17 @@ +# == Schema Information +# +# Table name: subscription_runs +# +# id :uuid not null, primary key +# source_subscription_id :uuid +# tip_source_data_id :uuid +# log :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# +class SubscriptionRun < ApplicationRecord + include Loggable + include Latest + belongs_to :tip_source_data + belongs_to :source_subscription +end diff --git a/portal/app/models/tip_source.rb b/portal/app/models/tip_source.rb new file mode 100644 index 0000000..3dc7cd4 --- /dev/null +++ b/portal/app/models/tip_source.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: tip_sources +# +# id :uuid not null, primary key +# tipster_account_id :string +# description :string +# source_type :string +# active :boolean default(FALSE) +# filters :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# +class TipSource < ApplicationRecord + belongs_to :tipster_account, dependent: :delete + has_many :source_subscriptions, dependent: :delete_all + has_many :tip_source_data, class_name: 'TipSourceData' + has_many :subscription_runs, through: :source_subscriptions + + scope :active_sources, -> { where(active: true)} + +end diff --git a/portal/app/models/tip_source_data.rb b/portal/app/models/tip_source_data.rb new file mode 100644 index 0000000..5058151 --- /dev/null +++ b/portal/app/models/tip_source_data.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: tip_source_data +# +# id :uuid not null, primary key +# tip_source_id :uuid +# data :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# +class TipSourceData < ApplicationRecord + include Latest + belongs_to :tip_source, dependent: :delete + has_many :source_subscriptions +end diff --git a/portal/app/models/tipster_account.rb b/portal/app/models/tipster_account.rb new file mode 100644 index 0000000..3664fd8 --- /dev/null +++ b/portal/app/models/tipster_account.rb @@ -0,0 +1,18 @@ +# == Schema Information +# +# Table name: tipster_accounts +# +# id :string not null, primary key +# tipster_name :string not null +# apikey :string not null +# filter_ids :string not null +# contact_name :string not null +# contact_email :string not null +# created_at :datetime not null +# updated_at :datetime not null +# +class TipsterAccount < ApplicationRecord + has_many :tip_sources + has_many :tip_source_data, through: :tip_sources, class_name: 'TipSourceData' + has_many :tip_source_data_processing_runs, through: :tip_source_data +end diff --git a/portal/app/models/user.rb b/portal/app/models/user.rb new file mode 100644 index 0000000..f5df25e --- /dev/null +++ b/portal/app/models/user.rb @@ -0,0 +1,82 @@ +# == Schema Information +# +# Table name: users +# +# id :uuid not null, primary key +# first_name :string +# last_name :string +# email :string default(""), not null +# encrypted_password :string default(""), not null +# reset_password_token :string +# reset_password_sent_at :datetime +# remember_created_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# uid :string +# provider :string +# +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and + devise :database_authenticatable, :registerable, :omniauthable + + + def name + "#{first_name} #{last_name}" + end + + def self.find_or_create_from_auth_hash(auth) + user = where(email: auth.info.email).first + + unless user.present? + user = where(provider: auth.provider, uid: auth.uid).first_or_initialize + user.email = auth.info.email + user.password = Devise.friendly_token.first(8) + end + + unless user.provider.present? + user.provider = auth.provider + user.uid = auth.uid + end + + user.first_name = auth.info.first_name + user.last_name = auth.info.last_name + + user.save! + + user + end + + def fullname + "#{first_name} #{last_name}" + end + + def confirmation_token + if read_attribute(:confirmation_token).nil? + self.confirmation_token = generate_confirmation_token + save! + end + read_attribute(:confirmation_token) + end + + def json_payload + { + id: id, + email: email, + first_name: first_name, + last_name: last_name, + } + end + + def admin? + %w[mike@wizewerx.com].include?(email) + end + + private + + def generate_confirmation_token + token = Devise.friendly_token(10) + token = Devise.friendly_token(10) while User.where(confirmation_token: token).count.positive? + self.confirmation_token = token + end +end diff --git a/portal/app/views/layouts/application.html.erb b/portal/app/views/layouts/application.html.erb new file mode 100644 index 0000000..b84b5e8 --- /dev/null +++ b/portal/app/views/layouts/application.html.erb @@ -0,0 +1,19 @@ + + + + betbeast : taking a bit out of arbitrage + " rel="shortcut icon"> + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + + + +
<%= yield %>
+ + + + diff --git a/portal/app/views/pages/index.html.erb b/portal/app/views/pages/index.html.erb new file mode 100644 index 0000000..e69de29 diff --git a/portal/app/views/user_mailer/notify_hourly_unread_activities.html.erb b/portal/app/views/user_mailer/notify_hourly_unread_activities.html.erb new file mode 100644 index 0000000..4ec2f2c --- /dev/null +++ b/portal/app/views/user_mailer/notify_hourly_unread_activities.html.erb @@ -0,0 +1,11 @@ +<% @activities_hash.except(:all_activities).each do |k, activities| %> + <% if k.starts_with?('project_asks.') %> +
+

Activities

+ + <% activities.each do |activity| %> + <%= render partial: 'partials/project_ask_digest', locals: { activity: activity } %> + <% end %> +
+ <% end %> +<% end %> diff --git a/portal/babel.config.js b/portal/babel.config.js new file mode 100644 index 0000000..1b36dca --- /dev/null +++ b/portal/babel.config.js @@ -0,0 +1,83 @@ +module.exports = function(api) { + const validEnv = ['development', 'test', 'production']; + const currentEnv = api.env(); + const isDevelopmentEnv = api.env('development'); + const isProductionEnv = api.env('production'); + const isTestEnv = api.env('test'); + + if (!validEnv.includes(currentEnv)) { + throw new Error( + `${'Please specify a valid `NODE_ENV` or ' + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: '}${JSON.stringify( + currentEnv + )}.` + ); + } + + return { + presets: [ + isTestEnv && [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + modules: 'commonjs', + }, + '@babel/preset-react', + ], + (isProductionEnv || isDevelopmentEnv) && [ + '@babel/preset-env', + { + forceAllTransforms: true, + useBuiltIns: 'entry', + corejs: 3, + modules: false, + exclude: ['transform-typeof-symbol'], + }, + ], + [ + '@babel/preset-react', + { + development: isDevelopmentEnv || isTestEnv, + useBuiltIns: true, + }, + ], + ].filter(Boolean), + plugins: [ + 'babel-plugin-macros', + '@babel/plugin-syntax-dynamic-import', + isTestEnv && 'babel-plugin-dynamic-import-node', + '@babel/plugin-transform-destructuring', + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-proposal-class-properties', { loose: false }], + [ + '@babel/plugin-proposal-object-rest-spread', + { + useBuiltIns: true, + }, + ], + [ + '@babel/plugin-transform-runtime', + { + helpers: false, + regenerator: true, + corejs: false, + }, + ], + [ + '@babel/plugin-transform-regenerator', + { + async: false, + }, + ], + isProductionEnv && [ + 'babel-plugin-transform-react-remove-prop-types', + { + removeImport: true, + }, + ], + ].filter(Boolean), + }; +}; diff --git a/portal/bin/bundle b/portal/bin/bundle new file mode 100755 index 0000000..b9a7b0f --- /dev/null +++ b/portal/bin/bundle @@ -0,0 +1,118 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV['BUNDLER_VERSION'] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + + bundler_version = Regexp.last_match(1) + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV['BUNDLE_GEMFILE'] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path('../Gemfile', __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0') + + requirement += '.a' if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV['BUNDLE_GEMFILE'] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem 'bundler', bundler_requirement + end + return if gem_error.nil? + + require_error = activation_error_handling do + require 'bundler/version' + end + if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + return + end + + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? diff --git a/portal/bin/rails b/portal/bin/rails new file mode 100755 index 0000000..7a8ff81 --- /dev/null +++ b/portal/bin/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('spring', __dir__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/portal/bin/rake b/portal/bin/rake new file mode 100755 index 0000000..0ba8c48 --- /dev/null +++ b/portal/bin/rake @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('spring', __dir__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/portal/bin/setup b/portal/bin/setup new file mode 100755 index 0000000..33a3949 --- /dev/null +++ b/portal/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require 'fileutils' + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to setup or update your development environment automatically. + # This script is idempotent, so that you can run it at anytime and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies + system('bin/yarn') + + puts "\n== Copying sample files ==" + FileUtils.cp 'config/database.yml', 'config/database.yml' unless File.exist?('config/database.yml') + + puts "\n== Preparing database ==" + system! 'bin/rails db:create' + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/portal/bin/spring b/portal/bin/spring new file mode 100755 index 0000000..d89ee49 --- /dev/null +++ b/portal/bin/spring @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +# This file loads Spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == 'spring' } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/portal/bin/webpack b/portal/bin/webpack new file mode 100755 index 0000000..d922a7d --- /dev/null +++ b/portal/bin/webpack @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV['RAILS_ENV'] ||= ENV['RACK_ENV'] || 'development' +ENV['NODE_ENV'] ||= 'development' + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +require 'bundler/setup' + +require 'webpacker' +require 'webpacker/webpack_runner' + +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/portal/bin/webpack-dev-server b/portal/bin/webpack-dev-server new file mode 100755 index 0000000..4e5f40d --- /dev/null +++ b/portal/bin/webpack-dev-server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV['RAILS_ENV'] ||= ENV['RACK_ENV'] || 'development' +ENV['NODE_ENV'] ||= 'development' + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +require 'bundler/setup' + +require 'webpacker' +require 'webpacker/dev_server_runner' + +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/portal/bin/yarn b/portal/bin/yarn new file mode 100755 index 0000000..d3627c3 --- /dev/null +++ b/portal/bin/yarn @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + exec 'yarnpkg', *ARGV +rescue Errno::ENOENT + warn 'Yarn executable was not detected in the system.' + warn 'Download Yarn at https://yarnpkg.com/en/docs/install' + exit 1 +end diff --git a/portal/client/packs/application.js b/portal/client/packs/application.js new file mode 100644 index 0000000..5e0fd42 --- /dev/null +++ b/portal/client/packs/application.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; + +import { library } from '@fortawesome/fontawesome-svg-core'; +import { fab } from '@fortawesome/free-brands-svg-icons'; +import { fas } from '@fortawesome/free-solid-svg-icons'; +import { far } from '@fortawesome/free-regular-svg-icons'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en.json'; + +import '../src/assets/style'; + +import EntryPoint from '../src/entry_point'; + +library.add(fas, fab, far); + +TimeAgo.addDefaultLocale(en); + +document.addEventListener('DOMContentLoaded', () => { + const container = document.getElementById('app'); + const root = createRoot(container); + root.render(); +}); diff --git a/portal/client/src/assets/images.js b/portal/client/src/assets/images.js new file mode 100644 index 0000000..e295d71 --- /dev/null +++ b/portal/client/src/assets/images.js @@ -0,0 +1,5 @@ +import MAIN_BACKGROUND_IMG from './images/main-bg.jpg'; +import LOGO from './images/logo.png'; +import LINKEDIN from './images/linkedin.png'; + +export { MAIN_BACKGROUND_IMG, LINKEDIN, LOGO }; diff --git a/portal/client/src/assets/images/iconmonstr-arrow-65-240.png b/portal/client/src/assets/images/iconmonstr-arrow-65-240.png new file mode 100644 index 0000000000000000000000000000000000000000..54986b8bb2d3ee72997859db826e753809738fa3 GIT binary patch literal 5087 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 literal 0 HcmV?d00001 diff --git a/portal/client/src/assets/images/logo.png b/portal/client/src/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..61964c60085916694d85db6fc900118a8674dc28 GIT binary patch literal 8180 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 Q`=rNHRnkzblD7={A1m>YJOBUy literal 0 HcmV?d00001 diff --git a/portal/client/src/assets/images/logo.svg b/portal/client/src/assets/images/logo.svg new file mode 100644 index 0000000..8bd246b --- /dev/null +++ b/portal/client/src/assets/images/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/portal/client/src/assets/images/main-bg.jpg b/portal/client/src/assets/images/main-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b41ef0ba65227ca09f977a02eb392bcbf4d7623 GIT binary patch literal 111605 zcmb@tcTiK?_cxpXp@bqO^dbQQNhku+i$Wk6LXm_XK&eWVj$pZ$l2Af1KtdNG1cXq8 z7?2`f2-1RuE>#5q5z(t$#CEUu<@@`+Gr#AZd7nR?nP;6jb3SLSHG8kK&u(X}b^cxY z_ccHq>)_}B00Myk%<%#EcaulS(a!Fo8~(h5qci$g008iaM2EyA@`wWfF;|ij@aIsf zKE4E1fi(aSAP?XLAOPk;A&GIeZf?$i|1BLY9%~=dobG?h`roSk{|msOVTmEfqQ8&N z3n6g{NdN%Q{}>BjO^W*u4nM|_;Hdw?H~xbYjtxBKQ~rZ5|2Jm*m(G9V-v83^z}p?` zJUPZTm;VHpJK|HI>zIX2<+ z@w;IB4=p_oi`9J`E0l{M_MjQa-1M>3l@$vHW@qu`G1c1lt5_}*@sDUlN zlp4-3C_V#HTrZ%G!gmczpPt!}(FjhkbI&aKBn$g&WKU-qha?g?$0aEOkL&&a)C1(< z1@M9R1&)PA5&)k65Rezh&;K8_<6g!i!3&i%;ImbeGQ$($Gs}fQIA!yNZMg1qo6<;vx158&biEqvQ>#W{?iG zyW6Hw$%_L4+)lyrEPn?0vdLNk8UEtU5CZ zzaJ9=vKu>aL>-wbOZV$(&RF446*jU6q=dn+oS=Zm*vhb-{16VhA?1eIn9=IGgDMAd zw{ZCh@%)`Jk*faNmYB#5PNjW@Y9l4aXHW8uEo3wtI3u%)lB#-zRih8&z#|N8xc~_V zp$x?wu;PNm2C&?IjH`6eH{aLM*~sAl4|Qk|kBmL%{q9Wi7S63^=LU5gn0EBNsnd;; zTO^%hqaZHxshkp?`vdmy+MFChB&!;o5^M7TLN6zj72zP#lOM=4EETuT>E?Zvz3DH; ziUy;4sz!(hVS&xeZ_Q0(91G&~^k&79T!2jjkqg<61S`H<5U_t&7ScxbgQdKgWBIaR5+{A`&^r$ zr0Zv#&r1@Zo7}?s6>9PI%YN#!ZcA!!%6Du*gczo?Cz=m&=|s9DOF`v8pgOa4ylR2tK~bZ zh6p1?q!*r%5h?e>m1yFC#UjbZ)TAb=%5wNSz(<0o_X#}v8uCvS#%I29hnqrp{lUqC zZ#c2$q^Dlw9k7r+&O5i$^Xhotp(XWQK73~Bb-CNN|BYCQLzlAf zgZj-Dxmn(atxKW&xE~DXsd#vu(HZisa11l7CQso9Aj#Sd0I*+-HSspDwsrqU7>CC@ z^*ZN~gp^9|6qs58Di0k~A~#TK*$d5uicT3sBjLXn#%z$>o#;JGh=$5VR{QCW`AL^f zNtMmt!qp8xtO^!$RcjgYtPZcRos|1>3h;?e$x*oqqgn#jwH1UF9ay*%F)6li9e8thZyD%w9~2=|`sr z?$W>eON>pKP~kUX>oUD1W8@%ZpM%tpKXhz>ml1e?ijBK)OLOS<;)Bi(5_VMx*O{Z0 z&{%IgE7?v=4erpscKs>HrxurcU#I+9gldAv6{E&1D!xucw3GhMy6Xpz--@no;V4Gsm1UBL~|y=miU`jhD<1(T}pEq@p(N?w3B?O(`> z&Pl%ex3i~0cx$|3chQrY_a^)J=KxTMxK+_@v}`kGbnX~ zE(Mb&?;rSz&Ad0H*1$t6=~3ZpEm%ni!mX1h!Zp(AT0?wYmOTGo9K{Vg>}Gnj$(>~7 z=`6HMTa-qTs}9ZsNCM_+kRIIC!=8 z@Q#r^=R}>j;-u|MYD!Xu@h{1ZZ1*72b1OdlINCy<*DpXO)&ci&Qet1}i(OJG{_CGw z%X+(YnF}(6e*sv#S9?gimWRmaS6d(%VM_rV7Mz6TjdIW|eAxW_oU=)W?uJkd-#_(; zzd=SzPa$tTvG>hAhwgLV-8!dp^sJCHYLw+Toh~qgJcok^bd@j?eNvV&wE4=NYZ2bG zMRzpMd34)#s8XA104%UKt+w$h?CHU+8TI+h2a@^c5P2@}2bw$~zQY?%d5s1@tUpKt zS(Y1d*x1*Dn=LEaJ(Ov?SdLNhygPk4%+-!Nvuw`D*z0?;Av)tcAT(KT%nfz8g}9uV zS5qr>CY?PMHc&+&0KL>%FWoSsM%+n)_SX%k-&Ct6-Z>5N%=tVt4*6PmC(O;4GW$js z%*G|#B6(6qR$aE$T*0h2qaat5U2*Ou#Q5xyEFS4ecR78z94%nzj+A>&`errS!}&4c@?QUxMU6v^rxuu;+T?}S^5#dmD1u* zd~_C$ba(9iJgwcwij9xDCdSO_ua_*5N%LkeJvbY`&+M)))Gct1TjI$86TivS> zO|OZD?=f@lSG|-le%Q=kE`aB4NOYk%JZnzYse2u+GjZS~>ttGC?)Kw%ZHq_uI3)Ac zKAVkRxSqps(8Y#oRtp5j%ZYBoxZW;}%;t^wTr2H$2jBTJ&nIMQHo7C~D!kx|R|EMG zIc6rb%_*pU;cEF!_btf$9sSJArKBQpADi8yWH0q(+FV9wNq1GT?apkj^Hq@W@IEtF zAHtqN@#r>{0%a=Us|!Ngj~QJI$ZIo44^K7>DZMH%-?m9={dkgAqQAL(lJFW-^8KU{>A4B+ zdu>>`BFxT)J$*~cn!F_~CL^U{?nW?S`cSp#_k}jROe2a*WTG4bZfOwSo^-?}wxqm| zaI*6i!t?pHI61sOKYcIkH}YeU?moRz{0ks|g=ar+c1b9qTSMc)R@q{Vt+jl6_8BpJ z?W(>1-DcbHE2G8_oA%chRxRmDplTlb6ntX>zZw-!Thj^l+66&1iP>Zs-Akwx`yJyK~NH(EPT0>7TU0(-QR_zkFA+rkiSqP56 z2<;d+hOr}}jE}dpTneNvG!7g^0?LL8*g#mg>TR8(7D~Y$E7y=CF(a2ShPuFoBTLt2 zw^%FA6$QmJNV_m2G0C-&1;{0y*AXeRvYO($YjA&_q6xmTeB_7^LeqfDn&EgExv>zU zcV$*YAJ%2dU^VG+ZjWsw!bky|HJ;U*B^tu22Fc;X-}Zb86yZxPl3busLK#{ZEx!$a zZ_B?@GVPIhY#eS{#U&H3vfgt~jNJByX%JX4<57znbKBSWR5gZg(9DdQ?^}fQSlaJ? zbEH#undn-nx{%@?2xn*POup$ZSGp&0_1(7Wr&RlQUm=4-v=XDL5a5RGUY#Wg6nITI zZiVvEzo1t3bV;S)_?xU7t)DG2TgB_7(i(-L-{p+TIH!xXNS-*fN{xe0n6r|lMN=V#3KAoQRNOF+WcDuYUww|%2NFtaRLaV2G# zaFfZjtD)tD?s*N2MO~pW>J)WM5sVPuFw6DpZ*qXdVWa$P`8l+LsAjn4^tyUDo8$%Q|M{VL5+x_0b!hKe-=k%&cN5+i z#`@!UMqF{xqKU~!IA4GTrn$3e#;&g6pqA=-d?z$UI{2FtZ-l=;Z9loL`rXaeCsWH3 zye0QnqBTuxCA&k=g@47iY$JBKr*!8to}M+PtSaZ7tVAvs$P_V)?ueQ^7e*HkunJys z(cvMst-r_@Z6C%p7ABs9BE5keTO#)t^Zu)0XX?^xW`d;Ds8qDaGgcbrW8n@LAYE=h z5WIy5+HFkcE30*Hq$lkE?#Y!)b5IYGM&}KAW(OP$56TUeY z5bS_A$}f;Xn%r|VsAW!t(^srZwNPfu&Y~UL<{cYJZ9*}7)t$NqpZaUq6?&FOzPL}H zx&zPl<116iwAVh!t~i*3)9sx&%1N#PK?@N*AeC-~m`q(B_G~C2Bkt@Hdae+Vvi^A= z!s9$+7b2gCb>2R7wJupoV^u-lP{#m$Z>eS8WYpszgDxWd2cq}GjXzgj-sXyMGeaHD zeF$W+q2rgdO&!s%W}Jg{%$)Lm(2DQWeqSGk4rZ1eSOkE&C6=o)Do2Z}V*luWmI@O4 zC0?#qF^tA*YzG<7-;rlAGuuYFhDO)Hwj97s_?8u{HqY&1pwy(pvphYkhQi5}>E`l> zswXy_+e{+@Ri-qWfl}P0;HqM@PtFFEAENsaG}+sJqP#Xk4cEOFR31cN$6lhY+K0LC z9Zz?0yq)+RH+H^V3jZ@mHeR!O10&-F25~aI_Pj2rfS<7QR)g9I0?7W2QXpEE z%?G2r!t)1XkDESWWkcfYRu)dU#Nhq&yhF~A@MMRXNTX#!( zae_;5N9h^SCM#8*IjFp9y&6|vHpXWNEL@|bwo*Srw9#Qh#9}m?<2EGVHUSIqdwpj7 ztn|GKWljh9#tlY@z4k^NZ~qOsUr{ZQ_Z_bcrQ*uaD$jC*sRgo!P7!|Q6{6(R2R=U| zCO2R(y07Z%Bja{S>%hs19QJGD4hcq1rzX2QDBF+ezVTb_xv*tP3DcMCmLGbJeJ zbdh6Sr+eC^0LV;Lc0;`n)|RPDCe-z0W;Zto5KY4%>e`{EB>PX>jb@72XOwHSK$Hz} zcNUch(hRPzYk}Jaa8Wrh%=`*cBt-(n_S)^|n%(WjRr^j2A(8e>qM#R$*9)prim8ZU9HMxnT zZZ9kI@G85C7~gC)jHjAayA>XxbLg(XdXoCOXTr&BsLyQDc_T&65bL;GAItifV<#8b z&RmO$U~VX?Npr(8fz5QBx9q5@LxY7C^JI+sG}j1>02tU9#6~=2A@94eBf=0 z$ZM~+O(vG9$eJHQng@5^og8lT7?`j4uJ;c9#e+=8rx@Sbw!RzZNf5pJAU(@AU(M@N zYZjOFHFLpt9hZADMB%2>MR8u`8)fi4wn3O$i3=o4Q3v?k3Z|s*S$9>@9$Xc3V$`r= zRQ1qtqM*Amyrd=UuZYd{JUFA^+1?Y~4exsw*Zs*YUiKkNLm!$isxnR_q?jl+K@WUl zAN1FjE&C0s29Ez``>61%_Z=%A{pt4~*6QyddOztv0f(hKqye8jH^g$%uP;g(?I~okM z)@S(!FI?FBIkvkX3yyaXk@Qn8+Cx4rx#oEzeqHJH?1}qYbvHQ59L=YWKu5uuHg;GK z^P|bOt`+nnYg&=@rYgI|Kq#^*jKJULlJ^kG*Z~Wg$TieNF#kweR=&X}9j-c2_K0fs z(Vu72%pUmI%zy7I8GQWZp!u3~A)J^Qqzc@fBV{Ogzv_-U%t2?gQ8JlmeFm5oG2uFPpK-Ug$- zhoG>bXwN@#=@pTgtv0X8yQ+HK6qY4DLi3MD|BGWFjh0;QJE+c^QlgSPN)ms{kng8e4# z>IZc6V}gCt#1IdhF?(1Kylb|}IkV}lrra%_^-;BvK)&9l?I+Vi!;=WVbzpRh`XCHf zy6JT%6!|CrLNL~5T&mG%$VFQ~dFF(hm(f>)!j)8wgM5pWM-|U(wx~+CXG%Q7E)hK> zzv5Nje@uTzcUSzh;&x@TX7QR^i2YsjZOw(6+~+W_MxR56fZI+zKU*R#?A|p0Bxe`rK}l#3}m9x zf?3-0C8Y#JYipgC+3#xI}LiO z5NpCt+VFisjW?@s`YOl$DP$;G0~>x9b-unJWruk{PB7czWyX0Pgoy&gOTq@me?FJ~xs6pV@z* zo#XY=0uxffMc)l4lw-Hh-Ap`Eyg=bW!Zz${(tp$@J|nHB7Jnt4GLsDCv1dp=WOMBS{{Uj zw3m&vxPI|1x#(&6zQujhoATA#t7a8@M;~%ZmEDAKw1k_v&%L=SWcWAs=uU3f-9NJb z1$a6d(O3pvW1Ab_?>sEcK2mvCX^t}a9`E(BCIx-F1bS)W>B;i2J0~?KSss!J-wg*N zc;2%dSvP0bqSY#99k!G5SW(m%L4vNFP=%yLpL47|J7$+wkgoeclkMgPU@vB1Obk&f z_+jUYL=MKeU*o63Xrwx)X~gl#HLvWfI+k(-?$BMkJ;{z#Zg*Xy3VYShh2n-`yRr7K z2mI{!yz{w9zzrM!MUA4!dLmlabOHaSjHEU=%nJ>+4CpDfZ9LJCRG3X#=QHKLO*&rr z=j_%c6*g>kE&5!PCDXSJhlKU%!a6JI(~v)HT-t&%^YAtZ)UZ8xK1!(FR(0E;Q6yho z7=^==xAhr(!!tGx0*(BT^bV$7ID#?sOuUm}=b~o~zTIe7?SeI}yVgh@Bc6b`L*+${ z-2G-WATbF~CJQ?Szye5xTs$v@kluBgCB+@8zmCCi6K$ZpRfQRkW%3<#1v@1KbV7wt zX%6UmpcIc>{Tt%5NqjOI{6n@oWjYTP9BN$V*#uDJ+J{iTn(;S=sTQf?#i!gv8)5~M z&IbFhAPLvCz6a(zUJs{NC^Xcp3I*;XM(EDD^^w9grc!rQu^?qk&Lp`oEi;i%W@+nQHG+1XFd0cAGbm_hmSqY2cbm1?%e z%EYNq9>1jCYvezZ4Q}CsBcjiqB3eeADOy3Rn3vL>kej;MImoLA6@Q=8Phj?~p4U!^ zWp^IM2G?tw^tMu_dhsF^!!iX)yHZ%}46d1KgkxNHN%GJr2Ac=i-yMS^;uy7WHQDgd;6HB2`G!7=ELYJi2|vx~VVwGdM}+&u>JBkA)6b7 zvQ>F5qPmxm*+7opsCvX<^J?D&pWLnXa85*7OKwW_xt+NPfiC*DBa3t81iOsA##?J! zFN7m6NZGl+2h5 zwAxIs3r-X9;4Uw$^S#q;%*U&xHCH96zIz)1;aN-9ZDSFzy574IE{gLL z_mKb%sTxAlwgqKy7@3n5IH3tdv0aQ~Wpw}tTtSHNx&D=Sf7tB>z9hQA1FG7>w~|fo zkO8ZGn6G)sRSyZ*w+*SSMRza-7AafFJXGz~`v<|m3%szs>b9d?X10>LB28_mPhrg+ic10_1kh@qqKx$;c{rT@FP3SiWE3crU0t!@fC}c@`7X^ z^gUGLr@)IX;ZU`2E`VNm6udsDUMvi%Bpvp2Lxb#GX>PCJ&ja1>FV;kZF>%{Is~mR@ zTqFSMyvj+R=S-`U79F=$761ud%_g59E9vBPtS6tct_a)b^MC_fvK>0e6^7PpoJcR`+_=(Nyew{%Wrv=g^e@S+7QCjDzJ9+S(G5yaRhSFn z9fdfq1teENDjnvk!uhXS^mKZMLi zwAf`b21o^mB*z6EVTWJMw$4SD6n(6zpZXq>;9_iXB`6frJtg=08KmpcqrA@hsSZ2* zu&()=HE3f<+8Oue`V;pZ6YSPh|G?eZmc2;P2%egd*{m4fhm;$zgIn@U!~H{#SJ0o4 z$9WY(yH!cexxZkcozb3h5!~_0cfN)E&A1KjlVn@Z)`ZSs7u!Q4m5GW^twukJpB<*V zqr+>~Sz{l~a66X!{b#PV^5|zxhsbbmfhKH_^Ig-}0^x1Qp?0B#EZWdC#YKlNuD&>- zdnqz+>B39N00}XIn}f+gDP`z{5F{slFt14e92%CcBCG!?H{U0e#`F1|rxFHsQAZ=5 zdyckeB9Si{-)XSoSsu3_5*Qw!J_xt&D%m8L49t^HbL2rZ1A}L0YTbXR?`w%2M?wfm zuCb579Y;7{`Am!U7(*%OM(Bfbz@Q8bSR@{y=Z(36U9N|m4|m~vuqoUSAF*xJD|68M zY*;TbV49LKvJ5HYiz^|#)`7USdCbbN+uCwInsjVC4R=>+fA`8%Hxabz;9Zyz7Ta`R z8p1a4d6LVsrEPkb3Fac5HXcs|mW>p&jJv!%#BdgdFPR}S+o+JPP1DG^e{>r4jQ~07 z^!${m#xXVCyaav8H)?aMljuxKfvK*D*q+uQQ8IhT*Va;jI$mF&6wUb!le<;|bz><{g!z?HZ;(n79WCsi9KV)7`LEU~57py_RKgUMm4}2R zRl58O*wM_`#a{iMN1T%~5YPD=?R-&EHrZ_sZCdXFwI$&ze$aB)`;ENvgOQZ8vzRM4;=0r zz9jSE8U?piSj1#c`*GBQVSH4BB_Z1`Osi{9T$In|6Ok{_4PC*b3xpKZx=E5DD>%#m z)c>x}qN9rmb&utZ;F&gn&>v0Ydx#fSDBv)AFYrS=<=$}0SNrjkbchG>&_@7Ife@fh z@&J!y!5VWnvc9SwR9ep-P$W_M&L-9V6_Hes&ag#64}B(H7uE~zRRp+T$L$H4wGldy zu`f3C6zlA2&YdLQ^t^`*CE1@El&XWwHZ@Y9k=jZx7+&g&6o~X&suAyvV+GQCmGl_) z$pH&yL}&+2g8TZ66MrV}T*UeKewSa6!Jaf)W66bIX-p{D&)&)FxV#xOVRjR^iM zU&NK0e5&OM`02j~MQ)LTvnpDX&MDCr__On+kGDFEKPYX_VxaV4*V;VCE5t+R!fL7& zKGa&uM8|6~NmWb|qdLpJ29r6U8J3U_g%$njrvT8j70KuU>ZDKMVlL%Xvcdgzhq)UK z5p@+c>Mwn1YO;7;iI+C!DjePz9`1CYam^pcUJkGwm5YIfN$QMTxcF2{EytBfNOh#W zd?&f|ybkz9O}CS(yysQ97ksr|{QzP9u-bJL+H>N!Jy?uNu7KcCo~PkqBZ)T8EOkK6 zcXLBc=4Hqh;jT)W&J;_YLD$pH@!j1sp%Feq1Mz+Z`Af~)w0;EHC|@MX|D}zcI>1VL zsO;F`e=!Kt74Vwze(nH3ohZ8CtWx1CD9Mj z_pqMvR_vN;oO4CP=Rz{BR5>9>et!E(%9TQwcSW?vPyKtzusBamCjsP~ayRonE_OrP z%P`4!Jl^dM8(uiESsX*3I^%f&a!M)FWa|!8<0pG;|KXw%>&#S4CKRna{c;Jx%mI zvt_l{nlxn>?5KZJKd^bc6Vi(uX47?&T#0FRvmv9l)VPlOupJ+xeyI*c14<5=qY(k& zd}Qa4XjSLHH>8SS*J%l{TytfQoD{72OJA6E@$|N{2kPC)2XwF186O6%#0Kb5UQ+Os z<5>=w@zAKcsWe?709G(VCojm1NZXg%>V3n%C2xS*@}8~=Wl^tKsYbP+%WHw!Kx?HL zt}&3I0M!?hF|-}Em70JnpyBZw(5Pv#y~aG(tMH}Ij?E?4MaRkwKj=Q^YbaA(B%NT7Sp^4&@8%Xe>izaI zx4kq*D=bUST2dn@vf^z6+D8>L+LP6igpc)DJVUjv#%3&-hAz-2EUQ|vqOB4qu|n1B z$bCjnn;j|#V#kaM=F?R5N(wgn08^OY0iC4b!h}NCAPwA4KWuc_+JH55vEae3(;0 z!G;#?l`xA^#9c#E=F5`3LLg?o{YIXh&WIC1k$n-T@j|!YjJ(kw+EQmKN(;P0Yb6J} z%IKcow(ccAD9n*hkLNzl|9hx=b22I3?Z^-IL-u4?bV1zYap33HbcMhFrX%~oSKHs@ zKET*~&pVmaamly%kigDnw&PkQq1Ykf-58`~c3eFL96p=7hAr~UIcVFQjA=C2|H#+U zZaC}TKyNbJCg^NvDU%p0o&FZ;T3vzZ2N~dpZH=*w4Cm-siJ&SEP4TO(x6bxs@*cIi zQsyMC2NspWE#GLjh|3&QEq$;q#Xc?)U%5`-P$k3LE19q+TK0{FlrLU#St5?tqvKjF zGuzFkJ!{iYP>OK5o~x=I&C=VHxM;}bg#~ui?&iB!tq3~Ga-F)E=@NA}B1g_E=_P76 z6P^-`qDaa!K#pWzQ6{R&f0|4YM4~oj@nd`>rG`Cmh>fic?h6W~3KBga7wR!=KnQ;7 zk$+yEf3Fy%E*Jtb>Mt_@p>YnNFSzayNf2oW5nTJ8?|4J>4mAcH#2uk{s_W8clCYG9 zg!e$;{@d&IZ2L^fpL9~O7JcWSZyjPpSA9mdqXyJ#?WEh5GXdMjU#;%-LTml|P@#gD z*)f}y4~KZs>D2Ts`c9Z4!5xLQk2Y(vzSy26BW`mYnjxhq1YvPDGG%&NH%B3;UF zMnkn2yNma;( zP7lQ)MG>a!_O=`m9Y}Q0mzjBh!Wk!4Vg&>%!{c@{7S9V-<+X3~JS7}wxBBnqd+rzk zGA|t`WU6QsJJphZX~Zm9AkXD!`}_+KRo5eq^_;=1yj9pQ)<}}xvHxm1yZS@JCC^wf z2wzZ?XZMM0V(aF}cq5^V@*whJTSFDX9k=8B3-qavGwfgBKTxY7mXzr5yTKRU3krB% zasEvd*+{xeb$6f=_HVfGC6FBeW0u5YzMu{1fSs;l1E{qf8lC1_nsNOn}Sz;MSD-PwzRtDcbq8Djn#P-{v2-?#LDnBT!IX;(dLr{YcZ86~rs>Y>fMm0(VU zwLtQ&QL~fU{q>;Db30E2!rpzZkQ)NJasQF!43@o-URG=-yS>Z}O%2~jR7QoajT~T4 zUli(~T8Vq6$tgHG6N4{P#P47J9Y&b=-ShV8X6<3|Y6<8ANGrXd+Rn>!=Ghs98RJ}e zTmk`*)=_^~vCRq@MWti{G zAGdww^gB;GmqRZ(QK;&h>v9;!(%RhHLGVY@m=XuyBM`h|AG!5 zTlPG4izK?`f>xZp;pC~SU;n_BTJK~*XevVxV=&q3ESzhd*uUDuQ%!nX( zs#7=u95NbLU+~o>xKvTdOVLFT08ExF-VdV(AOp%Rau97}Nw=EvVF7Y6W0&6IvwuU@#K(I-@9NMa>*#(F6<%ti#mhFoTq! zOrLqdfJls+jSrw@hl;R&oa{ev7YVQzkiwp#JwsWv1*Mnte@ILXh~mq9DGs?z$rz%O zLV3B-VQ^l|l8eq$iHp>CE7EYj#(61}>vZjgr2k-M?Z*ZLx_DaO-X; z)phiBHZad3u4e@UUMsbvppdzVkg*gsJxJ@|gl@q~(Ns(& zLb-0t|ByST#;IG^ex7SaV1DkNXz_uG!Myd?8}c4QxFEqmR_$b?^YwrbYG1%RhcN{^ zolw4f*0bG~g~`vIs-c(T&qt*YKa>>M1=i6_5!Ee?-n4hYJx1$lA+gAzOFPWzYUH6+ zyDpEi^zDLcvt>hG8Q}~SN@*!#RIMSOk};w zn`92sE&&-dpSxRUjzfNC&zm7Fb zA;fvopWOl^J}|#BDtK1NRm;V)=MT)9^U%F4>%Mij`fZO{rxP^IgId z#SI2D+JOuxi3fKEpYLj>^Sti+ny_ z4KLp%%_#=Id@{Rv*)q$KGUe)&`URq%+3r;oy1P)gv7yg-Ax6$NVtZwsjOpfYewU9& zi5W}TUM**98;9(Wp7Qpu6^^PgvPih6CiezwC(m5$Pwm$-4nqn0zn~!e^_O;;Q!1R> zMw9GmzGdgUpIH3XJm2o%PGQT5lIKE}KWh-%)+BebmnkJ*a?O%Jbm?M4X1M zG1$xKB!Q)ge_XvbbK-$&syu^=Stzd&h)-dArs27Z$?lro&S|@rn$|zZVH%BgZiQ?d zJ1#%+&5*N#aXMVd+!e^1qg{BqiIim35Q~=P7IReXXZL=#D3Jux(;ovDgp{Z@&5yi* z3!EZdqZ8-t8$PQAR(vQDQ!Z?;u<|`)<)@%{@#tSb|KqFn46~LL{_7&KD=MJ|<~c!K z-rLK+T6ujIS;lD(C-XXnaWPl%7+k6jWh1)x)ZV<{Id^7g7@ zyvtSl+IKVNJOc1^+>De39jV#tm}XM@G4F)0g8k^inNeiBhNgf5x(rAQDFT7HkT_iQ} zAUuh6WpmmXT-I(gAU4C-NnVtfp^J8SJgNfQDcSgcEh*{otE%&VpKwDA%#1PPk*n! z^KNVj{n4KN;{52hf5{3kAE5CAUhQaCBmZXQq#<>mj^76j5`VtGF5$4+miSEl<(rCTBX|v3_ zakP@lUCmgO-qe#bB(s^cKBYrDdBtn(IwCl~a;w|w?b@g8cY! zuOtA>1B8^GE(3e&I$Lq$efq;3rEH&7FVEZ$4rxfGbw*39XT}d1de|7fWuhV3kYc{Z zK<0o~Xwb%|W$nB#l``$P*j&k3*{L9@5I)$%V{Ts0u3eu--N!1Qbdvo<&4?>O?!d=) zeWB*^ue{Lq69UeU-kE?1jy2~68Oe3A7d&Q>>sdQ0@2h?~{KJ6pmr*CqgG}7C@Qp2d2gMkBga6hE1B zpqW}7qL3q61X#_W*=SOPchFt!f@HQPrMSsdo_&v#>Y2tr2jVWx-%ediDjM+B$U76@~Cj z?n>^l|(v($}<_HS6hVvu|sH9QL92wQ<2c|%8(9wJYV4aCodJf|J{u z*litKN$6r{w@lq{QkG_&2RA1}{?6idc&g7lHD!%lZkn|((n|p8I@7>g%SQdZ4vQrINin&!wl3lD2|L>s_O|LkFa60t@ zd6q|sVX|=7u6N}QoWE%cJrbs++$dB{ob=m++8KQ1TO>OEKt5b{1K*&05aPop?K@Om z7leGfjqOD~hVsiDSvjLLpVT>k{YDY9Ve9ofeqf)Up6yb)m&F~f>-ew>au)6iHxk09 z@=(@=rtN@{h})Dg&V1}Q_uMj)O0FI`$AVlj43oStZu8=djpZrZqHz;_0bx@fZRmXW zhU^Q{^Wv99591l;C4INkoKLg61z%qyHTJbzo8jS!FQt7?EE_KZw>>JqEEu9p)Djmi zZJo*RzEm>oTX~AruUd4Yz|pED)&prvZ?hRFYO7jl8Tt}>c9y>QY~pMlAr9ymGU!;z zchK5-&|Fp&kwQ38N9>=!*a+Ao@o=_nG-#?V5nt4RGnoXMswHirxr?t&g&VQI8`qmF zvi36XQ~@chiEQ}v_}^FJ>#EbYo+>%iki(Ct2Eqx~+1Qq{*x9nTkkDzjnHvnWyr@R9 za~*+ttwpgVL^J+%$NDH#YaQ{-nNlYFOG7)advf}&o_}89TDVS7#lBv0>oY~U1{tu- zUHEjP%!k_&{vnqQ8IYE(k*>$25b49MwTcfttfVI|;LBwets=?B(2=MX#Q;vjGN|r0 zB2t3#y6g>?h8r0cG@r5Jp|= zSVYpT4KO&g@*mHL`p4ZK1%3++?}9~7C`v$CW&U|%lPGmPtAt9a;O8*E$KzV_A9>*M zR$Y%=A8{|1n0^R;`pPDNME$fl?Ya3S!Q)BHL#nt1S-5^Mdv~U5z}9V(aS{tWLfUb~ zQ{7K_^3ubm8`QryU3H2@n{t8Y8up3#N@`%;xZ9lP$x5##g#8j}GJkTje zFv~m1=W)ic(^Z(+39KJUn!;BL0TkrJF?F}W7yucygNR_Ph((TRzW^gk{nwjq>;c6b zNOGjoLOS66$GgpVg~GPZyk@VLxK7Y0JI!C&Nlq=N-v5Ml?#{G+2>qUR+HQ^bnHI<; zuec4K#2+vfW^T0^yj%6yIhGt(FYyk`!+-2{_NZst`e=E=`Sg(BvE$r->AFaLaj;rj zLk9%NElGcmdWq+1L9Ga(>M5d7dxmLdt~dxbMoWKucl^0vlaP>J_i%F${;h)0B!Gn7 zLR*xBh7c=JqyiLWAd(`ZKbGYGk*rYHKYQyMprdi3VtrJ?lBXQsy>(3&`jqbFIu;+* zNs!tM2Wi9=cBry%vszS~r znj|aB*a>HB=yMN?Ki<* z6s&qHbWFMpOqc7hJ@CtOS{VP41rGeSKkSjq6g`=SoiUsAS@w(ZJpd=n{I27BoT8}D z@8G2*-9y zxCmO^+##Al(H1$rseKf*-4OWprN5z0nc-J9M+l%wgt@j;qKlBBkvY!ju46z;`3TBX ziNZF^xRDme^;cL37J@u?HK9B)%?p*gI+6{O04d3DqD_8_oPE$ZfZh-@7VugX8Qs-e znra8S&Yjn4kwm1*)4YIIbIrJ`Sr6cMAdL@&g;T}bIMnVwNYV()~=!LNYmyu7T zPB=8CZi$Q=LPYLJw#q$3RbMIC?(9&(PT@<~So;Z|;7F?2IZPG8uM=rnc;3XI-JqhL zg_7NgmDR$S-C-_Yi9;Pp>qM_*hKq8suolupL^(YCDs1S2(Na-GS9emaN=2&A3XwVG z*bA2dWjT@VPS~I|wya~tN0kC9M8mP%q;AJbp3MfGsvS$VSYLZv{QB;Q?<5 z+QLGcDH0y*34UFsIx}m#EM~8;TgM2t?n6WrY&MG)Co#K9DP8GOM+#%dblrGrQQnbS z7@aHWqSmBPGaZ)ii`@Wpc&x4gRprQZr`Rm^EJ4f3FC;gBt3)~FOs9n~^-Q#Z?3s_H zDkJJlo8d`ery{yD?6~IJZ3r}pjct?5P{)H=%Ku+qymmy0+1Bf2n3KgN_gctw_Ot4IawH0aG;Cq znOq<|CKMXsO9{0KLLd#z+|;7lDXcwhL&vhl0=Pm&gk3mLc|-uBZvjPDRn~|Ja!E|P zLPskjG6zve>x2SY5wrQC(26)I`2IZK`hSugYnvi@9-BfgFl}T&9;&awM~o ztui69%C8W&D}-&0PE58nt17*q6cx%ho=_%2KFGcoDNu-~BZ<^dB8*#>VUJ~}KG>Y6`ywdX6~cmq5{M2E7=Tc_qLP^u zJJz}~P3Z_w6`5ez5<8-ufyJ)Q3ReJ#Hr6r*r8yVI+0r~$3ajYa zviOKq%Xvm1$#1bGoTH2jMM!!lB$W>m2DMj`UBz)EsBC1E5=b(WmVvbRTV+9pn`odk zR&LRvsqDBI3u#lxf{JsdEk7k5X5q?6+ATJusHP#b$l{`%Lez%QO=Of(3Ap5hVhT@z zz=}MlQ0%1S&V>}p&4>V(E4H+&Z*2vglD zVr3iEcZzidX}q@59No`WOp8LPIwLA0q!+d3e)t1Ml!ge~BeIR2)ok%flT z{{TYXtm5t!Ij+jGPYSfI8;W?6$n*|7i(m3roCDgk+UhFHC!t8m100f6$1n}0SDVt} zpBuxiSIab(J&!-O%8whJ!j{5Fv~ag`mYh%m&We(k5-^7%j4_Z?WE9n5@ongo@CZ}8 zE~83{4!y`E=&v^)dEqM~7`3X3NP~k&q`_rdgiuwR>2(!uA`5s%K!VuONiwF=RHlMN zQhf;}Fb_&j;7W5`3lp+&V1E*OcxtV>oSGl$L7>IZnf@f85y)-GnBEhuB5!P%fC|T@ zSK50a-r>PQtSQlA#jAMi4huQ2C0l}W(2`315Q zwd&kXFB?(%BIMPfZ=f`Qixd%ZboNn^kRoo@RfI3gF0j&&_HO68EARl(KxzaEUDXvX1KDF7(3bJaXzrH(0Gi0ac0)>jsz6KtEAG4KjE|Cl z)qn|7-byOvKoqfvb5IbPJ&?FiL;{KwWN?8nrv(Hl0TpnGh0>O(rNDBri0-5dv_-3! z-hGxZ8v}x$DwB4p0QD$X=@jH;6dgEElC30!=#vDM6o|Hh8cZP_IxYj1G^96$j73_I z)gU5j!8kIM)go%@yFg?J;Y}6Q$|j3Iz@@s=PXwTFvWf!$0(v7j?+vmkKNvBuw-Bsm zWH84~t3u3?Ky;q!qgtGpJImn=^@W zP)0oqH*D)t3V^v0LbWJSv9_>K$eFpx=NxTA8!1@_(xoS}q7hZHnS|dR5e#<`(KmZT zli4F_XC`F`wpAFW`Box}XiW2(l>C4KTWq3Hh@7)Rl^a$kn|ehnMN&C(J7kVZdm56_ z%COFhGenx@GB<(M2UQ6>DxD3IP0dx$UWly;>WWkb?1v5#h)g`C-4w{1C9LMtS}4-! z`5jdBatx$Z6Cv~}%yAA@i+8mwW6`t|L6lp18619RR8NF4ye^1sM)2pffC)8x3rl+` z#c?Xx+@8^GNN6sQ28$%n1C{Bp{130>;+f#nC6S|o)|T!cQFfY7(Fme? zI?NVIt{d%<@)C<_I~Ry8%UB((S$Jlgw9l$O6J+cde$ji!D!blGs@F(MZqh@o^ol|^ zvr7g&p~C1aT zZ9>ydT@U$bQa@~oQY__4%VgVlRpbDLVk5dWS0I*>gnJD7va$G;oLD!zqC`zEk!xbb zv6fR#REcAUCP@M&DpJdCDVdYifK?6lQo=sR$v;TDjWs%!j^8pq7EU;_3f9MWq$HZ6 zm|d!6X&75-jJLS0Y?fGAk~fslb|zI!hDX8|jfFXf(w91A*o`U~jO$&NEGWessmT_H z_=o=h0K8`MwIXl;1(y-pK5D>}j_65PF}gGy(BP}SE>Oog?o%Bgr8Jr>-j0o-tFl8~ z8A^6VbV+_if)gmaCg!S&60)TNR!5k$$pxVgOu>e|@ z+A5_T3SviWzE@dlV@M$`PF4sVN_1kv0MAM|jX7o&*ygYhmo!u@SZK5o^3st#mT^>7 zX9@AD{RvEr^TKvTXtW0kTFXAc0}UQgnSHvXqREyUwia5aG{#3)WiODHn@DNg>scT+ z7!-|v;TwD74rT{3YBZZ7BYLYldRuokt0ew0s>W-~O)IcfC zJyQ0oJ9tZTX)ZR6g5D-PK+CnCEP7L7&=y=A(G+9O?T~z8u*9)W==`M+E zci9ZB0C1k)g+qWTQz+r1Im{%U5Ex12UUL8(s_!)J6%>wnLWLMWVoQY;YWSq5MmZ^y zCeV-(DW%o&OaMpC7(+@z#tZ0(4iq#CM_eEj2m@+>j*!t<#1u8|jR9F00!onG%38}P zz8(>YUz*4JqwBC1b>!M1e@?lyj6RD0Y_`2$trumD@@@YB z5t+8(>5p~S`YhPBEjtq;R^@{v5s~J+xX=XiWPA1tA6M06$j*VY2Sw)7n;1T#-*fC1 z`~Lt7AMF1CSY(mM9AAe$I=W56Y5AL8o(~pz6O+d@iRAzP9SC3~5-l$YA+E$D*ZfzBq#b!A+{F6yonB_xq zNJ+$Jn>ZbmO(x*9G;CmfPC>pg8GNQ{$jWGPsTbRit?x zQ>Aw$R(biSLNRhWn`=8D`iWO2e+xc$a#di6NtE(mO;pvH+FS~)OQt6)?BG)(VRMqg zwT=a%^cbsSz}m%9L2fGCj`W&Uk0~Nje}VXq>{GHJ-rH3oH06FbhclFt)3UhgQKnSK zgKUcYr^|G*j|jBMYKSfW8?Lro&;6)1~( zp^!F$iMc9TXi`kFa!{>>OH^}53DLB7RoL)Cb6>*DXfzr0lS`c^r>a|>XbZh7BndEH z`#n?+RCiJnk_n@f=C_2Vbx*HFq-9`%b44WaJr&xQ-BRjTLNKTOkd{{~A-_?T!mj>G zeI2}|7Mtj@G>&C}xy2PwN+!x8vG9a5^-O`jl&p`51=!7eL>{UsF^sfEE_x@iC_G?$ zrp$dQ8}@J$$Eq}}XzdjX*KR5#Zr2qJX(@#n8M3W3P*qsc(wt?Wk&PEev?9U>H@bCj zgz~(@(-J^j0x}l?Tp|OSIY-4P4O2)A0cwq=lL%2}%D@``aRXe6J^p&EJbQ7Cq(<9(09P*5Clw^rQO%!1* z=B)$}+U3>47||2L!MzETS1BYXxNay!S83BE(51-S%0o$opi?3=amw*iyi1o z^F2sd?@ANNQlj=UT5}aP4_dQD!AwwF`XZemNjWm^EMsRXzGZT+v7$J>Yd(Kl&8fq3 zvh&nvq(=xv^T<{4avc*UPy2nmAqlHYjA& zl05$aGsww?E>V;J05S9Fe~D?612M0V^p0_`{VZ_2&U`cCKZCaQ^`OXTQ;Sf(sr&1Q2=^CmBafc*+xekzvwURb}Nc8wef}B}E&s8NxDnOC3`O zD1iddUmy$9Y0)+~06o?&osZ^EWLOK*!3#(fxwA{r)q+0DQLtB467Fb5$o4m?RMHlT z>baL^Q|LpU94M;KX&uuXY}!n(ARbjw7F5VQ6+!07Rx%_e)hD~ud0h}kJlOLKUf!Q| zLU;=a`74pkM+!0kb8p=;Z7?{e6Et;Cj!mH@!kkoEr(_eN$&%Lq2uN=wIkrMxhEB=4 zDUF(f5eauRh4B`IG{;<)%b4@-D%8hxMP%c;oV1#EM@mwSGou{&Mv4Gbs@n&pR+Xi% zR$?TzG=;~e(GoV=mlJA4*$J+AQ5$_$f_fRPmjV6S&B!$JueXs(hFGT|kFaL@MK#H- z=#a%>qlIR2BZ=y&N^Gl7Z&g^g)yl{%00{vYkyvIpb9yc~pCmaVoFpNyB@sL+bUPGA z(oZW3Poa@TxLOytYn7OV3Q3)$I?|`Ijx`V&@h7;`QehmXy=EA6F1nJ9gj(wKB{UHG zSkFPtAEwoE72)cRl{X4;Ml77;?1-T*a@WJguF9SpEz4&azRGzdUlbKstbygn_E?iU zbIRP*Xhv28!(3zhmHK9i$6g(U`pnGXe3l8fBZbxLX|EdU_DZ)A>0|B{t|k*S(Th} zs`4DG6P{Ww!^09M<>;f5n@Msz)o3NLo6vEJ2>T$7$eeg98FW{F#H?ew+e*9nDX`m7 zeF8dUO7hI{@ zZ>o|xQWh~F0vAyWl=eVi4@CePMnZsy0IFToBOyR+BEn!Cz?j2-M7G~3K;$SKEMiIFM&$&kSR~!`Q_&5OqP{|D3PJTGGaLB%W$Gn&nnc+qE&iUOy!aSsY;bJ zh@euI@T86x1&kYAY0OA(Rbh!4NQMWd(j-2vQ~PkMzv4n6ka6@x!h3a8Wqg5EV{lJ%`e!$`*7-NlGD% zD&90#%Bu@4%Tg9>jFO`_M@}_jvSirGR5x9w=1FcLRlk~eDnznM*vsKolvz#WuJQ}H zr9|kHO_jPP#B8XNGQ z6=ylwFWNyZcMCsJ(w(Ki{J}QJM-J-A0TopB2b;37p2VTw>xQ zYq%k!z5M#39w5S;`keSl?D5Cu5?_I)=QNmziQmluQDm=vCKc{H(zDyMX4jel5l69({SaBSAfT2q!zqCjpL3l z8fs06p{?_-hq(Zs)Rp#pd`1Uv45uFJ;yxyn1(QL1NC9YkyU03By^{D~qC#n&dKm3MhyTRb-UDU#fFowTM=Nq?@wC zT}4vMb-6jR*sDm$1>CI#Ok9rQDT$1Ano4O#!^*2U;#_M%Vx>Q&>d2OxK~qVF)1a(gH1uV8mSehrkg%<-yE{jj6nh2W^XhV$B?orPDqK8z!p#ANO-A_$?LTLVK zsRZ(@V@*b!v;3h`y)i9ejPejj<7+}lgW6mb86zRc#n>em6PoK3n#KYWSSY8p)YX_{ zzQSu&S=qJ9%U@1IJ2wkKJ%(vGlYz-wP>L3@Fx(j;*kr&*X)-O3iTp%rE*mLeyW%TL zI%4C(%y$Hmsf|)2mD>rW7&KDaREK(%2_{?y<5VVhtgRM*l2&d<`GqVyDU+lI*V96;)oV@N+$ zmLEL1UWeDh=iYk#XByhL@LXth#1%M9K2cj|%k=D^L zDioF6B4Q1wQc50(fSUJ3{3vY|`cVLf4if1x-AW2Y3NPq}lS@62?copr1tu^HP#TY- z%xLH$-~v%~xldlI3rA3bR6L~Ez$_)65w!73s>;JHq8oI$x{6>NBu)=x7T8;)1her$ zC(JGsq0K$iG%6&K%;eiR@>Eykin(1$ganQhP)2?dmP#NP&~9#5{W}+;`Ke)%Kn7s86%2AKGQkPV;G9 zm7J)yl2u@>9*$`$Vtub}OdXrOvbrYo zv9x)2wci1H-`Z}kCx*N_%&ZtA{7y`sSDZlo-vsWi=Z+V}^hhPo%*n`^&XMnM4tu#S zB-u8-9Ev!g(c@v3PbVsqD!)&Wqs$yXwQyC8QIO_sdyCI|?6-38ys^ObSHsi#qSJ8N z*N4{wlWT+Py3y@J!BU+WZ^@q(#~tzlzyWk}0OoQosPKcy(`NqwKMl}0{{S8<=zr}t zKPp=q-i4VipZ+tB!2CB7=}cr7vV|K5+OOzU4Ba;|k&inIA{TjJw{&NaM;y}Z$C`0T zv~&4h7mq18!R-}?huJy*0FcjuQ~v;$H|}5h`cLMX%^yEbe);j|gW5n2)Bgadar&zG z_&K44&&J3t9WEyy?5^PIIhuQk$Hejx$Hl7!DKO#QS;b#4vk*kZdS5DNm9A*~` z{DB)9T}DDj0{gE4JB;phgzj_6aX#eozJxqPJf&bY>v3))%LHLVgbsGH%il?1f< zTeNK_LS>Ax#Y0NHkq~<>OoZ)E}flitrU)B-5+L~Y#esjU-w z=u2BebCTwk_zE(nAk$J~JBnnzlH}fpKnQiKra}TlZS<&ntAvkiviPBM$=fPHDzfrQ32bRS z3F=cK9H32%aGi*bbGE79ZA+7vE(e4tHhm(v=%9pqNKknz$~|Fpr)rb(QOzt6O{n^G zM0=>KSj3KqPYA%_2@4n+Q%XSWprQgQ!h+gF2vB;WVP&+UxJOD4R6s75QaYs+hy~JM z_)yYgN2L%I+}=tZsgD%oJklTlicvtKKFSKf2Y?Wgij)eXiGf4~{L~|Bg2N&zku*_I zPb-6{AUsA6iBbf>Z40?U4*C>h1**Q#9IgKV3Q2D%(<0(2?WekmAr)BRapS19j4tRXwLQJiluU7V2nsI90w@H5N-vexqp3tBp(Pbi zOLMtd3c_QkqLl7$*&w`yK5rCOpebsF9v52U$tn|*CzZ&8Aj?2B1(e9m9#nZ~gJ}Vh z80UIQtnR7j;YtGBf(*7yEG}};hAskLG+YH}3P~vxglPik_~?bLu8D;3pd>1t0_!WA z7(J6f2daZXWNAAoWk3KtEdGURWV+Z^TaT$;9UPPFAW^mDDN9Of6*A0jqiahVOKQqt zR$-TD>ZLVjPAO4qC873_9yNq!P3HQdGln%HwW75a?S$5~jU%G70cOF`WKEP@NCiwX zy!S=Hv?|E1k)b}*C#IA-Fy?egZQ7U2S}83RQeMg#acw+xOUEUzSG>PDA^658T`MVS zjyXf+Xw!#u?PP2g_mI|)a(XYQY5H4qjT#(KNABBicVh14cpn5~w4+EzhGAuTyn_xu8%_oc;Z~2X6}iR ziJg*Rhb9+F)5`>nC+*7ico=?Lz^;qr7&AjYLp<$MMbFyc3@y{77CurjmOiWM8a`8W z__3$Qo!J^zK@r=f^G-954X6JA6A9yJY^hep3e`>^5s0cT0Hmd!a&aqY4HC5 z=4BK4H2(mq^FA2RMh=G?=V=qkayWNw^dFM-KeU)evEtn$PH?y|qJCaQ^z{(dx9_z5 zl{uwt7J?7ZuP4vRz6ke!Wa0R8XP(b+WQ4Su`q@f|K8D7Z6VD4(xDK9UZLQr8VxN(K zad4d>1Qv^>&4&w`J&c}AaXR*Qhcc(oStdTGieOIBe20Gyl8cIR>Yd;#|D^EMxT)XAgidfo}$iH|Gft;R-K{Q8CEadC^Hi*sn2B#w(uFI1bu`ZMx&z3p^?o7&Pu zZRD34_5pRT)bS>{b=ly*YWf>+@HE+?$7C@RnCknf>ib?||J_)ky#F$IDlfA@bE>a)LT4T0VeluTK_dwjje zzc*iHX+bGCrP0LlO~;dG=J+vB;&~kk*kb@_0ddA_Brf@;HJ;0ZGq!quJZd9h^tYAI zkRB;PBsC?=nvR<}z!AYzvKo7>ZwGnkMn@}sdTo~F~DEhbg(&qp`rd&VhQC!nk97~WW>l@|&s^U0%XoSeBdN#yRpR&g6{ z(+u(z6s`i&MHUT#8~np&WaQtFp9W7s?m8v9#49 zorxt~64wZtf=P?V$yYBge<=42t5DJCt&lpU!*-t_+)zf^PP!o0%aQykGM`c=zJgnw zWg=TD-hybbk7x}`W2mOM@UskRijy;xwl|e!;@XzMPEMYQN;?`jjZ>4Cm3iL+tQm4G z7HO*;iw!5tYlSh8$klly+~l3Inu?)}an!e$|ajRq2152xin<7 znp7WcJgO~_lAwolx1Ln);Q=<8AxFbD(aL0!2L(_XM%ySfH!VJFp2%j& z{5xYaIov@LO@_4e-uG^CKR;p5pKpM*I5Dg65IL+HtIlaXJ-d_B{{VqLJecm=*%@P- zT@`#e;(VMu*s-58bqscmrUt=HX!mQrUp4D={QWtS%O-*($ARR>m4@%Trx$HxvjN6<9wPe^o) zYEv+ONpb!B%VxJ43vV|49{zuU3Qx1^&Bsv#rr!9v0e#Z=zO=6`%gSGFRdE=O!-+Z zohw842DsB+2Lvxch5Oh8w{9(=$?NpBL&Vy)jhPl}F!cz?C6h+t!qVA4ipM|*zWr4# zJ6gqtB4^6l2^&1d5xeL){Z}kJOp($@qnFQySf?km+~8zvk~ke7s`&)+uzY9Z7;#2U zmz#@}%!tSCp)3M7*dedoe7E}aT0R}hhJ0MS$YlI(Mk^feCmio_HN^nFVd6bsCtB8B zK2!z=M$r(7*SVk)dA{~amQ^fvq_%Tmam?*K8I6)pEv%APy{+iiIFJ+J$RXSwij30+ z*hP-}9IkZQ+2|&lq!JBFai()W77BJ~d7Fq%johm32iY-%j-h$dc6Y^4=^S>Xm-1~He+d#j zDhoP72sEk9>E%7oZq-tE@Kqv8OsA0IL0QlEjjOV}a;ZG}mqc`O%!F~a+<*%yjJBo` z8aVqJIZ=@2xNxg6&O8J~r>|uy-JM+SG(#cWcnW34*e;XthzFGHpbUI1$=wBGPBvz; zK+=oPplMcphm?#rP<#H~Fv?N&OGS3RreGN*EfZ-e6O;C~59)Nqv zwOUT-ohY)B=k6)e5={^~rUH@5;PMJ;5lV|&0UV$$r7^z3X%-S)8?w8&345rgNx&+r zo$6FVwjSy#z<%Z`d%0HNXme1K8V}GV+#zoTM81bEX|T7WUc-C+B`u#4862h~Xx4#t z$GzT^rnc0)*i!&Y=x*5{P)vcqzmzhXYOEzKaw}*Obj^7~On+sYELI9O$|CFwSXz(l80@k$P&{40@^}C zXInzJW|%EEGAu>zwLp2AWYTfNEOV zbu9@pdea*H-Bn_FMH7fWMD#&RVNL?9Pb$RU3ai~R5?zJab9hnFPeeru#UXdPn&A&# z7BD<0Cxly26hJ}lgrX>I5FJaus*yo)Hn7MO(IwpzN&-_LF)th?6zBA`lhraIBZ8Vx zK1zF{1agtWg&?35HKhajTndxQ0|M5o5j$x~90YANvl<#MvU&=9>s1=`Vai-fav!?; zsOVC+DZVXgF!9lG=#(BnP-G!$0~I*sR(Ma9;H0=(v`9!-WfZMPkGZxKeihbf9!OuN z#7D!qTA1$viiC{WNa1-M(#di~F}@7YAUy|?3KmVpT4Nt-YSBj17GOskcA6=Kg47K@ zrI=!=jc^shuJ&10D3QVv${eLminYkDTU{2fh4Y(%temGhzYmWrt@*e#mj1M}CEzA$ zgLIRhW;7qW+7%+KgM3e+czeaJ(DZ1qU#B)w!E!y47MH!;^MCvREu4QJX>*#%W=1I_ zkmoaSujb?I2Mftd3w*FoQa2yJ`%=u1xA6tU@n8Y+9Y48GFB=@TBy&zD)}z#P4E(P& zdVbrSh+qR-J|56sJz z-J~rasZxF#da{x7Ty~N4Em!t~r(;6~9u_R_gxPbz3@rosN6+8ptH zu|drpJv<(qw0$q#c&Ue-sl*B33O|`A{{ZDlV@vRzknT&`B(Ik9{i!qYF@bqm5%j#T z7ieY%K_*MfBx!V|@i;Z?9b5kZsF8mq82TQI;w-^FSb}+4&}4Kn*FnQgA7DN}aK3A) zJI~5!{vRSC_Acl8B92Yj&yj#PWP~_$_x{K)8;{v0gZsxXzA2kp&Ni2anfNU#C+B8M zKIT-LnAacRZ@3-Yet~+OH^O>miGnlyQ24SWZzZq4DTmpu9Ju{ePX~B;x?Z6Mp{Ua( z{3GUTTqs1_@AIxo!tqatX4Z9UZxIe)4(P^zHG7}-RmYzr{uVh@S8v-TlLj^XOp&(w zUxV1Q_Lml(L^?K=kByIhhdrB1zIz*0`IVF6JyI_mM$?fFEII7rJeWZR!}(k+*!lKY zejL!UwfxtH7d9Y3mm&WE!aiR!?7o=H?GN^SguqxCE1Seov^sgdWS)ccUc58-&Oc4V zl#d%XjpTCrke?KMA5g@S30)klERlwYJ(YN6+xA~t)pd+-+60qE?+rxKT3XD=nq=CkSv9ytYKV`|A#+E4{{SaOTj$;?)}{j* z8Dw%+Ml610-1AFvJ*{X1vh(t5Nc6GXw#xto$89C=prF5n0^~RZr$=Y1ClF8v5!DeG ziQ}PRP?_Wc3coYO8(EnT=Y?nyn1^#}yN9ViswiUI?p3acS0+R__AIG;HkEiZF_eeN z4>nZ!mRR*jL=7F$O+avs?DEG7ypP-#Ti9R4P_Hz#+$$`U%F0#S9jg#WwDOCgA6UK@?Sa;Q`i|CjjuK zL%qEgR+*%eMN=5sl*ZH3$)JUmxlu9{tg#&i@{y3=V3-XqeQo6`?6kJ1ryC2TT9G%) zjUyTWU1TQYNwbSWPTd0>c&lRr`r4Pi2pZHX*bn#@kGHrK)VI{DCnPt2L$1;44ju(ske?a~TtAmQJ)R zw!{YPYFnkJaG2w}jh&2Tq0`Y%7j2e$U#WTibMcc5+Q>W#^FMHB{c z=!HpjA+VVMoB6ILkb6(2!Mqvmmp)8 z0=Ymam^PkK*h>q7111!5?$)cD?DKaY~np!X7NYXi0+!`5RtXSo93cVb~%u|#%WwGQ

hFL>2z(}mni+= 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 z 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 literal 0 HcmV?d00001 diff --git a/portal/client/src/assets/style.js b/portal/client/src/assets/style.js new file mode 100644 index 0000000..414a48c --- /dev/null +++ b/portal/client/src/assets/style.js @@ -0,0 +1,9 @@ +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'react-datetime/css/react-datetime.css'; +import 'react-date-range/dist/styles.css'; +import 'react-date-range/dist/theme/default.css'; +import 'react-input-range/lib/css/index.css'; +import 'react-toastify/dist/ReactToastify.css'; +import 'react-confirm-alert/src/react-confirm-alert.css'; + +import './stylesheets/style.scss'; diff --git a/portal/client/src/assets/stylesheets/style.scss b/portal/client/src/assets/stylesheets/style.scss new file mode 100644 index 0000000..b9f6d8f --- /dev/null +++ b/portal/client/src/assets/stylesheets/style.scss @@ -0,0 +1,1210 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap'); + +$background-color: #7D04B6; +$primary-color: #A200A6; + +.btn-primary { + background: #57B8D7; + border-color: #57B8D7; +} + +.btn-xs { + padding: .25rem .5rem; + font-size: .775rem; + line-height: 1.2; + border-radius: .2rem; + + .btn-block { + display: block; + width: 100% + } + + .btn-block + .btn-block { + margin-top: .5rem + } +} + +@mixin position($position, $top, $right, $bottom, $left) { + position: $position; + top: $top; + left: $left; + right: $right; + bottom: $bottom; +} + +body { + font-family: 'Inter', sans-serif !important; +} + +section.landing-page { + display: flex; + justify-content: center; + align-items: center; +} + +.form-control, .form-control:disabled { + padding: 10px 15px; + height: auto; + font-size: 14px; + background: #E9F6F9; + border: none; +} + +.input-range__slider { + background: #57b8d7; + border: 1px solid #57b8d7; +} + +.input-range__label--value { + font-weight: bold; + font-size: 13px; + color: black; +} + +.input-range__track--active { + background: #57b8d7; +} + +.form-group { + .input-range { + margin-top: 12px; + } + + label { + margin-bottom: 15px; + } +} + +.btn-theme-primary { + background: $primary-color; + border-color: $primary-color; + font-weight: 500; +} + +.btn-normal-primary { + font-weight: 500; + background: #0072b1; + border-color: #0072b1; + color: white; +} + +.authenticated-layout { + height: 100vh; + + .sidebar { + background: #363F48; + width: 300px; + z-index: 1; + padding: 30px; + overflow-y: auto; + height: 100%; + overflow-x: hidden; + + .logo { + margin-bottom: 30px; + } + } + + .main-content { + padding: 15px 15px 15px 315px; + background: #F3F3F3; + height: 100%; + overflow-y: auto; + } +} + +.main-sidebar { + padding-left: 100px; +} + +.main-sidebar { + a { + color: #eee; + font-size: 12px; + padding: 5px 10px; + display: inline-block; + width: 100%; + border-radius: 5px 0px 0px 5px; + + &.active { + background: #a200a6; + pointer-events: none; + } + } +} + +.common-popover .popover-body { + padding: 20px; +} + +.common-popover .popover { + border: none; + max-width: initial; + box-shadow: 2px 2px 15px rgba(0, 0, 0, 0.1); + border-radius: 15px; +} + +.sidebar-links ul { + padding: 0; + list-style: none; +} + +.sidebar-links ul li a { + padding: 12px 30px; + font-size: 15px; + display: inline-block; + color: #17a2b8; + font-weight: 300; + width: 100%; + text-decoration: none !important; +} + +.sidebar-links ul { + margin: 0 -30px; +} + +.sidebar-links ul li a svg { + color: #57B8D7; + width: 30px !important; + margin-right: 10px; + font-size: 18px; +} + +.separator { + border-top: 1px solid #4D5A68; + margin-bottom: 10px; + margin-top: 10px; +} + +.project-selector { + margin-bottom: 20px; +} + +header .navbar { + background: #222C36; + height: 73px; +} + +.no-max-width .popover { + max-width: initial; +} + +main { + padding: 30px; + background: #EAECF1; +} + +.card, .card-body { + border-radius: 15px; + border: none; +} + +.ask-card { + padding: 20px; + border: 1px solid #EDEDED; + border-radius: 15px; + cursor: pointer +} + +.ask-header h3 { + font-size: 16px; + font-weight: 600; + margin: 0 15px 0 0; +} + +.ask-header { + display: flex; + align-items: center; + margin-bottom: 7px; + flex-wrap: wrap; +} + +.ask-header p { + margin: 0; +} + +.primary-badge { + padding: 3px 10px; + font-size: 10px; + background: #57b8d7; + color: #fff; + border-radius: 30px; + margin: 0 5px 0 0; +} + +.secondary-badge { + padding: 3px 10px; + font-size: 10px; + background: #DBE9EE; + color: #819AA1; + border-radius: 30px; + margin: 0; +} + +.role-builder { + //background: #a200a6;color: #fff; + // added for completeness - builder role is a primary badge +} + +.role-helper { + background: #a200a6; + color: #fff; +} + +.ask-body p { + color: #686868; + font-weight: 200; + font-size: 14px; + margin: 0 0 10px; +} + +.acceptance-criteria label { + font-weight: 100; + font-size: 14px; + font-style: italic; + color: #686868; +} + +.acceptance-criteria svg { + margin-left: 5px; + color: #57b8d7; +} + +.ask-footer { + display: flex; + justify-content: space-between; + font-size: 14px; + border-top: 1px solid #ededed; + border-bottom: 1px solid #ededed; +} + +.ask-footer > label { + margin: 0; +} + +.ask-footer-left > label { + margin: 0; + padding: 9px 15px; + border-right: 1px solid #ededed; + font-size: 11px; +} + +.ask-footer-right .btn { + font-size: 14px; + font-weight: 500; + border-left: 1px solid #ededed; + border-radius: 0; + padding: 6px 15px; + height: 35px; + text-decoration: none !important; +} + +.ask-footer-left label:not(:first-child) svg { + margin-right: 10px; +} + +.ask-footer-left label svg { + font-size: 14px; + color: #57b8d7; +} + +.ask-footer-right .btn:first-child { + border: none; +} + +.common-tabs { + padding: 0 20px; + border: 1px solid #ededed; + border-left: none; + border-right: none; + margin-bottom: 20px; +} + +.common-tabs .btn { + background: none !important; + border: 2px solid rgba(0, 0, 0, 0) !important; + color: #686868 !important; + font-size: 11px; + box-shadow: none !important; +} + +.common-tabs .btn svg { + margin-right: 6px; + color: #57b8d7; +} + +.common-tabs .btn.active { + color: #4e555b !important; + border-bottom: 2px solid #57b8d7 !important; + border-radius: 0 !important; +} + +.text-dark { + color: #434343 !important; +} + +.text-success { + color: #66D2AB !important; +} + +.text-danger { + color: #E46D6D !important; +} + +.project-tabs { + display: flex; + flex-direction: column; +} + +.project-tabs .btn { + text-align: left; + color: #686868; + margin-bottom: 20px; + box-shadow: none !important; + + &.active { + color: #000000; + font-weight: bold; + } +} + +.add-project-footer { + display: flex; +} + +.project-form { + min-height: 400px; +} + +.file-upload { + display: flex; + margin-top: 15px; + justify-content: space-between; + align-items: center; +} + +.file-upload span { + color: #57b8d7; + text-decoration: underline; + font-size: 12px; +} + +span.help-text { + color: #57b8d7; + font-size: 12px; + text-decoration: underline; + padding: 0; + cursor: pointer; +} + +.form-group > label { + text-transform: capitalize; +} + +.terms-box { + max-height: 500px; + overflow-y: auto; + margin-bottom: 20px; + background: #f3f3f3; + padding: 20px; + border-radius: 10px; + font-size: 14px; +} + +.terms-box h4 { + font-size: 18px; +} + +.project-stats { + display: flex; + height: 39px; + background: #14212E; + color: #fff; + padding: 0px 20px; + border-radius: 0 30px 30px 0; + text-transform: capitalize; + font-size: 14px; +} + +.selector-group { + display: flex; +} + +.project-status { + margin-right: 15px; + display: flex; + align-items: center; + border-right: 1px solid #4D5A68; + padding-right: 20px; +} + +.project-stats > div:last-child { + display: flex; + align-items: center; +} + + +.user-info-dropdown { + display: flex; + align-items: center; +} + +.wallets-balance { + background: #14212e; + color: #fff; + font-size: 12px; + padding: 7px 15px 5px 5px; + border-radius: 15px; + text-transform: capitalize; +} + +.wallets-balance svg { + color: #57b8d7; +} + +.user-name { + margin-right: 10px; + font-size: 10px; + font-weight: bold; +} + +.radio-btns { + display: flex; +} + +.radio-btns > label { + flex: 1; + border: 1px solid #d5d5d5; + text-align: center; + font-size: 12px; + position: relative; +} + +.radio-btns > label:first-child { + border-radius: 5px 0 0 5px; + border-right: none; +} + +.radio-btns > label:last-child { + border-radius: 0 5px 5px 0; +} + +.radio-btns > label span { + width: 100%; + padding: 7px 15px; + display: inline-block; +} + +.radio-btns > label input:checked ~ span { + background: #17a2b8; + color: #fff; +} + +.radio-btns > label input { + position: absolute; + left: 0; + right: 0; + margin: 0; + top: 0; + bottom: 0; + width: 100%; + height: 100%; + visibility: hidden; +} + +.messages { + display: flex; +} + +.message-users { + flex: 0 0 200px; + display: flex; + flex-direction: column; + border-right: 1px solid #ededed; +} + +.message-users .btn { + background: none; + border: none; + color: #212529; + text-align: left; + border-bottom: 1px solid #ededed; + border-radius: 0; +} + +.ask-more-details .common-tabs { + margin: 0; +} + +.ask-more-details .messages .btn { + font-size: 12px; +} + +.ask-footer-right .dropdown-toggle { + background: none; + color: #333; + border-left: 1px solid #ededed !important; +} + +.ask-status { + border: none !important; + height: auto; + margin: 0 0 0 12px !important; + padding: 2px 25px !important; + line-height: 21px; + border-radius: 30px; + font-weight: 500; + font-size: 12px; +} + +.share-project { + .btn { + color: #57b8d7 + } + + svg { + font-size: 24px; + } + + + .ask-helpers .ask-status { + position: absolute; + right: -25px; + font-size: 12px; + top: 10px; + } +} + +.card-body { + padding: 25px; +} + +.project-details { + h3 { + font-size: 22px; + font-weight: 600; + margin-bottom: 20px; + line-height: 33px; + } + + h1 { + font-size: 28px; + font-weight: 600; + margin-bottom: 20px; + } + + p { + color: #686868; + font-size: 14px; + margin-bottom: 20px; + font-weight: 300; + line-height: 27px; + } +} + +.project-size h3 { + font-size: 14px; + font-weight: 700; + margin: 0; + padding: 10px 0; +} + +.project-size { + display: flex; + border-top: 1px solid #ededed; + border-bottom: 1px solid #ededed; + margin-bottom: 10px; +} + +.project-size ul { + margin: 0; + display: flex; + font-size: 11px; +} + +.project-size ul li { + padding-left: 15px; + padding-right: 15px; + border-left: 1px solid rgba(196, 196, 196, 0.3); + //margin-left: 15px; + display: flex; + align-items: center; +} + +.project-size ul li strong { + margin-left: 6px; +} + +//.table thead th {border: none;background: #f3f3f3;font-weight: 400;font-size: 12px;} + +.table tbody tr:first-child td { + border: none; +} + +.table tbody td { + font-size: 14px; +} + +.height-100 { + height: 100%; +} + +.custom-checkbox-btn { + padding: 0; +} + +.custom-checkbox-btn .form-check-label { + position: relative; + width: 100%; + text-align: center; + text-transform: capitalize; + padding: 10px 40px; + border: 1px solid #57B8D7; + border-radius: 7px; + margin-bottom: 15px; + font-weight: 600; + font-size: 14px; +} + +.custom-checkbox-btn .form-check-label input { + margin: 0; + position: absolute; + left: 0; + right: 0; + width: 100%; + top: 0; + bottom: 0; + height: 100%; + opacity: 0; +} + +.project-card-group { + height: 100%; + display: flex; + flex-direction: column; +} + +.project-card-group .card:nth-child(2) { + flex: 1; +} + +.custom-checkbox-btn.checked .form-check-label { + background: #85C96D; + color: #fff; + border-color: #85C96D; + position: relative +} + +.custom-checkbox-btn.checked .form-check-label svg { + position: absolute; + left: 15px; + top: 13px +} + +.join-project-form .card-body { + display: flex; + flex: 1; + flex-direction: column; +} + +.join-project-form .card-body .project-details { + display: flex; + flex-direction: column; + flex: 1; +} + +@media (min-width: 1600px) { + .container-xl { + max-width: 1540px; + } +} + +.rdw-editor-wrapper { + padding: 7px; + border: 1px solid #eee; + background: #ffffff; + border-radius: 6px; + margin-bottom: 10px; +} + +.rdw-editor-toolbar { + padding: 0; + border: none; + border-bottom: 1px solid #eee; + border-radius: 0; +} + +.project-details .project-reason * { + font-family: 'Inter', sans-serif !important; + font-size: 16px; +} + +.project-details .project-reason p { + margin-bottom: 1em ; +} + + +.project-details .project-reason strong { + font-weight: 600; +} + +.project-reason-video { + min-width: 400px; + + .plyr--video { + flex: 0 0 40%; + width: 100%; + margin-right: 25px; + background: #000; + border-radius: 20px; + } +} + +.common-tabs-content { + padding: 25px; +} + +.ask-more-details + .ask-footer { + margin: 10px -15px -15px; +} + +.ask-card .ask-header { + flex-wrap: wrap; +} + +.ask-card .ask-header h3 { + width: 100%; + margin-bottom: 10px; +} + +.ask-card .ask-footer { + margin-top: 15px; +} + +.ask-card .ask-status { + position: absolute; + right: -15px; + top: 15px; +} + +.ask-card .ask-header { + padding-right: 105px; +} + +.ask-more-details .messages .btn.active { + background: none !important; + color: #57b8d7; + font-weight: 600; + border-color: #57b8d7; + border-width: 2px; + box-shadow: none !important; +} + +.conversation h4 { + flex: 1; + justify-content: space-between; + padding: 8px 15px; + font-size: 18px; + border-bottom: 1px solid #ededed; + align-items: center; + color: #2a2a2a; + font-weight: 200; + margin: 0; +} + +.conversation h4 .btn { + padding: 4px 10px; + background: none; + color: #57b8d7; + border: none; +} + +.messages-list { + height: 330px; + overflow-y: auto; + padding: 15px; +} + +.messages-list .message { + background: #e9f6f9; + border-radius: 10px; + margin-bottom: 15px; + padding: 15px; + font-size: 14px; +} + +.msg-footer { + font-size: 11px; + display: flex; + margin-top: 15px; +} + +.msg-footer > strong { + margin-right: 10px; +} + +form.message-form { + display: flex; + border-top: 1px solid #ededed; +} + +form.message-form .btn { + background: none; + border: none; + padding: 10px 25px; + color: #57b8d7; + font-size: 14px !important; + font-weight: 500; +} + +form.message-form .form-control { + background: none; + border-right: 1px solid #ededed; + border-radius: 0; +} + +.messages-area { + min-height: 436px; +} + +.messages-area { + min-height: 436px; + display: flex; + align-items: center; + justify-content: center; + + h4 { + font-size: 18px; + } +} + +.conversation { + flex: 1; +} + +.messages-list .message { + text-align: left; + max-width: 80%; +} + +.messages-list .message.is-you { + text-align: right; + margin-left: auto; +} + +.messages-list .message.is-you .msg-footer { + justify-content: end; +} + +.ask-helpers .card .ask-status { + position: absolute; + right: -15px; + top: 15px; +} + +.project-status-dash { + margin-bottom: 15px; + + .ask-status { + margin: 0 !important; + } +} + +.project-dash { + cursor: pointer; +} + +.ask-more-details .ask-tabs {display: flex;flex-direction: column;} + +.ask-more-details .common-tabs-content {flex: 1;padding: 0 25px;} + +.ask-more-details {display: flex;} + +.ask-more-details .nav {display: flex;flex-direction: column;margin-right: 15px;} + +.btn-info { + background: #17a2b8; + border-color: #17a2b8; +} + +.ask-more-details .tab-content { + flex: 1; +} + +.messages { + border: 1px solid #ededed; + border-radius: 10px; +} + +.record-badges .badge { + margin-right: 7px; +} + +.ask-header p { + font-weight: 100; + color: #000; +} + +.DraftEditor-editorContainer > * { + color: #333333; +} + +.project-details .project-reason span {color: #686868 !important;} + +.bootstrap-layout { + height: 100vh; + overflow: auto; + display: flex; +} +.bl-sidebar { + flex: 0 0 80px; + max-width: 80px; + transition: all ease-in-out 200ms; +} +.bl-content { + flex: 1 1 100%; +} + +.bl-sidebar-toggler {flex: 0 0 40px; background: none !important;box-shadow: none !important;border: none;width: 40px;display: flex;flex-direction: column;height: 40px;margin-top: auto;margin-bottom: auto;padding: 5px 7px;display: flex;justify-content: center;} + +.bl-header {display: flex;background: #222c36;padding-left: 15px;} + +.bl-sidebar-toggler span {height: 2px;width: 100%;background: #fff;position: relative;transition: all ease-in-out 400ms; transform: rotate(0deg); top: 0} + +.bl-sidebar-toggler span:nth-child(1) {top: -7px;} + +.bl-sidebar-toggler span:nth-child(3) {top: 7px;} + +.bl-sidebar-toggler.opened span:nth-child(1) {transform: rotate(45deg);top: 2px;} + +.bl-sidebar-toggler.opened span:nth-child(3) {transform: rotate(-45deg);top: -2px;} + +.bl-sidebar-toggler.opened span:nth-child(2) {opacity: 0} + +.sidebar-opened .bl-sidebar { + flex: 0 0 300px; + max-width: 300px; +} + +.sidebar-opened { + .bl-content { + max-width: calc(100% - 300px); + } +} + +.sidebar-closed { + .bl-content { + max-width: calc(100% - 80px); + } +} +.sidebar-closed { + header .navbar { + height: 84px; + } +} + +.authenticated-layout .sidebar-closed .sidebar {padding: 18px;} + +.authenticated-layout .sidebar-closed .sidebar .logo {margin: 0 0 20px;} + +.authenticated-layout .sidebar-closed .sidebar .primary-links {padding: 0 7px;} + +.authenticated-layout .sidebar-closed .sidebar-links ul li a svg {margin-right: 25px;} + +.bl-header header { + flex: 1; +} + +.more-info-toggle { + background: none !important; + border: none !important; + padding: 0 18px 0 15px; + box-shadow: none !important; +} + +.bl-content {display: flex;flex-direction: column} + +.bl-content main {flex: 1;overflow-y: auto;} + +@media screen and (max-width: 530px) { + .authenticated-layout .sidebar-closed .sidebar { + padding: 8px; + } + .sidebar-closed .bl-sidebar { + flex: 0 0 60px; + max-width: 60px; + } + .authenticated-layout .sidebar-closed .sidebar .logo { + margin: 0 0 8px; + } + .sidebar-closed { + .bl-content { + max-width: calc(100% - 60px); + } + header .navbar { + height: 62px; + } + } +} + +.popover-inner .project-stats {height: auto;border-radius: 0;flex-direction: column;padding: 20px;} + +.popover-inner .project-stats .project-status {border: none;margin: 0;display: flex;justify-content: center;border-bottom: 0.5px solid rgb(255 255 255 / 14%);padding-bottom: 10px;} + +.navbar-nav .dropdown-menu { + position: absolute; +} + +.navbar-nav .dropdown-menu .user-info-dropdown {flex-direction: column;padding: 15px;min-width: 270px;border-bottom: 1px solid #e4e4e4;margin-bottom: 7px;align-items: flex-start;} + +.navbar-nav .dropdown-menu .user-info-dropdown .user-name {margin-bottom: 7px;} + +.navbar-nav .dropdown-menu .user-info-dropdown .wallets-balance {color: #000;background: #f3f3f3;} + +.selector-group {min-width: 350px;} + +.selector-group > div:first-child {flex: 1;} + +.sidebar-opened .bl-content { + transition: all ease-in-out 400ms; +} +.project-form .project-public-view { + .col-lg-3 {flex: 0 0 33.33%;max-width: 33.33%;} + + .row {flex-wrap: nowrap;} + + .col-lg-9 {flex: 1 !important;max-width: calc(100% - 33.33%);} +} + +div:not(.project-form) .project-public-view { + .col-lg-3 {flex: 0 0 400px;max-width: 400px;} + + .row {flex-wrap: nowrap;} + + .col-lg-9 {flex: 1 !important;max-width: calc(100% - 400px);} +} + +@media screen and (max-width: 1550px) { + .add-new-project .p-5.card { + padding: 0 !important; + } + .project-form .project-reason-video {width: 100%;padding: 0;margin: 0 0 20px;} + .project-form { + .project-size {flex-direction: column;border: none;} + + .project-size ul {flex-direction: column;margin: 10px 0 0;} + + .project-size {margin: 0;} + } + + div:not(.project-form) .project-public-view { + .project-size {flex-direction: column;border: none;} + + .project-size ul {flex-direction: column;margin: 10px 0 0;} + } +} + +@media screen and (max-width: 1420px) { + div:not(.project-form) .project-public-view { + .project-details .project-reason-video { + float: none; + width: 100%; + padding: 0; + margin: 0 0 30px; + min-width: auto; + } + } +} + +@media screen and (max-width: 1199px) { + .filtered-lists .row { + flex-direction: column; + } + .bl-content .project-public-view .row {flex-direction: column;} + + .bl-content .project-public-view .row > div {max-width: 100%;flex: 1 1 100%;} + + div:not(.project-form) .project-public-view .row > .col-lg-3 { + margin-top: 30px; + } +} + +@media screen and (max-width: 991px) { + .selector-group {min-width: auto} +} + +@media screen and (max-width: 767px) { + header .navbar {flex: 1;} + + header .navbar .navbar-nav:first-child {flex: 1;margin-right: 10px;} + + .selector-group {min-width: auto} + header .navbar {flex: 1;} + + header .navbar .navbar-nav:first-child {flex: 1;margin-right: 10px;} + + .sidebar-opened .bl-sidebar { + flex: 0 0 80px; + max-width: 80px; + } + .sidebar-opened .bl-content { + max-width: calc(100% - 80px); + } + .sidebar-width { + width: 240px; + } + .authenticated-layout .sidebar { + position: fixed; + transition: all ease-in-out 400ms; + } + .authenticated-layout .sidebar-closed .sidebar { + width: 80px; + } + .sidebar-opened .bl-sidebar-toggler { + position: fixed; + left: 310px; + z-index: 99; + background: #363F48 !important; + top: 5px; + } +} + +@media screen and (max-width: 450px) { + .bl-header { + padding-left: 7px; + } + header .navbar { + padding: 0 7px; + } +} + +@media screen and (max-width: 530px) { + .sidebar-opened .bl-sidebar { + flex: 0 0 60px; + max-width: 60px; + } + .sidebar-opened .bl-content { + max-width: calc(100% - 60px); + } + .authenticated-layout .sidebar-closed .sidebar { + width: 60px; + } +} + +#exchangeAccounts { + font-size: 0.75rem !important; +} + +.form-control:disabled { + opacity: 0.5; +} + +.dt-range-pick { + display: flex; + background: #e9f6f9; + border-radius: 4px; + + .form-control { + opacity: 1; + } +} diff --git a/portal/client/src/channels/consumer.js b/portal/client/src/channels/consumer.js new file mode 100644 index 0000000..9dc0ced --- /dev/null +++ b/portal/client/src/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. + +import { createConsumer } from '@rails/actioncable'; + +export default createConsumer(); diff --git a/portal/client/src/data/axiosClient.js b/portal/client/src/data/axiosClient.js new file mode 100644 index 0000000..60d715d --- /dev/null +++ b/portal/client/src/data/axiosClient.js @@ -0,0 +1,15 @@ +import axios from 'axios'; + +const axiosObj = () => { + const instance = axios.create({ + headers: { + 'cache-control': 'no-cache', + 'Access-Control-Allow-Origin': '*', + 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content, + }, + }); + + return instance; +}; + +export default axiosObj(); diff --git a/portal/client/src/data/concerns/filterable.js b/portal/client/src/data/concerns/filterable.js new file mode 100644 index 0000000..9172cf0 --- /dev/null +++ b/portal/client/src/data/concerns/filterable.js @@ -0,0 +1,19 @@ +import {action, observable} from 'mobx'; + +import Pagination from '../entities/pagination'; + +const Filterable = { + pagination: null, + + initializeFilterable: action(function(params) { + this.pagination = new Pagination( + { + page: 1, + params, + }, + this + ); + }), +}; + +export default Filterable; diff --git a/portal/client/src/data/concerns/notfications.js b/portal/client/src/data/concerns/notfications.js new file mode 100644 index 0000000..64754b7 --- /dev/null +++ b/portal/client/src/data/concerns/notfications.js @@ -0,0 +1,8 @@ +import { toast } from 'react-toastify'; + +const notification = { + notifySuccess: message => toast(message, { type: 'success' }), + notifyError: message => toast(message, { type: 'error' }), +}; + +export default notification; diff --git a/portal/client/src/data/entities/base_entity.js b/portal/client/src/data/entities/base_entity.js new file mode 100644 index 0000000..d4d7511 --- /dev/null +++ b/portal/client/src/data/entities/base_entity.js @@ -0,0 +1,67 @@ +import { + action, + computed, + extendObservable, + flow, + makeObservable, + observable, +} from 'mobx'; +import { camelCase, isEmpty, map } from 'lodash'; + +import client from '../axiosClient'; +import notification from '../concerns/notfications'; + +class BaseEntity { + /* eslint-disable */ + store; + client; + @observable dirty = false; + @observable json = false; + /* eslint-enable */ + + constructor(value, store) { + makeObservable(this); + + this.store = store; + this.client = client; + this.json = value; + + extendObservable(this, notification); + } + + @computed + get currentUser() { + return this.store.rootStore.userStore.currentUser; + } + + @action + initialize(params) { + this.update(params); + } + + @action + update(params, updateServer) { + map( + Object.keys(params), + function(k) { + this[camelCase(k)] = params[k]; + }.bind(this) + ); + + if (updateServer) this.updateServer(params); + } + + @action + destroy() { + if (isEmpty(this.store)) return; + + this.store.records.splice(this.store.records.indexOf(this), 1); + } + + @flow + *updateServer(params) { + yield this.client.put(this.updateUrl, params); + } +} + +export default BaseEntity; diff --git a/portal/client/src/data/entities/bet.js b/portal/client/src/data/entities/bet.js new file mode 100644 index 0000000..a1cf501 --- /dev/null +++ b/portal/client/src/data/entities/bet.js @@ -0,0 +1,37 @@ +import { action, computed, makeObservable, observable } from 'mobx'; +import BaseEntity from './base_entity'; + +class Bet extends BaseEntity { + /* eslint-disable */ + id; + outcome; + exchange; + exchangeEventName; + exchangeOdds; + tipProviderOdds; + stake; + expectedValue; + marketIdentifier; + exchangeMarketDetails; + betPlacementType; + executedOdds; + evPercent; + eventScheduledTime; + /* eslint-enable */ + + constructor(value, store) { + super(value, store); + + makeObservable(this); + + this.handleConstruction(value); + } + + @action + handleConstruction(value) { + const val = { ...value }; + this.initialize(val); + } +} + +export default Bet; diff --git a/portal/client/src/data/entities/exchange_account.js b/portal/client/src/data/entities/exchange_account.js new file mode 100644 index 0000000..73826ba --- /dev/null +++ b/portal/client/src/data/entities/exchange_account.js @@ -0,0 +1,48 @@ +import { action, computed, flow, makeObservable, observable } from 'mobx'; +import BaseEntity from './base_entity'; +import client from '../axiosClient'; + +class ExchangeAccount extends BaseEntity { + /* eslint-disable */ + id; + exchangeName; + exchangeId; + contactEmail; + contactName; + stakeStrategyName; + sourceTypes; + stakeStrategyConfig; + stakeStrategy; + lastLogTime; + @observable isActive; + @observable canBet; + log; + + @observable accountBalance; + /* eslint-enable */ + + constructor(value, store) { + super(value, store); + + makeObservable(this); + + this.handleConstruction(value); + } + + @action + handleConstruction(value) { + const val = { ...value }; + this.initialize(val); + } + + @flow + *updateExchangeAccount(params) { + this.update(params); + yield client.put('/api/v1/exchange_account.json', { + exchange_account_id: this.id, + ...params, + }); + } +} + +export default ExchangeAccount; diff --git a/portal/client/src/data/entities/pagination.js b/portal/client/src/data/entities/pagination.js new file mode 100644 index 0000000..e8f1668 --- /dev/null +++ b/portal/client/src/data/entities/pagination.js @@ -0,0 +1,58 @@ +import { action, computed, makeObservable, observable } from 'mobx'; +import { isNull } from 'lodash'; + +import BaseEntity from './base_entity'; + +class Pagination extends BaseEntity { + /* eslint-disable */ + @observable count = 0; + @observable page = 1; + @observable pages = null; + @observable prev = null; + @observable next = null; + @observable last = null; + @observable from = null; + @observable to = null; + @observable params = {}; + @observable filterType = 'fetch'; + initialParams = {}; + /* eslint-enable */ + + constructor(value, store) { + super(value, store); + + makeObservable(this); + + this.initialize(value); + this.initialParams = value.params; + } + + @action + fetch() { + this.store.fetch({ ...this.params, page: this.page }); + } + + @action + updateParams(params) { + this.update({ params: { ...this.params, ...params } }); + } + + @action + gotoPage(page) { + this.update({ page }); + this.fetch(); + } + + @action + resetPageAndFetch() { + this.gotoPage(1); + } + + @action + reset() { + this.update({ page: 1, params: this.initialParams }); + this.fetch(); + } +} + +export default Pagination; diff --git a/portal/client/src/data/entities/user.js b/portal/client/src/data/entities/user.js new file mode 100644 index 0000000..7c25cf5 --- /dev/null +++ b/portal/client/src/data/entities/user.js @@ -0,0 +1,51 @@ +import { action, computed, makeObservable, observable } from 'mobx'; +import BaseEntity from './base_entity'; + +class User extends BaseEntity { + /* eslint-disable */ + id; + email; + accountId; + accountName; + @observable status; + @observable firstName; + @observable lastName; + @observable strKey; + @observable fullname; + @observable onboarded = []; + @observable environment = 'production'; + /* eslint-enable */ + + constructor(value, store) { + super(value, store); + + makeObservable(this); + + this.handleConstruction(value); + } + + updateUrl = () => `/api/v1/users/${this.id}.json`; + + @computed + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + + @computed + get asSelectOption() { + return { label: this.fullName, value: this.id }; + } + + @action + logout = () => { + window.location.href = '/public'; + }; + + @action + handleConstruction(value) { + const val = { ...value }; + this.initialize(val); + } +} + +export default User; diff --git a/portal/client/src/data/index.js b/portal/client/src/data/index.js new file mode 100644 index 0000000..bef90dc --- /dev/null +++ b/portal/client/src/data/index.js @@ -0,0 +1,15 @@ +import { makeAutoObservable } from 'mobx'; + +import AppStore from './stores/app_store'; +import UserStore from './stores/user_store'; +import BetStore from './stores/bet_store'; + +export default class RootStore { + constructor() { + this.appStore = new AppStore(this); + this.userStore = new UserStore(this); + this.betStore = new BetStore(this); + + makeAutoObservable(this); + } +} diff --git a/portal/client/src/data/provider.js b/portal/client/src/data/provider.js new file mode 100644 index 0000000..7a83ec7 --- /dev/null +++ b/portal/client/src/data/provider.js @@ -0,0 +1,4 @@ +import React from 'react'; + +export const StoresContext = React.createContext(null); +export const StoreProvider = StoresContext.Provider; diff --git a/portal/client/src/data/store.js b/portal/client/src/data/store.js new file mode 100644 index 0000000..c95d1b2 --- /dev/null +++ b/portal/client/src/data/store.js @@ -0,0 +1,7 @@ +import React from 'react'; + +import { StoresContext } from './provider'; + +const useStore = () => React.useContext(StoresContext); + +export default useStore; diff --git a/portal/client/src/data/stores/app_store.js b/portal/client/src/data/stores/app_store.js new file mode 100644 index 0000000..873c0e3 --- /dev/null +++ b/portal/client/src/data/stores/app_store.js @@ -0,0 +1,126 @@ +import { action, flow, makeObservable, observable } from 'mobx'; +import BaseStore from './base_store'; +import client from '../axiosClient'; +import ExchangeAccount from '../entities/exchange_account'; +import consumer from '../../channels/consumer'; + +class AppStore extends BaseStore { + @observable sidebarOpened = true; + + @observable exchangeAccount; + + @observable currentExchangeAccountId; + + @observable totalWinAmount = 0; + + @observable totalLostAmount = 0; + + @observable totalRiskedAmount = 0; + + @observable totalTips = 0; + + @observable totalTipsSkipped = 0; + + @observable totalTipsProcessing = 0; + + @observable totalTipsExpired = 0; + + @observable totalTipsIgnored = 0; + + @observable totalTipsVoided = 0; + + @observable totalPlacedBets = 0; + + @observable totalPlacedBetsWon = 0; + + @observable totalPlacedBetsLost = 0; + + @observable totalPlacedBetsOpen = 0; + + @observable averageOddsWon = 0; + + @observable averageOddsLost = 0; + + @observable averageOdds = 0; + + + @observable appInitialised = false; + + @observable autoRefreshResults = true; + + @observable runningSince = null; + + @observable exchangeAccounts = []; + + constructor(store) { + super(store, null); + this.consumer = consumer; + makeObservable(this); + this.fetchConfigFromServer(); + } + + @action + asPercentOfTotal(val) { + if (val === 0 || this.totalPlacedBets === 0) return '0%'; + return `${Math.round((val / this.totalPlacedBets) * 100)}%`; + } + + @flow + *fetchConfigFromServer() { + const response = yield client.get('/api/v1/app/configuration.json'); + if (response.data.success) { + this.update(response.data.config); + } + } + + @flow + *fetchSummaryFiguresFromServer() { + const response = yield client.get('/api/v1/app/summary.json', { + params: { + exchange_account_id: + this.currentExchangeAccountId || this.exchangeAccount?.id || '', + }, + }); + if (response.data.success) { + this.updateSummary(response.data.summary); + } + } + + updateSummary(summary) { + console.log('Refreshing dashboard'); + this.update(summary); + const ea = new ExchangeAccount(summary.exchange_account); + this.update({ + exchangeAccount: ea, + appInitialised: true, + currentExchangeAccountId: ea.id, + }); + } + + refresh() { + console.log('Refreshing filtered results'); + this.rootStore.betStore.fetch(this.rootStore.betStore.params); + return null; + } + + startListeningToAccountChannel() { + const obj = this; + this.consumer.subscriptions.create( + { channel: 'ExchangeAccountChannel', id: obj.exchangeAccount.id }, + { + connected() { + console.log('Connected to ExchangeAccountChannel'); + }, + disconnected() { + console.log('Disconnected from ExchangeAccountChannel'); + }, + received(data) { + console.log('Cable received'); + return null; + }, + } + ); + } +} + +export default AppStore; diff --git a/portal/client/src/data/stores/base_store.js b/portal/client/src/data/stores/base_store.js new file mode 100644 index 0000000..f9d7672 --- /dev/null +++ b/portal/client/src/data/stores/base_store.js @@ -0,0 +1,86 @@ +import { + action, + computed, + extendObservable, + makeObservable, + observable, +} from 'mobx'; +import { + camelCase, + filter, + find, + includes, + isEmpty, + map, + uniqBy, +} from 'lodash'; +import notification from '../concerns/notfications'; +import client from '../axiosClient'; + +class BaseStore { + /* eslint-disable */ + rootStore; + client; + Entity; + @observable records = []; + @observable fetching = false; + @observable fetched = false; + /* eslint-enable */ + + constructor(store, entity) { + makeObservable(this); + + this.rootStore = store; + this.client = client; + this.Entity = entity; + + extendObservable(this, notification); + } + + getById = id => this.getByParams({ id }); + + getByParams = params => find(this.records, params); + + getMultipleByParams = params => filter(this.records, params); + + getMultipleById = ids => filter(this.records, r => includes(ids, r.id)); + + @computed + get filteredRecords() { + return this.records; + } + + @computed + get hasRecords() { + return !isEmpty(this.records); + } + + @action + initialize(params) { + this.update(params); + } + + @action + update(params) { + map( + Object.keys(params), + function(k) { + this[camelCase(k)] = params[k]; + }.bind(this) + ); + } + + @action + addRecord(record) { + const obj = new this.Entity(record, this); + + this.records = uniqBy( + [...this.records, new this.Entity(record, this)], + 'id' + ); + + return obj; + } +} + +export default BaseStore; diff --git a/portal/client/src/data/stores/bet_store.js b/portal/client/src/data/stores/bet_store.js new file mode 100644 index 0000000..f1d3b26 --- /dev/null +++ b/portal/client/src/data/stores/bet_store.js @@ -0,0 +1,70 @@ +import { + extendObservable, + flow, + makeObservable, + observable, + override, +} from 'mobx'; + +import { map, orderBy } from 'lodash'; +import BaseStore from './base_store'; +import Bet from '../entities/bet'; +import Filterable from '../concerns/filterable'; +import Pagination from '../entities/pagination'; + +class BetStore extends BaseStore { + @observable params = { + event_name: '', + outcome: '', + created_at: { from: null, to: null }, + }; + + + constructor(store) { + super(store, Bet); + + extendObservable(this, Filterable); + + this.initializeFilterable(this.params); + makeObservable(this); + } + + @override + get filteredRecords() { + return orderBy(this.records, ['createdAt'], ['desc']); + } + + @flow + *fetch(pms) { + this.params = pms; + const response = yield this.client.get('/api/v1/bets.json', { + params: { + ...this.params, + exchange_account_id: this.rootStore.appStore.currentExchangeAccountId, + }, + }); + + if (response.data.summary) { + this.rootStore.appStore.updateSummary(response.data.summary); + } + + if (response.data.bets) { + this.update({ + records: map(response.data.bets, bet => this.addRecord(bet)), + }); + } + if (response.data.pagy) { + const p = new Pagination({ ...response.data.pagy }, this); + this.pagination.count = p.count; + this.pagination.prev = p.prev; + this.pagination.next = p.next; + this.pagination.last = p.last; + this.pagination.from = p.from; + this.pagination.to = p.to; + this.pagination.page = p.page; + this.pagination.pages = p.pages; + } + } +} + +export default BetStore; diff --git a/portal/client/src/data/stores/user_store.js b/portal/client/src/data/stores/user_store.js new file mode 100644 index 0000000..a1f336f --- /dev/null +++ b/portal/client/src/data/stores/user_store.js @@ -0,0 +1,36 @@ +import { computed, flow, makeObservable, observable } from 'mobx'; + +import { isEmpty } from 'lodash'; +import BaseStore from './base_store'; +import User from '../entities/user'; + +class UserStore extends BaseStore { + /* eslint-disable */ + @observable currentUser = {}; + @observable fetchingCurrentUser = true; + /* eslint-enable */ + + constructor(store) { + super(store, User); + + makeObservable(this); + } + + @computed + get userSignedIn() { + return !isEmpty(this.currentUser); + } + + @flow + *fetchCurrentUser() { + const response = yield this.client.get('/api/v1/users/auth.json'); + + if (response.data.success) { + this.update({ currentUser: new User(response.data.user, this) }); + } + + this.fetchingCurrentUser = false; + } +} + +export default UserStore; diff --git a/portal/client/src/entry_point/index.js b/portal/client/src/entry_point/index.js new file mode 100644 index 0000000..1fb4b9e --- /dev/null +++ b/portal/client/src/entry_point/index.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { BrowserRouter, Switch } from 'react-router-dom'; +import { ToastContainer } from 'react-toastify'; +import { StoreProvider } from '../data/provider'; +import RootStore from '../data'; +import AppRoutes from '../routes'; + +const EntryPoint = () => ( + + +

+ + + + +
+ + +); + +export default EntryPoint; diff --git a/portal/client/src/helpers/filter_helpers.js b/portal/client/src/helpers/filter_helpers.js new file mode 100644 index 0000000..950bf1d --- /dev/null +++ b/portal/client/src/helpers/filter_helpers.js @@ -0,0 +1,66 @@ +import React from 'react'; + +import { + DateRange, + InputRangeTag, + ReactSelectTag, + SearchInput, + SelectTag, +} from '../views/shared/form_components'; + +export const renderFilter = (filter, handleChange) => { + switch (filter.type) { + case 'search': + return ; + + case 'select': + return ; + + case 'date-range': + return ; + + case 'input-range': + return ; + + case 'react-select': + return ; + + default: + return null; + } +}; + +export const betFilters = [ + { + name: 'outcome', + type: 'react-select', + placeholder: 'Select...', + label: 'Outcome', + isMulti: true, + options: [ + { label: 'All', value: '' }, + { label: 'Open', value: 'open' }, + { label: 'Lost', value: 'lost' }, + { label: 'Won', value: 'won' }, + { label: 'Processing', value: 'processing' }, + { label: 'Expired', value: 'expired' }, + { label: 'Skipped', value: 'skipped' }, + { label: 'Ignored', value: 'ignored' }, + { label: 'Errored', value: 'errored' }, + { label: 'Voided', value: 'voided' }, + { label: 'Cancelled', value: 'cancelled' }, + ], + }, + { + name: 'created_at', + type: 'date-range', + placeholder: 'Select Date', + label: 'Created Date', + }, + { + name: 'event_name', + type: 'search', + placeholder: 'Search...', + label: 'Event Name', + }, +]; diff --git a/portal/client/src/helpers/shared_helpers.js b/portal/client/src/helpers/shared_helpers.js new file mode 100644 index 0000000..ebd175b --- /dev/null +++ b/portal/client/src/helpers/shared_helpers.js @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; + +export const toSentence = arr => { + if (arr.length > 1) + return `${arr.slice(0, arr.length - 1).join(', ')}, and ${arr.slice(-1)}`; + + return arr[0]; +}; + +export const amountFormatter = number => { + const unitlist = ['', 'K', 'M', 'G']; + let num = number || 0; + + const sign = Math.sign(num); + let unit = 0; + while (Math.abs(num) > 1000) { + unit += 1; + num = Math.floor(Math.abs(num) / 100) / 10; + } + + return sign * num + unitlist[unit]; +}; + +export const useWindowSize = () => { + // Initialize state with undefined width/height so server and client renders match + // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ + const [windowSize, setWindowSize] = useState({ + width: undefined, + height: undefined, + }); + useEffect(() => { + // Handler to call on window resize + function handleResize() { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + // Add event listener + window.addEventListener('resize', handleResize); + // Call handler right away so state gets updated with initial window size + handleResize(); + // Remove event listener on cleanup + return () => window.removeEventListener('resize', handleResize); + }, []); // Empty array ensures that effect is only run on mount + return windowSize; +}; + +export const basicEditorToolbar = [ + ['bold', 'italic', 'underline'], // ['strike'] toggled buttons + ['blockquote', 'code-block'], + ['link', 'image'], + ['emoji'], + // [{ header: 1 }, { header: 2 }], // custom button values + [{ list: 'ordered' }, { list: 'bullet' }], + // [{ script: 'sub' }, { script: 'super' }], // superscript/subscript + // [{ indent: '-1' }, { indent: '+1' }], // outdent/indent + // [{ direction: 'rtl' }], // text direction + + // [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown + [{ header: [1, 2, 3, 4, 5, 6, false] }], + + [{ color: [] }, { background: [] }], // dropdown with defaults from theme + [{ font: [] }], + [{ align: [] }], + + // ['clean'], +]; + +export const commentEditorToolbar = [ + ['bold', 'italic', 'underline'], + [{ font: [] }], + ['emoji'], +]; diff --git a/portal/client/src/helpers/sidebar_helpers.js b/portal/client/src/helpers/sidebar_helpers.js new file mode 100644 index 0000000..115370d --- /dev/null +++ b/portal/client/src/helpers/sidebar_helpers.js @@ -0,0 +1,61 @@ +export const primaryLinks = [ + { + linkName: 'dashboard', + iconName: 'tachometer-alt', + }, +]; + +const controlStyle = { + paddingLeft: 15, + backgroundColor: '#6D7E8F', + color: '#fff', + boxShadow: 'none', + border: 'none', +}; + +const commonStyles = { + placeholder: provided => ({ + ...provided, + color: '#ffffff', + }), + input: (provided, st) => ({ + ...provided, + color: '#fff', + }), + singleValue: provided => ({ + ...provided, + color: '#ffffff', + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected ? '#57B8D7' : '#1f252b', + color: '#ffffff', + paddingLeft: 20, + fontSize: 14, + }), + menu: provided => ({ + ...provided, + backgroundColor: '#1f252b', + borderRadius: 15, + overflow: 'hidden', + padding: 0, + }), +}; + +export const customStylesAlt = { + ...commonStyles, + control: (provided, st) => ({ + ...provided, + ...controlStyle, + borderRadius: '30px 0px 0px 30px', + }), +}; + +export const customStyles = { + ...commonStyles, + control: (provided, st) => ({ + ...provided, + ...controlStyle, + borderRadius: 30, + }), +}; diff --git a/portal/client/src/routes/AuthenticatedRoute.jsx b/portal/client/src/routes/AuthenticatedRoute.jsx new file mode 100644 index 0000000..aa1ee0f --- /dev/null +++ b/portal/client/src/routes/AuthenticatedRoute.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { Route } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; + +import useStore from '../data/store'; + +export const KEYCLOAK_AUTH_URL = '/users/auth/keycloakopenid'; + +const AuthenticatedRoute = ({ component, path }) => { + const { userStore } = useStore(); + + const Component = component; + + const HandleAuth = () => { + localStorage.setItem( + 'redirectURL', + `${window.location.pathname}${window.location.search}` + ); + + window.location.href = KEYCLOAK_AUTH_URL; + + return null; + }; + + return ( + + ); +}; + +AuthenticatedRoute.propTypes = { + component: PropTypes.func, + path: PropTypes.string, +}; + +export default observer(AuthenticatedRoute); diff --git a/portal/client/src/routes/index.jsx b/portal/client/src/routes/index.jsx new file mode 100644 index 0000000..8157dac --- /dev/null +++ b/portal/client/src/routes/index.jsx @@ -0,0 +1,36 @@ +import React, { useEffect } from 'react'; + +import { Route } from 'react-router-dom'; +import { isEmpty, map } from 'lodash'; +import { observer } from 'mobx-react'; + +import AuthenticatedRoute from './AuthenticatedRoute'; + +import * as allRoutes from './routes'; +import useStore from '../data/store'; + +const AppRoutes = () => { + const { userStore, appStore } = useStore(); + + useEffect(() => { + userStore.fetchCurrentUser().then(() => { + console.log('Fetched user'); + if (userStore.userSignedIn) appStore.startListeningToAccountChannel(); + }); + }, []); + + if (userStore.fetchingCurrentUser) return null; + + return ( + <> + {map(allRoutes.normalRoutes, (r, i) => ( + + ))} + {map(allRoutes.protectedRoutes, (r, i) => ( + + ))} + + ); +}; + +export default observer(AppRoutes); diff --git a/portal/client/src/routes/routes.js b/portal/client/src/routes/routes.js new file mode 100644 index 0000000..7f65bea --- /dev/null +++ b/portal/client/src/routes/routes.js @@ -0,0 +1,5 @@ +import Dashboard from '../views/dashboard'; +import Public from '../views/public'; + +export const normalRoutes = [{ route: '/', component: Public }]; +export const protectedRoutes = [{ route: '/dashboard', component: Dashboard }]; diff --git a/portal/client/src/t.js b/portal/client/src/t.js new file mode 100644 index 0000000..b9c6445 --- /dev/null +++ b/portal/client/src/t.js @@ -0,0 +1,44 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { isObject, map } from 'lodash'; + +import en from '../../config/locales/en.yml'; + +const checkForDynamicValues = data => { + let updatedData = data; + + if (isObject(data)) { + map(Object.keys(updatedData), k => { + updatedData[k] = checkForDynamicValues(updatedData[k]); + }); + + return updatedData; + } + + updatedData ||= ''; + updatedData = updatedData.replaceAll('%', ''); + updatedData = updatedData.replaceAll('{', '{{'); + updatedData = updatedData.replaceAll('}', '}}'); + + return updatedData; +}; + +const createLocaleData = (data, key) => ({ + translation: checkForDynamicValues(data[key]), +}); + +const resources = { + en: createLocaleData(en, 'en'), +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: 'en', + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export default i18n.t; diff --git a/portal/client/src/utils/Notifier.js b/portal/client/src/utils/Notifier.js new file mode 100644 index 0000000..6c4853d --- /dev/null +++ b/portal/client/src/utils/Notifier.js @@ -0,0 +1,18 @@ +import { store } from 'react-notifications-component'; + +const Notifier = (type, title, message) => + store.addNotification({ + title, + message, + type, + insert: 'top', + container: 'top-right', + animationIn: ['animate__animated', 'animate__fadeIn'], + animationOut: ['animate__animated', 'animate__fadeOut'], + dismiss: { + duration: 5000, + onScreen: true, + }, + }); + +export default Notifier; diff --git a/portal/client/src/utils/needfulMethods.js b/portal/client/src/utils/needfulMethods.js new file mode 100644 index 0000000..52ca112 --- /dev/null +++ b/portal/client/src/utils/needfulMethods.js @@ -0,0 +1,2 @@ +export const getInitials = u => `${u.firstName[0]}${u.lastName[0]}`; +export const getFullName = u => `${u.firstName} ${u.lastName}`; diff --git a/portal/client/src/views/bets/_bets_list.js b/portal/client/src/views/bets/_bets_list.js new file mode 100644 index 0000000..7e22285 --- /dev/null +++ b/portal/client/src/views/bets/_bets_list.js @@ -0,0 +1,84 @@ +import { isEmpty, map } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import ReactTimeAgo from 'react-time-ago'; +import { Table } from 'reactstrap'; +import moment from 'moment'; + +const BetsList = ({ records }) => { + if (isEmpty(records)) + return

No bets

; + + return ( + + + + + + + + + + + + + + + + + + {map(records, (record, i) => ( + + + + + + + + + + + + + + ))} + +
EventTip Market / Exchange Market InfoTip EVTip OddsExchange OddsExecuted OddsStakeProjected ProfitOutcomeUpdatedType
+ {record.exchangeEventName} +
+ {!isEmpty(record.eventScheduledTime) && ( + <> + + {moment(new Date(record.eventScheduledTime)).format( + 'DD.MMM.YYYY HH:mm:ss' + )} + +
+ + )} + + + {record.exchange}|{record.id} + + +
+ {' '} + {record.marketIdentifier}
+ + {record.exchangeMarketDetails} + +
{record.evPercent} {record.tipProviderOdds} {record.exchangeOdds} {record.executedOdds} {record.stake} {record.expectedValue} + + {record.outcome}{' '} + + + + {record.betPlacementType}
+ ); +}; +BetsList.propTypes = { + records: PropTypes.instanceOf(Array).isRequired, +}; + +export default observer(BetsList); diff --git a/portal/client/src/views/bets/index.js b/portal/client/src/views/bets/index.js new file mode 100644 index 0000000..9b78f0a --- /dev/null +++ b/portal/client/src/views/bets/index.js @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from 'react'; + +import { observer } from 'mobx-react'; +import { Card, CardBody } from 'reactstrap'; + +import PropTypes from 'prop-types'; +import FilteredList from '../modules/filtered_list'; +import useStore from '../../data/store'; +import { betFilters } from '../../helpers/filter_helpers'; +import BetsList from './_bets_list'; + +const BetsIndex = ({ match }) => { + const { betStore } = useStore(); + + useEffect(() => { + if (betStore.fetched) { + console.log('Bets fetched'); + } + }, [betStore.fetched]); + + return ( + + + + + + ); +}; + +BetsIndex.propTypes = { + match: PropTypes.instanceOf(Object).isRequired, +}; + +export default observer(BetsIndex); diff --git a/portal/client/src/views/dashboard/index.js b/portal/client/src/views/dashboard/index.js new file mode 100644 index 0000000..880323a --- /dev/null +++ b/portal/client/src/views/dashboard/index.js @@ -0,0 +1,33 @@ +import React, { useEffect } from 'react'; + +import { Card, CardBody, Col, Row } from 'reactstrap'; +import { observer } from 'mobx-react'; +import ReactTimeAgo from 'react-time-ago'; +import { map } from 'lodash'; +import PublicLayout from '../layouts/public_layout'; +import BetsIndex from '../bets'; +import useStore from '../../data/store'; +import CountdownRefresh from '../modules/countdown_refresh'; +import { SelectTag } from '../shared/form_components'; +import AuthenticatedLayout from '../layouts/authenticate'; + +const Dashboard = () => { + const { appStore } = useStore(); + const renderBody = () => ( + <> + {!appStore.appInitialised &&
Refreshing...
} + {appStore.appInitialised && ( +
+ + + + + +
+ )} + + ); + return {renderBody()}; +}; + +export default observer(Dashboard); diff --git a/portal/client/src/views/layouts/authenticate/_header.js b/portal/client/src/views/layouts/authenticate/_header.js new file mode 100644 index 0000000..de66540 --- /dev/null +++ b/portal/client/src/views/layouts/authenticate/_header.js @@ -0,0 +1,96 @@ +import React from 'react'; + +import { observer } from 'mobx-react'; +import { + Navbar, + Nav, + UncontrolledDropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + Button, + Row, + Col, +} from 'reactstrap'; +import { Initial } from 'react-initial'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useHistory } from 'react-router-dom'; +import { isEmpty } from 'lodash'; +import useStore from '../../../data/store'; +import { useWindowSize } from '../../../helpers/shared_helpers'; + +const _header = () => { + const { appStore } = useStore(); + + return ( +
+ +
+ Results Dashboard +
+
+ +
+ Total Risked: £{appStore.totalRiskedAmount} +
+
Total Won: £{appStore.totalWinAmount}
+
Total Lost: £{appStore.totalLostAmount}
+
+ + Total Profit:{' '} + = 0 + ? 'text-success' + : 'text-danger' + } + > + £ + {Math.round(appStore.totalWinAmount - appStore.totalLostAmount)} + + +
+ + +
Total Tips: {appStore.totalTips}
+
Total Tips Executed: {appStore.totalPlacedBets}
+
Total Tips Skipped: {appStore.totalTipsSkipped}
+ + +
+ Total Bets Placed: {appStore.totalPlacedBets} +
+
Total Tips Expired: {appStore.totalTipsExpired}
+
Total Tips Ignored: {appStore.totalTipsIgnored}
+
Total Tips Voided: {appStore.totalTipsVoided}
+ + +
Total Bets Open: {appStore.totalPlacedBetsOpen}
+
+ Total Bets Won: {appStore.totalPlacedBetsWon} + + {' '} + ({appStore.asPercentOfTotal(appStore.totalPlacedBetsWon)}) + +
+
+ Average Odds Won at: {appStore.averageOddsWon} +
+
+ Total Bets Lost: {appStore.totalPlacedBetsLost} + + {' '} + ({appStore.asPercentOfTotal(appStore.totalPlacedBetsLost)}) + +
+
+ Average Odds Lost at: {appStore.averageOddsLost} +
+ +
+
+
+ ); +}; + +export default observer(_header); diff --git a/portal/client/src/views/layouts/authenticate/_sidebar.js b/portal/client/src/views/layouts/authenticate/_sidebar.js new file mode 100644 index 0000000..46f5e03 --- /dev/null +++ b/portal/client/src/views/layouts/authenticate/_sidebar.js @@ -0,0 +1,151 @@ +import React, { useEffect } from 'react'; + +import { Link } from 'react-router-dom'; +import { observer } from 'mobx-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import ReactTimeAgo from 'react-time-ago'; +import { map } from 'lodash'; +import moment from 'moment'; +import { Form, FormGroup, Input, Label } from 'reactstrap'; +import LOGO from '../../../assets/images/logo.svg'; +import useStore from '../../../data/store'; +import { SelectTag } from '../../shared/form_components'; +import CountdownRefresh from '../../modules/countdown_refresh'; +import EditableHash from '../../modules/editable_hash'; +import Toggler from '../../modules/toggler.js'; + +const _sidebar = () => { + const { appStore } = useStore(); + + useEffect(() => { + appStore.refresh(); + }, []); + + const changeExchangeAccount = e => { + appStore.update({ currentExchangeAccountId: e.target.value }); + appStore.refresh(); + }; + + const exchangeAccountsAsOptions = () => ({ + name: 'exchangeAccounts', + value: appStore.currentExchangeAccountId, + type: 'select', + placeholder: 'Select...', + label: 'Available Accounts', + options: map(appStore.exchangeAccounts, r => ({ label: r, value: r })), + }); + + return ( + <> + {!appStore.appInitialised &&
Refreshing...
} + {appStore.appInitialised && ( +
+
+
+ + logo + +
+
+
+
+
+
Available Accounts:
+ +
+
+ { + appStore.exchangeAccount.updateExchangeAccount({ + isActive: !appStore.exchangeAccount.isActive, + }); + }} + /> +
+
+ { + appStore.exchangeAccount.updateExchangeAccount({ + canBet: !appStore.exchangeAccount.canBet, + }); + }} + /> +
+
+ +
+
+
+
+ Account Balance: £{appStore.exchangeAccount.accountBalance} +
+
+ Running since:{' '} + +
+
+ Feed type: {appStore.exchangeAccount.sourceTypes} +
+
+ Stake Strategy: {appStore.exchangeAccount.stakeStrategyName} +
+
+ Strategy Config: + { + alert('this'); + }} + /> +
+
+
+ + Last log was at{' '} + {moment( + new Date(appStore.exchangeAccount.lastLogTime) + ).format('DD.MMM.YYYY HH:mm:ss')} + +
+ + {appStore.exchangeAccount.log} + +
+
+
+
+ +
+
+ +
+
+
+ )} + ; + + ); +}; + +export default observer(_sidebar); diff --git a/portal/client/src/views/layouts/authenticate/index.js b/portal/client/src/views/layouts/authenticate/index.js new file mode 100644 index 0000000..787f8fe --- /dev/null +++ b/portal/client/src/views/layouts/authenticate/index.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; + +import { Button } from 'reactstrap'; +import classnames from 'classnames'; +import Header from './_header'; +import Sidebar from './_sidebar'; +import useStore from '../../../data/store'; + +const AuthenticatedLayout = ({ children, loading }) => { + const { appStore } = useStore(); + + return ( +
+
+
+ +
+
+
+
{loading ? 'Loading' : children}
+
+
+
+ ); +}; + +AuthenticatedLayout.defaultProps = { + loading: false, +}; + +AuthenticatedLayout.propTypes = { + children: PropTypes.node, + loading: PropTypes.bool, +}; + +export default observer(AuthenticatedLayout); diff --git a/portal/client/src/views/layouts/public_layout.js b/portal/client/src/views/layouts/public_layout.js new file mode 100644 index 0000000..6914a1c --- /dev/null +++ b/portal/client/src/views/layouts/public_layout.js @@ -0,0 +1,40 @@ +import React from 'react'; + +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; + +import { Container, Navbar, NavbarBrand } from 'reactstrap'; +import LOGO from '../../assets/images/logo.svg'; +import useStore from '../../data/store'; + +const PublicLayout = ({ children }) => { + const { userStore } = useStore(); + return ( +
+ + + logo + + {userStore.userSignedIn && ( + + )} + + + {children} + +
+ ); +}; + +PublicLayout.propTypes = { + children: PropTypes.node, +}; + +export default observer(PublicLayout); diff --git a/portal/client/src/views/modules/countdown_refresh/index.js b/portal/client/src/views/modules/countdown_refresh/index.js new file mode 100644 index 0000000..0639eab --- /dev/null +++ b/portal/client/src/views/modules/countdown_refresh/index.js @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; + +import { observer } from 'mobx-react'; +import { Form, FormGroup, Input, Label } from 'reactstrap'; +import useStore from '../../../data/store'; +import Toggler from '../toggler.js'; + +const CountdownRefresh = () => { + const { appStore } = useStore(); + const refreshTime = 1; + const [countdownToNextRefresh, setCountdownToNextRefresh] = useState( + refreshTime + ); + const [enableCountdown, setEnableCountdown] = useState( + appStore.autoRefreshResults + ); + const [intervalId, setIntervalId] = useState(); + let counter = refreshTime; + const startCountdown = () => { + setIntervalId( + setInterval(() => { + counter -= 1; + if (counter === 0) { + counter = refreshTime; + appStore.refresh(); + } + setCountdownToNextRefresh(counter); + }, 1000) + ); + }; + + useEffect(() => { + if (enableCountdown) { + startCountdown(); + } + }, [enableCountdown]); + + const handleChange = () => { + setEnableCountdown(!enableCountdown); + console.log('Stopping counter refresh'); + clearInterval(intervalId); + }; + + return ( + + ); +}; + +export default observer(CountdownRefresh); diff --git a/portal/client/src/views/modules/editable_hash/index.js b/portal/client/src/views/modules/editable_hash/index.js new file mode 100644 index 0000000..5b7cdf6 --- /dev/null +++ b/portal/client/src/views/modules/editable_hash/index.js @@ -0,0 +1,67 @@ +import React, { useEffect } from 'react'; + +import { observer } from 'mobx-react'; + +import { forEach, includes, isEmpty, isNull } from 'lodash'; +import { Button } from 'reactstrap'; +import PropTypes from 'prop-types'; + +const EditableHash = ({ source, updateFunction }) => { + const editableValues = []; + // + // const editableValues = [ + // 'max_ev', + // 'min_ev', + // 'odds_margin', + // 'max_odds_to_bet', + // 'min_odds_to_bet', + // 'stake_sizing', + // 'max_bankroll_per_bet', + // ]; + + const showOnlyEditable = false; + const divOfValues = hash => { + if (Object.keys(hash).length === 0 || typeof hash === 'string') { + return hash; + } + const x = []; + // forEach(Object.keys(hash), key => { + // if (includes(editableValues, key)){ + // const v = divOfValues(hash[key]) + // const vEntry = + // x.push( + //
{key}: {vEntry}
+ // ); + // } + // }); + + forEach(Object.keys(hash), key => { + const v = divOfValues(hash[key]); + const vEntry = includes(editableValues, key) ? ( + + ) : ( + v + ); + const hideThis = showOnlyEditable && !includes(editableValues, key) + + if (!hideThis) { + x.push( +
+ {key}: {vEntry} +
+ ); + } + }); + return
{x}
; + }; + return ( +
{divOfValues(source)}
+ ); +}; + +EditableHash.propTypes = { + source: PropTypes.instanceOf(Object).isRequired, + updateFunction: PropTypes.func.isRequired, +}; + +export default observer(EditableHash); diff --git a/portal/client/src/views/modules/filtered_list/_filters.js b/portal/client/src/views/modules/filtered_list/_filters.js new file mode 100644 index 0000000..c374ee6 --- /dev/null +++ b/portal/client/src/views/modules/filtered_list/_filters.js @@ -0,0 +1,55 @@ +import React, { Fragment, useState } from 'react'; + +import PropTypes from 'prop-types'; +import { isBoolean, map } from 'lodash'; +import { Col, FormGroup, Label, Row } from 'reactstrap'; +import { observer } from 'mobx-react'; + +import { renderFilter } from '../../../helpers/filter_helpers'; + +const Filters = ({ pagination, filters }) => { + const [timeout, setTimeOut] = useState(null); + + const handleChange = e => { + let timer = null; + + setTimeOut(clearTimeout(timeout)); + + pagination.update({ + params: { ...pagination.params, [e.target.name]: e.target.value }, + }); + + timer = setTimeout(() => { + pagination.resetPageAndFetch(); + }, 1000); + + setTimeOut(timer); + }; + + return ( + + {map(filters, (filter, i) => ( + + {isBoolean(filter.show) && !filter.show ? null : ( + + + + {renderFilter( + { ...filter, value: pagination.params[filter.name] }, + handleChange + )} + + + )} + + ))} + + ); +}; + +Filters.propTypes = { + pagination: PropTypes.instanceOf(Object).isRequired, + filters: PropTypes.instanceOf(Array).isRequired, +}; + +export default observer(Filters); diff --git a/portal/client/src/views/modules/filtered_list/index.js b/portal/client/src/views/modules/filtered_list/index.js new file mode 100644 index 0000000..b48b128 --- /dev/null +++ b/portal/client/src/views/modules/filtered_list/index.js @@ -0,0 +1,111 @@ +import React, { useEffect } from 'react'; + +import { observer } from 'mobx-react'; + +import { isEmpty, isNull } from 'lodash'; +import { Button } from 'reactstrap'; +import PropTypes from 'prop-types'; +import Filters from './_filters'; + +const FilteredList = ({ + store, + listComponent, + gridComponent, + filters, + changeTrigger, + listClassName, + recordClassName, +}) => { + const ListComponent = listComponent; + // const Component = gridComponent; + + useEffect(() => { + store.pagination.fetch(); + }, [changeTrigger]); + + return ( +
+ + + {isEmpty(store.filteredRecords) && ( + <> +
+ Nothing here. + + )} + {!isEmpty(store.filteredRecords) && ( +
+
+
+ +
+
+
+ )} +
+ ); +}; + +FilteredList.defaultProps = { + listClassName: '', + recordClassName: '', +}; + +FilteredList.propTypes = { + store: PropTypes.instanceOf(Object).isRequired, + listComponent: PropTypes.func.isRequired, + gridComponent: PropTypes.func, + filters: PropTypes.instanceOf(Array).isRequired, + changeTrigger: PropTypes.string.isRequired, + listClassName: PropTypes.string, + recordClassName: PropTypes.string, +}; + +export default observer(FilteredList); diff --git a/portal/client/src/views/modules/toggler.js/index.js b/portal/client/src/views/modules/toggler.js/index.js new file mode 100644 index 0000000..bca7e69 --- /dev/null +++ b/portal/client/src/views/modules/toggler.js/index.js @@ -0,0 +1,58 @@ +import React from 'react'; + +import { observer } from 'mobx-react'; +import { Form, FormGroup, Input, Label } from 'reactstrap'; +import PropTypes from 'prop-types'; + +const Toggler = ({ + prompt, + disabled, + state, + onText, + offText, + onChangeHandler, +}) => { + const handleChange = () => { + if (prompt) { + // eslint-disable-next-line no-restricted-globals + if (confirm('Are you sure?') === false) return false; + } + onChangeHandler(); + }; + + return ( +
+ + + + +
+ ); +}; + +Toggler.defaultProps = { + disabled: false, + prompt: false, +}; + +Toggler.propTypes = { + disabled: PropTypes.bool, + prompt: PropTypes.bool, + state: PropTypes.bool.isRequired, + onText: PropTypes.string.isRequired, + offText: PropTypes.string.isRequired, + onChangeHandler: PropTypes.func.isRequired, +}; +export default observer(Toggler); diff --git a/portal/client/src/views/public/index.js b/portal/client/src/views/public/index.js new file mode 100644 index 0000000..ee2c1ba --- /dev/null +++ b/portal/client/src/views/public/index.js @@ -0,0 +1,47 @@ +import React from 'react'; + +import { Button } from 'reactstrap'; +import { observer } from 'mobx-react'; +import { Link } from 'react-router-dom'; +import useStore from '../../data/store'; +import { KEYCLOAK_AUTH_URL } from '../../routes/AuthenticatedRoute'; +import PublicLayout from '../layouts/public_layout'; + +const Public = () => { + const { userStore } = useStore(); + + const handleSignInSignUp = () => { + localStorage.setItem( + 'redirectURL', + `${window.location.pathname}${window.location.search}` + ); + + window.location.href = KEYCLOAK_AUTH_URL; + }; + + const handleSignOut = () => { + window.location.href = '/sign_out'; + }; + + const renderBody = () => ( +
+
Welcome to BetBeast.
+
+ {userStore.userSignedIn && ( +
+ + Go to Dashboard + + +
+ )} + {!userStore.userSignedIn && ( + + )} +
+
+ ); + return {renderBody()}; +}; + +export default observer(Public); diff --git a/portal/client/src/views/shared/form_components.js b/portal/client/src/views/shared/form_components.js new file mode 100644 index 0000000..b1cbdbb --- /dev/null +++ b/portal/client/src/views/shared/form_components.js @@ -0,0 +1,182 @@ +import React, { useState } from 'react'; + +import { Button, Input, Popover, PopoverBody } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { find, filter, isDate, isObject, map, includes } from 'lodash'; +import { DateRangePicker } from 'react-date-range'; +import InputRange from 'react-input-range'; +import Select from 'react-select'; +import moment from 'moment'; + +export const SearchInput = ({ input, handleChange }) => ( + +); + +export const SelectTag = ({ input, handleChange }) => ( + + {map(input.options, (opt, i) => ( + + ))} + +); + +export const DateRange = ({ input, handleChange }) => { + const [isOpen, setIsOpen] = useState(false); + + const togglePopover = () => setIsOpen(!isOpen); + + const dateLabel = () => { + if (!isDate(input.value.from) && !isDate(input.value.to)) + return input.placeholder; + + const startDate = isDate(input.value.from) + ? moment(input.value.from).format('MMM/DD/YYYY') + : 'NA'; + + const endDate = isDate(input.value.to) + ? moment(input.value.to).format('MMM/DD/YYYY') + : 'NA'; + + return `${startDate} - ${endDate}`; + }; + + return ( +
+ + {isDate(input.value.from) && ( + + )} + + + + handleChange({ + target: { + name: input.name, + value: { + from: d[input.name].startDate, + to: d[input.name].endDate, + }, + }, + }) + } + /> + + +
+ ); +}; + +export const InputRangeTag = ({ input, handleChange }) => ( + + handleChange({ + target: { + name: input.name, + value, + }, + }) + } + /> +); + +export const ReactSelectTag = ({ input, handleChange }) => ( +