init push - laying out the project
							
								
								
									
										2
									
								
								portal/.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| .env.* | ||||
| .env*.* | ||||
							
								
								
									
										26
									
								
								portal/.eslintrc.json
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| --require spec_helper | ||||
							
								
								
									
										150
									
								
								portal/.rubocop.yml
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
							
								
								
									
										1
									
								
								portal/.ruby-gemset
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| betbeast | ||||
							
								
								
									
										1
									
								
								portal/.ruby-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| ruby-2.6.5 | ||||
							
								
								
									
										80
									
								
								portal/Gemfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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 | ||||
							
								
								
									
										2
									
								
								portal/app/assets/config/manifest.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| //= link_tree ../images | ||||
| //= link_directory ../stylesheets .css | ||||
							
								
								
									
										0
									
								
								portal/app/assets/images/.keep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								portal/app/assets/images/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 52 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portal/app/assets/images/logo-white.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portal/app/assets/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.0 KiB | 
							
								
								
									
										15
									
								
								portal/app/assets/stylesheets/application.css
									
									
									
									
									
										Normal 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 | ||||
|  */ | ||||
							
								
								
									
										4
									
								
								portal/app/channels/application_cable/channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| module ApplicationCable | ||||
|   class Channel < ActionCable::Channel::Base | ||||
|   end | ||||
| end | ||||
							
								
								
									
										4
									
								
								portal/app/channels/application_cable/connection.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| module ApplicationCable | ||||
|   class Connection < ActionCable::Connection::Base | ||||
|   end | ||||
| end | ||||
							
								
								
									
										9
									
								
								portal/app/channels/exchange_account_channel.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -0,0 +1,3 @@ | ||||
| class Api::AuthenticatedApiController < ActionController::Base | ||||
|   before_action :authenticate_user! | ||||
| end | ||||
							
								
								
									
										11
									
								
								portal/app/controllers/api/v1/account_controller.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										7
									
								
								portal/app/controllers/api/v1/app_controller.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										104
									
								
								portal/app/controllers/api/v1/bets_controller.rb
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
							
								
								
									
										13
									
								
								portal/app/controllers/api/v1/users_controller.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										2
									
								
								portal/app/controllers/application_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| class ApplicationController < ActionController::Base | ||||
| end | ||||
							
								
								
									
										4
									
								
								portal/app/controllers/pages_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| class PagesController < ApplicationController | ||||
|   def index | ||||
|   end | ||||
| end | ||||
| @@ -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 | ||||
							
								
								
									
										25
									
								
								portal/app/helpers/application_helper.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										11
									
								
								portal/app/helpers/mailer_style_helper.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										14
									
								
								portal/app/jobs/account_sync_and_reconciliation_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										8
									
								
								portal/app/jobs/application_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										63
									
								
								portal/app/jobs/bet_placement_service.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										8
									
								
								portal/app/jobs/clear_old_pulls_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										10
									
								
								portal/app/jobs/process_subscription_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										11
									
								
								portal/app/jobs/pull_event_markets_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										13
									
								
								portal/app/jobs/pull_latest_odds_prices_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										9
									
								
								portal/app/jobs/pull_runner_odds_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										16
									
								
								portal/app/jobs/pull_tips_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										13
									
								
								portal/app/jobs/pull_upcoming_events_job.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										6
									
								
								portal/app/lib/general_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| class GeneralHelper | ||||
|  | ||||
|   def self.broadcast_to_account_channel(account_id, data) | ||||
|     ExchangeAccountChannel.broadcast_to(account_id, data) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										940
									
								
								portal/app/lib/integrations/betburger.rb
									
									
									
									
									
										Normal 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 > 1.5 goals - yes', | ||||
