init push - laying out the project

This commit is contained in:
Mike Sutton 2022-11-12 02:27:46 +01:00
commit 14e163a1a5
183 changed files with 20069 additions and 0 deletions

103
.drone.yml Normal file
View File

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

44
.gitignore vendored Normal file
View File

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

15
consumer/Gemfile Normal file
View File

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

15
notifications/Gemfile Normal file
View File

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

2
portal/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.env.*
.env*.*

26
portal/.eslintrc.json Normal file
View File

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

45
portal/.gitignore vendored Normal file
View File

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

1
portal/.rspec Normal file
View File

@ -0,0 +1 @@
--require spec_helper

150
portal/.rubocop.yml Normal file
View File

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

0
portal/.rubocop_todo.yml Normal file
View File

1
portal/.ruby-gemset Normal file
View File

@ -0,0 +1 @@
betbeast

1
portal/.ruby-version Normal file
View File

@ -0,0 +1 @@
ruby-2.6.5

80
portal/Gemfile Normal file
View File

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

446
portal/Gemfile.lock Normal file
View File

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

9
portal/README.md Normal file
View File

@ -0,0 +1,9 @@
# Betbeast
Betbeast is a private bets arbitration service.
## Setting up development
`$ bundle exec rails db:setup`
`$ bundle exec rails s`

6
portal/Rakefile Normal file
View File

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

View File

@ -0,0 +1,2 @@
//= link_tree ../images
//= link_directory ../stylesheets .css

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,15 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@ -0,0 +1,9 @@
class ExchangeAccountChannel < ApplicationCable::Channel
def subscribed
stream_from "exchange_account:#{params[:id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end

View File

@ -0,0 +1,3 @@
class Api::AuthenticatedApiController < ActionController::Base
before_action :authenticate_user!
end

View File

@ -0,0 +1,11 @@
class Api::V1::AccountController < Api::AuthenticatedApiController
def create
render json: { success: true} and return
end
# def is_domain_valid
# valid = !Helpbuild::Ecosystem.reserved_account_domains.include?(params[:domain])
# valid &= /[A-Za-z0-9-]{1,63}/.match(params[:domain]).present?
# render json: { success: valid} and return
# end
end

View File

@ -0,0 +1,7 @@
class Api::V1::AppController < Api::AuthenticatedApiController
skip_before_action :authenticate_user!, only: [:configuration]
def configuration
rtn = {exchange_accounts: ExchangeAccount.pluck(:id).sort}
render json: { success: true, config: rtn} and return
end
end

View File

@ -0,0 +1,104 @@
class Api::V1::BetsController < Api::AuthenticatedApiController
include ApplicationHelper
include Pagy::Backend
skip_before_action :authenticate_user!, only: %i[add_placed_bet tips]
skip_before_action :verify_authenticity_token, only: %i[add_placed_bet tips]
def index
exchange_account = params[:exchange_account_id].blank? ? ExchangeAccount.mounted_account : ExchangeAccount.find(params[:exchange_account_id])
bets = filter_by_date_range(exchange_account.my_bets, 'created_at')
bets = bets.where(outcome: params[:outcome]) unless params[:outcome].blank?
bets = bets.where("exchange_event_name ILIKE '%%#{params[:event_name]}%%'") unless params[:event_name].blank?
ev_range = params[:expected_value]
if ev_range.present?
v_symbolize = JSON.parse(ev_range).symbolize_keys
bets = bets.where("expected_value >= #{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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
class ApplicationController < ActionController::Base
end

View File

@ -0,0 +1,4 @@
class PagesController < ApplicationController
def index
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
class GeneralHelper
def self.broadcast_to_account_channel(account_id, data)
ExchangeAccountChannel.broadcast_to(account_id, data)
end
end

View File

@ -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 &gt; 1.5 goals - yes',
'126' => 'Both halves &gt; 1.5 goals - no',
'127' => 'Both halves &lt; 1.5 goals - yes',
'128' => 'Both halves &lt; 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
module Services
class BetOutcomeService
def bet_outcome(bet_id)
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

82
portal/app/models/bet.rb Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
class BetfairRunnerOdd < ApplicationRecord
belongs_to :betfair_event_runner
enum bet_type: {back: 'back', lay: 'lay'}
end

View File

@ -0,0 +1,6 @@
module Latest
extend ActiveSupport::Concern
included do
scope :latest, -> { order(created_at: :desc).first }
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

82
portal/app/models/user.rb Normal file
View File

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

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>betbeast : taking a bit out of arbitrage</title>
<link href="<%= asset_path("favicon.png") %>" rel="shortcut icon"></link>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- End Google Tag Manager -->
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div id='app'><%= yield %></div>
</body>
</html>

View File

View File

@ -0,0 +1,11 @@
<% @activities_hash.except(:all_activities).each do |k, activities| %>
<% if k.starts_with?('project_asks.') %>
<div style="border: 1px solid #eee; padding: 15px; margin-bottom: 15px;border-radius: 15px; background: #f3f3f3">
<h3 style="margin: 0 0 15px;">Activities</h3>
<% activities.each do |activity| %>
<%= render partial: 'partials/project_ask_digest', locals: { activity: activity } %>
<% end %>
</div>
<% end %>
<% end %>

83
portal/babel.config.js Normal file
View File

@ -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),
};
};

118
portal/bin/bundle Executable file
View File

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

9
portal/bin/rails Executable file
View File

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

9
portal/bin/rake Executable file
View File

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

35
portal/bin/setup Executable file
View File

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

17
portal/bin/spring Executable file
View File

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

18
portal/bin/webpack Executable file
View File

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

18
portal/bin/webpack-dev-server Executable file
View File

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

9
portal/bin/yarn Executable file
View File

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

View File

@ -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(<EntryPoint />);
});

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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();

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

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