|           '126' => 'Both halves > 1.5 goals - no', | ||||
|           '127' => 'Both halves < 1.5 goals - yes', | ||||
|           '128' => 'Both halves < 1.5 goals - no', | ||||
|           '129' => 'Sets (%s)', | ||||
|           '130' => 'Sets (%s) - not', | ||||
|           '131' => 'Asian Handicap1(%s) - Sets', | ||||
|           '132' => 'Asian Handicap2(%s) - Sets', | ||||
|           '133' => 'Total Over(%s) - Sets', | ||||
|           '134' => 'Total Under(%s) - Sets', | ||||
|           '135' => 'Team1/Team1', | ||||
|           '136' => 'Team1/Team1 - no', | ||||
|           '137' => 'Team1/Draw', | ||||
|           '138' => 'Team1/Draw - no', | ||||
|           '139' => 'Team1/Team2', | ||||
|           '140' => 'Team1/Team2 - no', | ||||
|           '141' => 'Draw/Team1', | ||||
|           '142' => 'Draw/Team1 - no', | ||||
|           '143' => 'Draw/Draw', | ||||
|           '144' => 'Draw/Draw - no', | ||||
|           '145' => 'Draw/Team2', | ||||
|           '146' => 'Draw/Team2 - no', | ||||
|           '147' => 'Team2/Team1', | ||||
|           '148' => 'Team2/Team1 - no', | ||||
|           '149' => 'Team2/Draw', | ||||
|           '150' => 'Team2/Draw - no', | ||||
|           '151' => 'Team2/Team2', | ||||
|           '152' => 'Team2/Team2 - no', | ||||
|           '153' => 'Exact (%s)', | ||||
|           '154' => 'Exact (%s) - no', | ||||
|           '155' => 'Exact (%s) for Team1', | ||||
|           '156' => 'Exact (%s) for Team1 - no', | ||||
|           '157' => 'Exact (%s) for Team2', | ||||
|           '158' => 'Exact (%s) for Team2 - no', | ||||
|           '159' => 'More goals in the 1st half', | ||||
|           '160' => 'Equal goals in halves', | ||||
|           '161' => 'More goals in the 2nd half', | ||||
|           '162' => '1 half most goals (draw no bet)', | ||||
|           '163' => '2 half most goals (draw no bet)', | ||||
|           '164' => 'Team1 - 1st goal', | ||||
|           '165' => 'No goal', | ||||
|           '166' => 'Team2 - 1st goal', | ||||
|           '167' => 'Team1 - 1st goal (draw no bet)', | ||||
|           '168' => 'Team2 - 1st goal (draw no bet)', | ||||
|           '169' => 'Team1 - Last goal', | ||||
|           '170' => 'No goal', | ||||
|           '171' => 'Team2 - Last goal', | ||||
|           '172' => 'Team1 - Last goal (draw no bet)', | ||||
|           '173' => 'Team2 - Last goal (draw no bet)', | ||||
|           '174' => 'Total Over(%s) - Aces', | ||||
|           '175' => 'Total Under(%s) - Aces', | ||||
|           '176' => 'Total Over(%s) for Team1 - Aces', | ||||
|           '177' => 'Total Under(%s) for Team1 - Aces', | ||||
|           '178' => 'Total Over(%s) for Team2 - Aces', | ||||
|           '179' => 'Total Under(%s) for Team2 - Aces', | ||||
|           '180' => 'Total Over(%s) - Double Faults', | ||||
|           '181' => 'Total Under(%s) - Double Faults', | ||||
|           '182' => 'Total Over(%s) for Team1 - Double Faults', | ||||
|           '183' => 'Total Under(%s) for Team1 - Double Faults', | ||||
|           '184' => 'Total Over(%s) for Team2 - Double Faults', | ||||
|           '185' => 'Total Under(%s) for Team2 - Double Faults', | ||||
|           '186' => 'Total Over(%s) for Team1 - 1st Serve', | ||||
|           '187' => 'Total Under(%s) for Team1 - 1st Serve', | ||||
|           '188' => 'Total Over(%s) for Team2 - 1st Serve', | ||||
|           '189' => 'Total Under(%s) for Team2 - 1st Serve', | ||||
|           '190' => 'Asian Handicap1(%s) - Aces', | ||||
|           '191' => 'Asian Handicap2(%s) - Aces', | ||||
|           '192' => 'Asian Handicap1(%s) - Double Faults', | ||||
|           '193' => 'Asian Handicap2(%s) - Double Faults', | ||||
|           '194' => 'Asian Handicap1(%s) - 1st Serve', | ||||
|           '195' => 'Asian Handicap2(%s) - 1st Serve', | ||||
|           '196' => 'Player1 - 1st Ace', | ||||
|           '197' => 'Player2 - 1st Ace', | ||||
|           '198' => 'Player1 - 1st Double Fault', | ||||
|           '199' => 'Player2 - 1st Double Fault', | ||||
|           '200' => 'Player1 - 1st Break', | ||||
|           '201' => 'Player2 - 1st Break', | ||||
|           '202' => 'Player1 - 1st Break', | ||||
|           '203' => 'No break - 1st Break', | ||||
|           '204' => 'Player2 - 1st Break', | ||||
|           '205' => '6-0 Set - yes', | ||||
|           '206' => '6-0 Set - no', | ||||
|           '207' => 'Win From Behind - yes', | ||||
|           '208' => 'Win From Behind - no', | ||||
|           '209' => 'Exact (%s) - Sets', | ||||
|           '210' => 'Exact (%s) - Sets - no', | ||||
|           '211' => 'Team1 - 1st corner', | ||||
|           '212' => 'No corners', | ||||
|           '213' => 'Team2 - 1st corner', | ||||
|           '214' => 'Team1 - Last corner', | ||||
|           '215' => 'No corners', | ||||
|           '216' => 'Team2 - Last corner', | ||||
|           '217' => 'Team1 - 1st Yellow Card', | ||||
|           '218' => 'No Yellow Card', | ||||
|           '219' => 'Team2 - 1st Yellow Card', | ||||
|           '220' => 'Team1 - Last Yellow Card', | ||||
|           '221' => 'No Yellow Card', | ||||
|           '222' => 'Team2 - Last Yellow Card', | ||||
|           '223' => 'Team1 - 1st offside', | ||||
|           '224' => 'No offsides', | ||||
|           '225' => 'Team2 - 1st offside', | ||||
|           '226' => 'Team1 - Last offside', | ||||
|           '227' => 'No offsides', | ||||
|           '228' => 'Team2 - Last offside', | ||||
|           '229' => '1st substitution - 1 half', | ||||
|           '230' => '1st substitution - intermission', | ||||
|           '231' => '1st substitution - 2 half', | ||||
|           '232' => '1st goal - 1 half', | ||||
|           '233' => 'No goal', | ||||
|           '234' => '1st goal - 2 half', | ||||
|           '235' => 'Team1 - 1st subs', | ||||
|           '236' => 'Team2 - 1st subs', | ||||
|           '237' => 'Team1 - Last subs', | ||||
|           '238' => 'Team2 - Last subs', | ||||
|           '239' => '1 - Shots on goal', | ||||
|           '240' => 'X - Shots on goal', | ||||
|           '241' => '2 - Shots on goal', | ||||
|           '242' => '1X - Shots on goal', | ||||
|           '243' => '12 - Shots on goal', | ||||
|           '244' => 'X2 - Shots on goal', | ||||
|           '245' => 'Asian Handicap1(%s) - Shots on goal', | ||||
|           '246' => 'Asian Handicap2(%s) - Shots on goal', | ||||
|           '247' => 'Total Over(%s) - Shots on goal', | ||||
|           '248' => 'Total Under(%s) - Shots on goal', | ||||
|           '249' => 'Total Over(%s) for Team1 - Shots on goal', | ||||
|           '250' => 'Total Under(%s) for Team1 - Shots on goal', | ||||
|           '251' => 'Total Over(%s) for Team2 - Shots on goal', | ||||
|           '252' => 'Total Under(%s) for Team2 - Shots on goal', | ||||
|           '253' => 'Odd - Shots on goal', | ||||
|           '254' => 'Even - Shots on goal', | ||||
|           '255' => '1 - Fouls', | ||||
|           '256' => 'X - Fouls', | ||||
|           '257' => '2 - Fouls', | ||||
|           '258' => '1X - Fouls', | ||||
|           '259' => '12 - Fouls', | ||||
|           '260' => 'X2 - Fouls', | ||||
|           '261' => 'Asian Handicap1(%s) - Fouls', | ||||
|           '262' => 'Asian Handicap2(%s) - Fouls', | ||||
|           '263' => 'Total Over(%s) - Fouls', | ||||
|           '264' => 'Total Under(%s) - Fouls', | ||||
|           '265' => 'Total Over(%s) for Team1 - Fouls', | ||||
|           '266' => 'Total Under(%s) for Team1 - Fouls', | ||||
|           '267' => 'Total Over(%s) for Team2 - Fouls', | ||||
|           '268' => 'Total Under(%s) for Team2 - Fouls', | ||||
|           '269' => 'Odd - Fouls', | ||||
|           '270' => 'Even - Fouls', | ||||
|           '271' => '1 - Offsides', | ||||
|           '272' => 'X - Offsides', | ||||
|           '273' => '2 - Offsides', | ||||
|           '274' => '1X - Offsides', | ||||
|           '275' => '12 - Offsides', | ||||
|           '276' => 'X2 - Offsides', | ||||
|           '277' => 'Asian Handicap1(%s) - Offsides', | ||||
|           '278' => 'Asian Handicap2(%s) - Offsides', | ||||
|           '279' => 'Total Over(%s) - Offsides', | ||||
|           '280' => 'Total Under(%s) - Offsides', | ||||
|           '281' => 'Total Over(%s) for Team1 - Offsides', | ||||
|           '282' => 'Total Under(%s) for Team1 - Offsides', | ||||
|           '283' => 'Total Over(%s) for Team2 - Offsides', | ||||
|           '284' => 'Total Under(%s) for Team2 - Offsides', | ||||
|           '285' => 'Odd - Offsides', | ||||
|           '286' => 'Even - Offsides', | ||||
|           '287' => 'Team1 to Win From Behind - yes', | ||||
|           '288' => 'Team1 to Win From Behind - no', | ||||
|           '289' => 'Team2 to Win From Behind - yes', | ||||
|           '290' => 'Team2 to Win From Behind - no', | ||||
|           '291' => 'Both To Score and W1 - yes', | ||||
|           '292' => 'Both To Score and W1 - no', | ||||
|           '293' => 'Both To Score and W2 - yes', | ||||
|           '294' => 'Both To Score and W2 - no', | ||||
|           '295' => 'Both To Score and Draw - yes', | ||||
|           '296' => 'Both To Score and Draw - no', | ||||
|           '297' => 'Exact (%s) - Added time', | ||||
|           '298' => 'Exact (%s) - Added time - no', | ||||
|           '299' => 'Total Over(%s) - Added time', | ||||
|           '300' => 'Total Under(%s) - Added time', | ||||
|           '301' => 'Home No Bet - W2', | ||||
|           '302' => 'Home No Bet - Draw', | ||||
|           '303' => 'Home No Bet - No Draw', | ||||
|           '304' => 'Away No Bet - W1', | ||||
|           '305' => 'Away No Bet - Draw', | ||||
|           '306' => 'Away No Bet - No Draw', | ||||
|           '307' => 'Total Over(%s) - Subs', | ||||
|           '308' => 'Total Under(%s) - Subs', | ||||
|           '309' => 'Total Over(%s) for Team1 - Subs', | ||||
|           '310' => 'Total Under(%s) for Team1 - Subs', | ||||
|           '311' => 'Total Over(%s) for Team2 - Subs', | ||||
|           '312' => 'Total Under(%s) for Team1 - Subs', | ||||
|           '313' => '1 - Ball possession', | ||||
|           '314' => 'X - Ball possession', | ||||
|           '315' => '2 - Ball possession', | ||||
|           '316' => '1X - Ball possession', | ||||
|           '317' => '12 - Ball possession', | ||||
|           '318' => 'X2 - Ball possession', | ||||
|           '319' => 'Asian Handicap1(%s) - Ball possession', | ||||
|           '320' => 'Asian Handicap2(%s) - Ball possession', | ||||
|           '321' => 'Total Over(%s) for Team1 - Ball possession', | ||||
|           '322' => 'Total Under(%s) for Team1 - Ball possession', | ||||
|           '323' => 'Total Over(%s) for Team2 - Ball possession', | ||||
|           '324' => 'Total Under(%s) for Team2 - Ball possession', | ||||
|           '325' => 'Team1 - 1st corner', | ||||
|           '326' => 'Team2 - 1st corner', | ||||
|           '327' => 'Team1 - Last corner', | ||||
|           '328' => 'Team2 - Last corner', | ||||
|           '329' => 'Team1 - 1st Yellow Card', | ||||
|           '330' => 'Team2 - 1st Yellow Card', | ||||
|           '331' => 'Team1 - Last Yellow Card', | ||||
|           '332' => 'Team2 - Last Yellow Card', | ||||
|           '333' => 'Team1 - 1st offside', | ||||
|           '334' => 'Team2 - 1st offside', | ||||
|           '335' => 'Team1 - Last offside', | ||||
|           '336' => 'Team2 - Last offside', | ||||
|           '337' => 'Home No Bet - No W2', | ||||
|           '338' => 'Away No Bet - No W1', | ||||
|           '339' => '1X and Total Over 2.5 - yes', | ||||
|           '340' => '1X and Total Over 2.5 - no', | ||||
|           '341' => '1X and Total Under 2.5 - yes', | ||||
|           '342' => '1X and Total Under 2.5 - no', | ||||
|           '343' => 'X2 and Total Over 2.5 - yes', | ||||
|           '344' => 'X2 and Total Over 2.5 - no', | ||||
|           '345' => 'X2 and Total Under 2.5 - yes', | ||||
|           '346' => 'X2 and Total Under 2.5 - no', | ||||
|           '348' => 'Overtime - yes', | ||||
|           '351' => 'Overtime - no', | ||||
|           '354' => 'Score Draw - yes', | ||||
|           '357' => 'Score Draw - no', | ||||
|           '360' => 'Race to 2 - Team1', | ||||
|           '363' => 'Race to 2 - Team2', | ||||
|           '366' => 'Race to 2 - Team1', | ||||
|           '369' => 'Race to 2 - neither', | ||||
|           '372' => 'Race to 2 - Team2', | ||||
|           '375' => 'Race to 3 - Team1', | ||||
|           '378' => 'Race to 3 - Team2', | ||||
|           '381' => 'Race to 3 - Team1', | ||||
|           '384' => 'Race to 3 - neither', | ||||
|           '387' => 'Race to 3 - Team2', | ||||
|           '390' => 'Race to 4 - Team1', | ||||
|           '393' => 'Race to 4 - Team2', | ||||
|           '396' => 'Race to 4 - Team1', | ||||
|           '399' => 'Race to 4 - neither', | ||||
|           '402' => 'Race to 4 - Team2', | ||||
|           '405' => 'Race to 5 - Team1', | ||||
|           '408' => 'Race to 5 - Team2', | ||||
|           '411' => 'Race to 5 - Team1', | ||||
|           '414' => 'Race to 5 - neither', | ||||
|           '417' => 'Race to 5 - Team2', | ||||
|           '420' => 'Race to 10 - Team1', | ||||
|           '423' => 'Race to 10 - Team2', | ||||
|           '426' => 'Race to 10 - Team1', | ||||
|           '429' => 'Race to 10 - neither', | ||||
|           '432' => 'Race to 10 - Team2', | ||||
|           '435' => 'Race to 15 - Team1', | ||||
|           '438' => 'Race to 15 - Team2', | ||||
|           '441' => 'Race to 15 - Team1', | ||||
|           '444' => 'Race to 15 - neither', | ||||
|           '447' => 'Race to 15 - Team2', | ||||
|           '450' => 'Race to 20 - Team1', | ||||
|           '453' => 'Race to 20 - Team2', | ||||
|           '456' => 'Race to 20 - Team1', | ||||
|           '459' => 'Race to 20 - neither', | ||||
|           '462' => 'Race to 20 - Team2', | ||||
|           '467' => 'Team1 - Next goal (draw no bet)', | ||||
|           '470' => 'Team2 - Next goal (draw no bet)', | ||||
|           '473' => 'Team1 - Next goal', | ||||
|           '476' => 'No next goal', | ||||
|           '479' => 'Team2 - Next goal', | ||||
|           '481' => '1st - yes', | ||||
|           '484' => '1st - no', | ||||
|           '487' => '1st-3rd - yes', | ||||
|           '490' => '1st-3rd - no', | ||||
|           '493' => 'W1', | ||||
|           '496' => 'W2', | ||||
|           '499' => 'TO(%s) - Hits', | ||||
|           '502' => 'TU(%s) - Hits', | ||||
|           '505' => 'TO(%s) for Team1 - Hits', | ||||
|           '508' => 'TU(%s) for Team1 - Hits', | ||||
|           '511' => 'TO(%s) for Team2 - Hits', | ||||
|           '514' => 'TU(%s) for Team2 - Hits', | ||||
|           '517' => 'TO(%s) - Errors', | ||||
|           '520' => 'TU(%s) - Errors', | ||||
|           '523' => 'TO(%s) - Hits+Errors+Runs', | ||||
|           '526' => 'TU(%s) - Hits+Errors+Runs', | ||||
|           '529' => 'AH1(%s) - Hits', | ||||
|           '532' => 'AH2(%s) - Hits', | ||||
|           '535' => '1 - Hits', | ||||
|           '538' => 'X - Hits', | ||||
|           '541' => '2 - Hits', | ||||
|           '546' => 'Team1 Win - Kills', | ||||
|           '549' => 'Team2 Win - Kills', | ||||
|           '552' => 'Asian Handicap1(%s) - Kills', | ||||
|           '555' => 'Asian Handicap2(%s) - Kills', | ||||
|           '558' => 'Total Over(%s) - Kills', | ||||
|           '561' => 'Total Under(%s) - Kills', | ||||
|           '564' => 'Total Over(%s) for Team1 - Kills', | ||||
|           '567' => 'Total Under(%s) for Team1 - Kills', | ||||
|           '570' => 'Total Over(%s) for Team2 - Kills', | ||||
|           '573' => 'Total Under(%s) for Team2 - Kills', | ||||
|           '574' => 'W1 - 1st blood', | ||||
|           '575' => 'W2 - 1st blood', | ||||
|           '576' => 'W1 - 1st tower', | ||||
|           '577' => 'W2 - 1st tower', | ||||
|           '578' => 'W1 - 1st dragon', | ||||
|           '579' => 'W2 - 1st dragon', | ||||
|           '580' => 'W1 - 1st baron', | ||||
|           '581' => 'W2 - 1st baron', | ||||
|           '582' => 'W1 - 1st inhibitor', | ||||
|           '583' => 'W2 - 1st inhibitor', | ||||
|           '584' => 'W1 - 1st roshan', | ||||
|           '585' => 'W2 - 1st roshan', | ||||
|           '586' => 'Win pistol rounds - Yes', | ||||
|           '587' => 'Win pistol rounds - No', | ||||
|           '588' => 'TO(%s) - Duration', | ||||
|           '589' => 'TU(%s) - Duration', | ||||
|           '590' => 'TO(%s) - Barons', | ||||
|           '591' => 'TU(%s) - Barons', | ||||
|           '592' => 'TO(%s) - Inhibitors', | ||||
|           '593' => 'TU(%s) - Inhibitors', | ||||
|           '594' => 'TO(%s) - Towers', | ||||
|           '595' => 'TU(%s) - Towers', | ||||
|           '596' => 'TO(%s) - Dragons', | ||||
|           '597' => 'TU(%s) - Dragons', | ||||
|           '598' => 'TO(%s) - Roshans', | ||||
|           '599' => 'TU(%s) - Roshans', | ||||
|           '600' => 'TO(%s) for Team1 - Sets', | ||||
|           '601' => 'TO(%s) for Team1 - Sets', | ||||
|           '602' => 'TO(%s) for Team2 - Sets', | ||||
|           '603' => 'TO(%s) for Team2 - Sets', | ||||
|           '604' => 'W1 - Longest TD', | ||||
|           '605' => 'W2 - Longest TD', | ||||
|           '606' => 'W1 - Longest FG', | ||||
|           '607' => 'W2 - Longest FG', | ||||
|           '608' => 'Touchdown - Yes', | ||||
|           '609' => 'Touchdown - No', | ||||
|           '610' => 'Safety - Yes', | ||||
|           '611' => 'Safety - No', | ||||
|           '612' => 'First score TD - Yes', | ||||
|           '613' => 'First score TD - No', | ||||
|           '614' => 'Both teams 10 pts - Yes', | ||||
|           '615' => 'Both teams 10 pts - No', | ||||
|           '616' => 'Both teams 15 pts - Yes', | ||||
|           '617' => 'Both teams 15 pts - No', | ||||
|           '618' => 'Both teams 20 pts - Yes', | ||||
|           '619' => 'Both teams 20 pts - No', | ||||
|           '620' => 'Both teams 25 pts - Yes', | ||||
|           '621' => 'Both teams 25 pts - No', | ||||
|           '622' => 'Both teams 30 pts - Yes', | ||||
|           '623' => 'Both teams 30 pts - No', | ||||
|           '624' => 'Both teams 35 pts - Yes', | ||||
|           '625' => 'Both teams 35 pts - No', | ||||
|           '626' => 'Both teams 40 pts - Yes', | ||||
|           '627' => 'Both teams 40 pts - No', | ||||
|           '628' => 'Both teams 45 pts - Yes', | ||||
|           '629' => 'Both teams 45 pts - No', | ||||
|           '630' => 'Both teams 50 pts - Yes', | ||||
|           '631' => 'Both teams 50 pts - No', | ||||
|           '632' => 'Highest Scoring Quarter - 1st', | ||||
|           '633' => 'Highest Scoring Quarter - 2nd', | ||||
|           '634' => 'Highest Scoring Quarter - 3rd', | ||||
|           '635' => 'Highest Scoring Quarter - 4th', | ||||
|           '636' => 'Highest Scoring Quarter - Tie', | ||||
|           '637' => 'TO(%s) - Field Goals', | ||||
|           '638' => 'TU(%s) - Field Goals', | ||||
|           '639' => 'TO(%s) - Touchdowns', | ||||
|           '640' => 'TU(%s) - Touchdowns', | ||||
|           '641' => 'TO(%s) - Longest TD, distance', | ||||
|           '642' => 'TU(%s) - Longest TD, distance', | ||||
|           '643' => 'TO(%s) - Longest FG, distance', | ||||
|           '644' => 'TU(%s) - Longest FG, distance', | ||||
|           '645' => 'TO(%s) for Team1 - Field Goals', | ||||
|           '646' => 'TU(%s) for Team1 - Field Goals', | ||||
|           '647' => 'TO(%s) for Team2 - Field Goals', | ||||
|           '648' => 'TU(%s) for Team2 - Field Goals', | ||||
|           '649' => 'TO(%s) for Team1 - Touchdowns', | ||||
|           '650' => 'TU(%s) for Team1 - Touchdowns', | ||||
|           '651' => 'TO(%s) for Team2 - Touchdowns', | ||||
|           '652' => 'TU(%s) for Team2 - Touchdowns', | ||||
|           '653' => 'AH1(%s) - Maps', | ||||
|           '654' => 'AH2(%s) - Maps', | ||||
|           '655' => 'TO(%s) - Maps', | ||||
|           '656' => 'TU(%s) - Maps', | ||||
|           '657' => 'TO(%s) for Team1 - Maps', | ||||
|           '658' => 'TU(%s) for Team1 - Maps', | ||||
|           '659' => 'TO(%s) for Team2 - Maps', | ||||
|           '660' => 'TU(%s) for Team2 - Maps', | ||||
|           '661' => 'Maps (%s)', | ||||
|           '662' => 'Maps (%s) - not', | ||||
|           '663' => 'Exact (%s) - Maps', | ||||
|           '664' => 'Exact (%s) - Maps - no', | ||||
|           '665' => 'Win both pistol rounds - Yes', | ||||
|           '666' => 'Win both pistol rounds - No', | ||||
|           '667' => 'Team1 to win both pistol rounds - Yes', | ||||
|           '668' => 'Team1 to win both pistol rounds - No', | ||||
|           '669' => 'Team2 to win both pistol rounds - Yes', | ||||
|           '670' => 'Team1 to win both pistol rounds - No', | ||||
|           '671' => 'Team1 to win at least 1 set - Yes', | ||||
|           '672' => 'Team1 to win at least 1 set - No', | ||||
|           '673' => 'Team2 to win at least 1 set - Yes', | ||||
|           '674' => 'Team1 to win at least 1 set - No', | ||||
|           '675' => 'Team1 to win at least 1 map - Yes', | ||||
|           '676' => 'Team1 to win at least 1 map - No', | ||||
|           '677' => 'Team2 to win at least 1 map - Yes', | ||||
|           '678' => 'Team1 to win at least 1 map - No', | ||||
|           '679' => 'Both teams kill a dragon - Yes', | ||||
|           '680' => 'Both teams kill a dragon - No', | ||||
|           '681' => 'Both teams kill a baron - Yes', | ||||
|           '682' => 'Both teams kill a baron - No', | ||||
|           '683' => 'W1 - 1st Barracks', | ||||
|           '684' => 'W2 - 1st Barracks', | ||||
|           '685' => 'W1 - 1st Double Kill', | ||||
|           '686' => 'W2 - 1st Double Kill', | ||||
|           '687' => 'TO(%s) - Barracks', | ||||
|           '688' => 'TU(%s) - Barracks', | ||||
|           '689' => 'TO(%s) - Double Kills', | ||||
|           '690' => 'TU(%s) - Double Kills', | ||||
|           '691' => 'AH1(%s) - Barons', | ||||
|           '692' => 'AH2(%s) - Barons', | ||||
|           '693' => 'AH1(%s) - Dragons', | ||||
|           '694' => 'AH2(%s) - Dragons', | ||||
|           '695' => 'AH1(%s) - Towers/Turrets', | ||||
|           '696' => 'AH2(%s) - Towers/Turrets', | ||||
|           '697' => '1 - 3-points', | ||||
|           '698' => 'X - 3-points', | ||||
|           '699' => '2 - 3-points', | ||||
|           '700' => '1 - Rebounds', | ||||
|           '701' => 'X - Rebounds', | ||||
|           '702' => '2 - Rebounds', | ||||
|           '703' => '1 - Assists', | ||||
|           '704' => 'X - Assists', | ||||
|           '705' => '2 - Assists', | ||||
|           '706' => '1X - 3-points', | ||||
|           '707' => 'X2 - 3-points', | ||||
|           '708' => '12 - 3-points', | ||||
|           '709' => '1X - Rebounds', | ||||
|           '710' => 'X2 - Rebounds', | ||||
|           '711' => '12 - Rebounds', | ||||
|           '712' => '1X - Assists', | ||||
|           '713' => 'X2 - Assists', | ||||
|           '714' => '12 - Assists', | ||||
|           '715' => 'AH1(%s) - 3-points', | ||||
|           '716' => 'AH2(%s) - 3-points', | ||||
|           '717' => 'AH1(%s) - Rebounds', | ||||
|           '718' => 'AH2(%s) - Rebounds', | ||||
|           '719' => 'AH1(%s) - Assists', | ||||
|           '720' => 'AH2(%s) - Assists', | ||||
|           '721' => 'TO(%s) - 3-points', | ||||
|           '722' => 'TU(%s) - 3-points', | ||||
|           '723' => 'TO(%s) - Rebounds', | ||||
|           '724' => 'TU(%s) - Rebounds', | ||||
|           '725' => 'TO(%s) - Assists', | ||||
|           '726' => 'TU(%s) - Assists', | ||||
|           '727' => 'TO(%s) for Team1 - 3-points', | ||||
|           '728' => 'TU(%s) for Team1 - 3-points', | ||||
|           '729' => 'TO(%s) for Team1 - Rebounds', | ||||
|           '730' => 'TU(%s) for Team1 - Rebounds', | ||||
|           '731' => 'TO(%s) for Team1 - Assists', | ||||
|           '732' => 'TU(%s) for Team1 - Assists', | ||||
|           '733' => 'TO(%s) for Team2 - 3-points', | ||||
|           '734' => 'TU(%s) for Team2 - 3-points', | ||||
|           '735' => 'TO(%s) for Team2 - Rebounds', | ||||
|           '736' => 'TU(%s) for Team2 - Rebounds', | ||||
|           '737' => 'TO(%s) for Team2 - Assists', | ||||
|           '738' => 'TU(%s) for Team2 - Assists', | ||||
|           '739' => 'W1 - 180s', | ||||
|           '740' => 'W2 - 180s', | ||||
|           '741' => 'AH1(%s) - 180s', | ||||
|           '742' => 'AH2(%s) - 180s', | ||||
|           '743' => 'TO(%s) - 180s', | ||||
|           '744' => 'TU(%s) - 180s', | ||||
|           '745' => '1 - Cards', | ||||
|           '746' => 'X - Cards', | ||||
|           '747' => '2 - Cards', | ||||
|           '748' => '1 - Booking points', | ||||
|           '749' => 'X - Booking points', | ||||
|           '750' => '2 - Booking points', | ||||
|           '751' => '1X - Cards', | ||||
|           '752' => 'X2 - Cards', | ||||
|           '753' => '12 - Cards', | ||||
|           '754' => '1X - Booking points', | ||||
|           '755' => 'X2 - Booking points', | ||||
|           '756' => '12 - Booking points', | ||||
|           '757' => 'AH1(%s) - Cards', | ||||
|           '758' => 'AH2(%s) - Cards', | ||||
|           '759' => 'AH1(%s) - Booking points', | ||||
|           '760' => 'AH2(%s) - Booking points', | ||||
|           '761' => 'TO(%s) - Cards', | ||||
|           '762' => 'TU(%s) - Cards', | ||||
|           '763' => 'TO(%s) - Booking points', | ||||
|           '764' => 'TU(%s) - Booking points', | ||||
|           '765' => 'TO(%s) for Team1 - Cards', | ||||
|           '766' => 'TU(%s) for Team1 - Cards', | ||||
|           '767' => 'TO(%s) for Team1 - Booking points', | ||||
|           '768' => 'TU(%s) for Team1 - Booking points', | ||||
|           '769' => 'TO(%s) for Team2 - Cards', | ||||
|           '770' => 'TU(%s) for Team2 - Cards', | ||||
|           '771' => 'TO(%s) for Team2 - Booking points', | ||||
|           '772' => 'TU(%s) for Team2 - Booking points', | ||||
|           '773' => 'Odd - Cards', | ||||
|           '774' => 'Even - Cards', | ||||
|           '775' => 'Odd - Booking points', | ||||
|           '776' => 'Even - Booking points' } | ||||
|  | ||||
|     end | ||||
|  | ||||
|     def periods | ||||
|       @periods ||= | ||||
|         { | ||||
|           '1': 'match (pairs)', | ||||
|           '2': 'with OT and SO', | ||||
|           '3': 'with OT', | ||||
|           '4': 'regular time', #match odds | ||||
|           '5': '1st', | ||||
|           '6': '2nd', | ||||
|           '7': '3rd', | ||||
|           '8': '4th', | ||||
|           '9': '5th', | ||||
|           '10': '1st half', #first | ||||
|           '13': '2nd half', | ||||
|           '14': '1 set, 01 game', | ||||
|           '15': '1 set, 02 game', | ||||
|           '16': '1 set, 03 game', | ||||
|           '17': '1 set, 04 game', | ||||
|           '18': '1 set, 05 game', | ||||
|           '19': '1 set, 06 game', | ||||
|           '20': '1 set, 07 game', | ||||
|           '21': '1 set, 08 game', | ||||
|           '22': '1 set, 09 game', | ||||
|           '23': '1 set, 10 game', | ||||
|           '24': '1 set, 11 game', | ||||
|           '25': '1 set, 12 game', | ||||
|           '26': '1 set, 13 game', | ||||
|           '44': '2 set, 01 game', | ||||
|           '45': '2 set, 02 game', | ||||
|           '46': '2 set, 03 game', | ||||
|           '47': '2 set, 04 game', | ||||
|           '48': '2 set, 05 game', | ||||
|           '49': '2 set, 06 game', | ||||
|           '50': '2 set, 07 game', | ||||
|           '51': '2 set, 08 game', | ||||
|           '52': '2 set, 09 game', | ||||
|           '53': '2 set, 10 game', | ||||
|           '54': '2 set, 11 game', | ||||
|           '55': '2 set, 12 game', | ||||
|           '56': '2 set, 13 game', | ||||
|           '57': '3 set, 01 game', | ||||
|           '58': '3 set, 02 game', | ||||
|           '59': '3 set, 03 game', | ||||
|           '60': '3 set, 04 game', | ||||
|           '61': '3 set, 05 game', | ||||
|           '62': '3 set, 06 game', | ||||
|           '63': '3 set, 07 game', | ||||
|           '64': '3 set, 08 game', | ||||
|           '65': '3 set, 09 game', | ||||
|           '66': '3 set, 10 game', | ||||
|           '67': '3 set, 11 game', | ||||
|           '68': '3 set, 12 game', | ||||
|           '71': '3 set, 13 game', | ||||
|           '76': '6th', | ||||
|           '78': '7th', | ||||
|           '86': 'regular time', | ||||
|           '92': 'regular time', | ||||
|           '93': '1st half', | ||||
|           '95': 'to qualify', | ||||
|           '96': '8th', | ||||
|           '97': '9th', | ||||
|           '113': '4 set, 01 game', | ||||
|           '114': '4 set, 02 game', | ||||
|           '115': '4 set, 03 game', | ||||
|           '116': '4 set, 04 game', | ||||
|           '117': '4 set, 05 game', | ||||
|           '118': '4 set, 06 game', | ||||
|           '119': '4 set, 07 game', | ||||
|           '120': '4 set, 08 game', | ||||
|           '121': '4 set, 09 game', | ||||
|           '122': '4 set, 10 game', | ||||
|           '123': '4 set, 11 game', | ||||
|           '124': '4 set, 12 game', | ||||
|           '125': '5 set, 01 game', | ||||
|           '126': '5 set, 02 game', | ||||
|           '127': '5 set, 03 game', | ||||
|           '128': '5 set, 04 game', | ||||
|           '129': '5 set, 05 game', | ||||
|           '130': '5 set, 06 game', | ||||
|           '131': '5 set, 07 game', | ||||
|           '132': '5 set, 08 game', | ||||
|           '133': '5 set, 09 game', | ||||
|           '134': '5 set, 10 game', | ||||
|           '156': '4 set, 13 game', | ||||
|           '159': '5 set, 11 game', | ||||
|           '161': '5 set, 12 game', | ||||
|           '169': '5 set, 13 game', | ||||
|           '223': 'regular time', | ||||
|           '243': '1st half', | ||||
|           '245': '2nd half', | ||||
|           '246': '2nd half', | ||||
|           '317': '1st half', | ||||
|           '318': '2nd half', | ||||
|           '4091': 'regular time', #match odds | ||||
|           '4094': '1st half' | ||||
|         } | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								portal/app/lib/integrations/betfair/account_manager.rb
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										40
									
								
								portal/app/lib/integrations/betfair/base.rb
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										173
									
								
								portal/app/lib/integrations/betfair/bet_manager.rb
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										39
									
								
								portal/app/lib/integrations/betfair/connection.rb
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										77
									
								
								portal/app/lib/integrations/betfair/opportunity_hunter.rb
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
							
								
								
									
										6
									
								
								portal/app/lib/services/bet_outcome_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| module Services | ||||
|   class BetOutcomeService | ||||
|     def bet_outcome(bet_id) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										44
									
								
								portal/app/mailers/application_mailer.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										76
									
								
								portal/app/mailers/project_mailer.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										21
									
								
								portal/app/mailers/user_mailer.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										15
									
								
								portal/app/models/account.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										3
									
								
								portal/app/models/application_record.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| class ApplicationRecord < ActiveRecord::Base | ||||
|   self.abstract_class = true | ||||
| end | ||||
							
								
								
									
										82
									
								
								portal/app/models/bet.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										5
									
								
								portal/app/models/betfair_event.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										6
									
								
								portal/app/models/betfair_event_runner.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										4
									
								
								portal/app/models/betfair_runner_odd.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| class BetfairRunnerOdd < ApplicationRecord | ||||
|   belongs_to :betfair_event_runner | ||||
|   enum bet_type: {back: 'back', lay: 'lay'} | ||||
| end | ||||
							
								
								
									
										6
									
								
								portal/app/models/concerns/latest.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| module Latest | ||||
|   extend ActiveSupport::Concern | ||||
|   included do | ||||
|     scope :latest, -> { order(created_at: :desc).first } | ||||
|   end | ||||
| end | ||||
							
								
								
									
										218
									
								
								portal/app/models/exchange_account.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										20
									
								
								portal/app/models/loggable.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										17
									
								
								portal/app/models/source_subscription.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										17
									
								
								portal/app/models/subscription_run.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										22
									
								
								portal/app/models/tip_source.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										15
									
								
								portal/app/models/tip_source_data.rb
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										18
									
								
								portal/app/models/tipster_account.rb
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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 | ||||
							
								
								
									
										19
									
								
								portal/app/views/layouts/application.html.erb
									
									
									
									
									
										Normal 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> | ||||
|  | ||||
							
								
								
									
										0
									
								
								portal/app/views/pages/index.html.erb
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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 | ||||
							
								
								
									
										24
									
								
								portal/client/packs/application.js
									
									
									
									
									
										Normal 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 />); | ||||
| }); | ||||
							
								
								
									
										5
									
								
								portal/client/src/assets/images.js
									
									
									
									
									
										Normal 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 }; | ||||
							
								
								
									
										
											BIN
										
									
								
								portal/client/src/assets/images/iconmonstr-arrow-65-240.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portal/client/src/assets/images/linkedin.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portal/client/src/assets/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.0 KiB | 
							
								
								
									
										4
									
								
								portal/client/src/assets/images/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portal/client/src/assets/images/main-bg.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 109 KiB | 
							
								
								
									
										9
									
								
								portal/client/src/assets/style.js
									
									
									
									
									
										Normal 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'; | ||||
							
								
								
									
										1210
									
								
								portal/client/src/assets/stylesheets/style.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										6
									
								
								portal/client/src/channels/consumer.js
									
									
									
									
									
										Normal 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(); | ||||
							
								
								
									
										15
									
								
								portal/client/src/data/axiosClient.js
									
									
									
									
									
										Normal 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(); | ||||
							
								
								
									
										19
									
								
								portal/client/src/data/concerns/filterable.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										8
									
								
								portal/client/src/data/concerns/notfications.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										67
									
								
								portal/client/src/data/entities/base_entity.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										37
									
								
								portal/client/src/data/entities/bet.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										48
									
								
								portal/client/src/data/entities/exchange_account.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										58
									
								
								portal/client/src/data/entities/pagination.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| import { action, computed, makeObservable, observable } from 'mobx'; | ||||
| import { isNull } from 'lodash'; | ||||
|  | ||||
| import BaseEntity from './base_entity'; | ||||
|  | ||||
| class Pagination extends BaseEntity { | ||||
|   /* eslint-disable */ | ||||
|   @observable count = 0; | ||||
|   @observable page = 1; | ||||
|   @observable pages = null; | ||||
|   @observable prev = null; | ||||
|   @observable next = null; | ||||
|   @observable last = null; | ||||
|   @observable from = null; | ||||
|   @observable to = null; | ||||
|   @observable params = {}; | ||||
|   @observable filterType = 'fetch'; | ||||
|   initialParams = {}; | ||||
|   /* eslint-enable */ | ||||
|  | ||||
|   constructor(value, store) { | ||||
|     super(value, store); | ||||
|  | ||||
|     makeObservable(this); | ||||
|  | ||||
|     this.initialize(value); | ||||
|     this.initialParams = value.params; | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   fetch() { | ||||
|     this.store.fetch({ ...this.params, page: this.page }); | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   updateParams(params) { | ||||
|     this.update({ params: { ...this.params, ...params } }); | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   gotoPage(page) { | ||||
|     this.update({ page }); | ||||
|     this.fetch(); | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   resetPageAndFetch() { | ||||
|     this.gotoPage(1); | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   reset() { | ||||
|     this.update({ page: 1, params: this.initialParams }); | ||||
|     this.fetch(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default Pagination; | ||||
							
								
								
									
										51
									
								
								portal/client/src/data/entities/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | ||||
| import { action, computed, makeObservable, observable } from 'mobx'; | ||||
| import BaseEntity from './base_entity'; | ||||
|  | ||||
| class User extends BaseEntity { | ||||
|   /* eslint-disable */ | ||||
|   id; | ||||
|   email; | ||||
|   accountId; | ||||
|   accountName; | ||||
|   @observable status; | ||||
|   @observable firstName; | ||||
|   @observable lastName; | ||||
|   @observable strKey; | ||||
|   @observable fullname; | ||||
|   @observable onboarded = []; | ||||
|   @observable environment = 'production'; | ||||
|   /* eslint-enable */ | ||||
|  | ||||
|   constructor(value, store) { | ||||
|     super(value, store); | ||||
|  | ||||
|     makeObservable(this); | ||||
|  | ||||
|     this.handleConstruction(value); | ||||
|   } | ||||
|  | ||||
|   updateUrl = () => `/api/v1/users/${this.id}.json`; | ||||
|  | ||||
|   @computed | ||||
|   get fullName() { | ||||
|     return `${this.firstName} ${this.lastName}`; | ||||
|   } | ||||
|  | ||||
|   @computed | ||||
|   get asSelectOption() { | ||||
|     return { label: this.fullName, value: this.id }; | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   logout = () => { | ||||
|     window.location.href = '/public'; | ||||
|   }; | ||||
|  | ||||
|   @action | ||||
|   handleConstruction(value) { | ||||
|     const val = { ...value }; | ||||
|     this.initialize(val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default User; | ||||
							
								
								
									
										15
									
								
								portal/client/src/data/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| import { makeAutoObservable } from 'mobx'; | ||||
|  | ||||
| import AppStore from './stores/app_store'; | ||||
| import UserStore from './stores/user_store'; | ||||
| import BetStore from './stores/bet_store'; | ||||
|  | ||||
| export default class RootStore { | ||||
|   constructor() { | ||||
|     this.appStore = new AppStore(this); | ||||
|     this.userStore = new UserStore(this); | ||||
|     this.betStore = new BetStore(this); | ||||
|  | ||||
|     makeAutoObservable(this); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										4
									
								
								portal/client/src/data/provider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| import React from 'react'; | ||||
|  | ||||
| export const StoresContext = React.createContext(null); | ||||
| export const StoreProvider = StoresContext.Provider; | ||||