├── log └── .keep ├── app ├── mailers │ ├── .keep │ ├── application_mailer.rb │ └── admin_mailer.rb ├── models │ ├── .keep │ ├── concerns │ │ ├── .keep │ │ ├── reference_id.rb │ │ ├── currency.rb │ │ ├── reload_uuid.rb │ │ ├── percentize.rb │ │ └── dollarize.rb │ ├── artist_standing_score.rb │ ├── application_record.rb │ ├── offer_response.rb │ ├── admin_user.rb │ ├── partner.rb │ ├── note.rb │ ├── consignment_inquiry.rb │ ├── demand_calculator.rb │ ├── user.rb │ └── partner_submission.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── x.png │ │ ├── facebook.png │ │ ├── twitter.png │ │ ├── artsy-logo.jpg │ │ ├── instagram.png │ │ ├── missing_image.png │ │ ├── artsy-take-photo.jpg │ │ ├── missing_photo_todo.png │ │ ├── desc_arrow.svg │ │ ├── asc_arrow.svg │ │ └── lock.svg │ ├── javascripts │ │ ├── offer.js.coffee │ │ ├── consignment.js.coffee │ │ ├── application.js │ │ └── new_partner.js.coffee │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ └── application.css.scss ├── graphql │ ├── types │ │ ├── .keep │ │ ├── base_enum.rb │ │ ├── base_union.rb │ │ ├── base_scalar.rb │ │ ├── base_argument.rb │ │ ├── offer_sort_type.rb │ │ ├── base_input_object.rb │ │ ├── consignment_sort_type.rb │ │ ├── asset_type_type.rb │ │ ├── base_interface.rb │ │ ├── submission_sort_type.rb │ │ ├── submission_source_type.rb │ │ ├── state_type.rb │ │ ├── intended_state_type.rb │ │ ├── base_object.rb │ │ ├── offer_connection_type.rb │ │ ├── consignment_state_type.rb │ │ ├── json_type.rb │ │ ├── submission_connection_type.rb │ │ ├── consignment_connection_type.rb │ │ ├── attribution_class_type.rb │ │ ├── offer_response_type.rb │ │ ├── date_type.rb │ │ ├── base_field.rb │ │ ├── consignment_type.rb │ │ ├── asset_type.rb │ │ ├── sort_type.rb │ │ ├── mutation_type.rb │ │ ├── category_type.rb │ │ └── offer_type.rb │ ├── mutations │ │ ├── .keep │ │ ├── base_mutation.rb │ │ ├── add_user_to_submission_mutation.rb │ │ ├── remove_asset_from_consignment_submission.rb │ │ ├── create_offer_response_mutation.rb │ │ ├── add_asset_to_consignment_submission.rb │ │ └── add_assets_to_consignment_submission.rb │ ├── extensions │ │ ├── default_value_extension.rb │ │ └── nilable_field_extension.rb │ ├── convection_schema.rb │ └── resolvers │ │ ├── submission_resolver.rb │ │ ├── create_submission_resolver.rb │ │ ├── remove_asset_from_submission_resolver.rb │ │ ├── update_submission_resolver.rb │ │ ├── create_offer_resolver.rb │ │ ├── add_user_to_submission_resolver.rb │ │ ├── create_offer_response_resolver.rb │ │ ├── offers_resolver.rb │ │ ├── consignments_resolver.rb │ │ ├── offer_resolver.rb │ │ ├── add_asset_to_submission_resolver.rb │ │ └── concerns │ │ └── resolvers │ │ └── submissionable.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── application.html.erb │ │ ├── _head.html.erb │ │ ├── mailer_no_footer.html.erb │ │ ├── _nav.html.erb │ │ └── mailer.html.erb │ ├── admin │ │ ├── submissions │ │ │ ├── new.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── show.js.erb │ │ │ ├── _submission_section.html.erb │ │ │ ├── _rejection_reason.html.erb │ │ │ ├── _assigned_to.html.erb │ │ │ ├── _salesforce.html.erb │ │ │ ├── _cataloguer.html.erb │ │ │ ├── _listed_artworks.html.erb │ │ │ ├── _created_by.html.erb │ │ │ ├── _collector_info.html.erb │ │ │ └── _state_actions.html.erb │ │ ├── admin_users │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── offers │ │ │ ├── _offer_section.html.erb │ │ │ ├── _purchase_form.html.erb │ │ │ ├── _offers_section.html.erb │ │ │ ├── _purchase.html.erb │ │ │ ├── _net_price.html.erb │ │ │ ├── _net_price_form.html.erb │ │ │ ├── _retail.html.erb │ │ │ ├── _interested.html.erb │ │ │ └── _retail_form.html.erb │ │ ├── consignments │ │ │ └── _consignment_section.html.erb │ │ ├── shared │ │ │ ├── _partner_info.html.erb │ │ │ └── _sort_label.html.erb │ │ ├── notes │ │ │ ├── _form.html.erb │ │ │ └── _note.html.erb │ │ ├── dashboard │ │ │ └── _row.html.erb │ │ ├── offer_responses │ │ │ ├── _offer_responses_section.html.erb │ │ │ └── _offer_response_section.html.erb │ │ ├── assets │ │ │ ├── _gemini_form.html.erb │ │ │ └── _s3_form.html.erb │ │ └── partner_submissions │ │ │ └── index.html.erb │ └── shared │ │ └── email │ │ ├── _email_header.html.erb │ │ ├── _offer_note.html.erb │ │ ├── _submission.html.erb │ │ └── _offer_type.html.erb ├── helpers │ ├── partner_submissions_helper.rb │ ├── dashboard_helper.rb │ └── utm_params_helper.rb ├── services │ ├── user_service.rb │ ├── offer_response_service.rb │ ├── partner_update_service.rb │ ├── partner_service.rb │ ├── consignment_inquiry_service.rb │ └── my_collection_artwork_updated_service.rb ├── controllers │ ├── system_controller.rb │ ├── api │ │ ├── users_controller.rb │ │ ├── graphql_controller.rb │ │ ├── consignment_inquiries_controller.rb │ │ ├── consignments_controller.rb │ │ ├── base_controller.rb │ │ ├── callbacks_controller.rb │ │ └── assets_controller.rb │ └── admin │ │ ├── users_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── partner_submissions_controller.rb │ │ └── notes_controller.rb └── workers │ └── sneakers │ └── my_collection_artworks_worker.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ ├── submissions.rake │ ├── partners.rake │ ├── partner_submissions.rake │ ├── admin_user.rake │ └── offers.rake ├── metaql.rb ├── gravql.rb ├── jwt_middleware.rb ├── gravity.rb ├── s3.rb ├── artsy_admin_auth.rb └── gravity_v1.rb ├── public ├── favicon.ico └── robots.txt ├── .husky ├── .gitignore └── pre-commit ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .foreman ├── .rspec ├── .standard.yml ├── .tool-versions ├── bin ├── worker ├── console ├── server ├── rake ├── rails ├── bundle ├── schema_check ├── pull_data ├── detect-secrets └── update ├── Procfile ├── .vscode ├── extensions.json └── settings.json ├── .prettierignore ├── .env.example ├── hokusai ├── build.yml ├── templates │ └── docker-compose-service.yml.j2 ├── config.yml ├── test.yml ├── ci.sh ├── development.yml └── rubyrep.yml ├── renovate.json ├── config ├── storage.yml ├── initializers │ ├── sentry.rb │ ├── graphiql.rb │ ├── graphql_rails_logger.rb │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── money.rb │ ├── artsy_auth.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── artsy-event_publisher.rb │ ├── markdown_parser.rb │ ├── sidekiq.rb │ ├── mail.rb │ ├── backtrace_silencers.rb │ ├── permissions_policy.rb │ ├── unleash.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ ├── datadog.rb │ ├── sneakers.rb │ └── content_security_policy.rb ├── environment.rb ├── boot.rb ├── database.yml ├── puma.config ├── puma.rb ├── locales │ └── en.yml └── secrets.yml ├── spec ├── fabricators │ ├── offer_response_fabricator.rb │ ├── artist_standing_score_fabricator.rb │ ├── note_fabricator.rb │ ├── offer_fabricator.rb │ ├── user_fabricator.rb │ ├── partner_fabricator.rb │ ├── consignment_inquiry_fabricator.rb │ ├── admin_user_fabricator.rb │ ├── partner_submission_fabricator.rb │ ├── submission_fabricator.rb │ └── asset_fabricator.rb ├── models │ ├── admin_user_spec.rb │ ├── offer_responses_spec.rb │ └── consignment_inquiry_spec.rb ├── support │ ├── jwt_helper.rb │ ├── graphql_helper.rb │ └── gravql_helper.rb ├── lib │ └── time_zone_spec.rb ├── requests │ ├── system_spec.rb │ ├── admin │ │ └── submissions │ │ │ └── index_spec.rb │ └── api │ │ └── graphql │ │ └── extensions │ │ └── default_value_extension_spec.rb ├── spec_helper.rb ├── helpers │ └── application_helper_spec.rb ├── types │ └── sort_type_spec.rb ├── controllers │ └── api │ │ ├── consignment_contoller_spec.rb │ │ └── consignment_inquiries_spec.rb └── services │ ├── consignment_inquiry_service_spec.rb │ ├── partner_update_service_spec.rb │ └── notification_service_spec.rb ├── db ├── migrate │ ├── 20220124121120_add_email_to_admin_users.rb │ ├── 20220310110820_add_source_to_submissions.rb │ ├── 20211130121953_add_created_by_to_submissions.rb │ ├── 20171102162905_add_name_to_partner.rb │ ├── 20220405085819_add_postal_code_to_submissions.rb │ ├── 20210707130204_add_notes_to_user.rb │ ├── 20220406080052_add_country_code_to_submissions.rb │ ├── 20200211224225_add_partner_info_to_offers.rb │ ├── 20180129221644_add_counter_cache.rb │ ├── 20180212170737_add_currency_to_ps.rb │ ├── 20180214173948_add_cancelled_reason.rb │ ├── 20200520221322_add_sale_location_to_offer.rb │ ├── 20170221184930_add_submission_to_asset.rb │ ├── 20180411194253_add_override_email_to_offers.rb │ ├── 20190408181209_add_user_agent_to_submissions.rb │ ├── 20210526211807_add_assets_asset_type_index.rb │ ├── 20210529174358_add_publisher_to_submissions.rb │ ├── 20170602140738_add_admin_receipt_sent_at.rb │ ├── 20171211221203_change_type_of_integer_cents.rb │ ├── 20200305193734_add_assigned_to_to_submissions.rb │ ├── 20201120231224_add_source_id_to_submissions.rb │ ├── 20210529175722_add_literature_to_submissions.rb │ ├── 20210529175840_add_exhibition_to_submissions.rb │ ├── 20210628123858_add_cataloguer_to_submissions.rb │ ├── 20211209141138_add_name_and_size_fields_to_assets.rb │ ├── 20200604164331_add_published_at_to_submission.rb │ ├── 20210406185140_add_starting_bid_cents_to_offers.rb │ ├── 20210529174611_add_artist_proofs_to_submissions.rb │ ├── 20170601203337_add_reminders_sent_count.rb │ ├── 20190808142434_add_marked_as_deleted_to_submissions.rb │ ├── 20171214192939_add_offer_sent_at.rb │ ├── 20201113001511_add_offer_responses_count_to_offers.rb │ ├── 20210529182121_add_condition_report_to_submissions.rb │ ├── 20210602193423_add_signature_detail_to_submissions.rb │ ├── 20210624120342_add_rejection_reason_to_submissions.rb │ ├── 20210527195938_add_attribution_class_to_submissions.rb │ ├── 20240508215653_add_listed_artwork_ids_to_submissions.rb │ ├── 20220117133849_add_source_artwork_id_index_to_submission_table.rb │ ├── 20240705092437_add_s3_bucket_and_path_to_assets.rb │ ├── 20210512082147_add_invoice_number_to_partner_submissions.rb │ ├── 20171212160509_add_submission_id_to_offer.rb │ ├── 20180125155145_add_offer_state_fields.rb │ ├── 20220131105557_add_my_collection_artwork_id_field_to_submission.rb │ ├── 20170518124302_add_edition_number_and_size.rb │ ├── 20180126212637_denormalize_collector_email.rb │ ├── 20200108220043_add_demand_scores_to_estimate.rb │ ├── 20240716094023_add_location_address_to_submissions.rb │ ├── 20180821221301_add_minimum_price_to_submissions.rb │ ├── 20211121122634_change_gravity_user_id_to_not_unique.rb │ ├── 20211102083208_add_phone_and_name_fields_to_users.rb │ ├── 20230302151711_add_recipient_email_to_consignment_inquiry.rb │ ├── 20180131182229_rename_submission_table.rb │ ├── 20170516194441_add_status_to_submission.rb │ ├── 20170908222106_add_primary_asset_to_submission.rb │ ├── 20210524185949_add_columns_to_submission.rb │ ├── 20210622110240_add_coa_fields_to_submissions.rb │ ├── 20200826154051_drop_stale_offer_fields.rb │ ├── 20211014123304_add_contact_information_fields_to_submission.rb │ ├── 20170814141232_create_partners.rb │ ├── 20171101190725_update_primary_image.rb │ ├── 20200108210457_create_artist_standing_scores.rb │ ├── 20200625153607_create_notes.rb │ ├── 20180131172252_add_users_table.rb │ ├── 20170503151447_change_image_urls_type.rb │ ├── 20180226122325_use_bigint.rb │ ├── 20221205131817_create_consignment_inquiries.rb │ ├── 20170717153030_add_approved_by_to_submission.rb │ ├── 20211019105312_change_edition_size_type_to_string.rb │ ├── 20211201142602_remove_name_and_phone_from_user.rb │ ├── 20201111212135_add_offers_response_model.rb │ ├── 20220125100834_add_external_id_to_submissions.rb │ ├── 20171218214858_add_reject_accept_fields.rb │ ├── 20170823171400_add_partner_submission.rb │ ├── 20210712125843_create_admin_users.rb │ ├── 20211116122633_add_session_id_to_user_and_set_gravity_id_nullable.rb │ ├── 20170210232121_create_submissions.rb │ ├── 20171207210014_add_offers_table.rb │ └── 20220128111006_add_foreign_key_from_submission_to_admin_user.rb └── seeds.rb ├── config.ru ├── .gitignore ├── scripts └── load_secrets_and_run.sh ├── .github └── dependabot.yml ├── package.json ├── .dockerignore ├── Rakefile ├── Guardfile ├── docs └── scripts.md ├── Dockerfile └── LICENSE.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/types/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/graphql/mutations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.foreman: -------------------------------------------------------------------------------- 1 | env: .env.shared,.env 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | format: progress 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.0.2 2 | nodejs 18.15.0 3 | -------------------------------------------------------------------------------- /bin/worker: -------------------------------------------------------------------------------- 1 | foreman run bundle exec sidekiq 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | foreman run bundle exec rails console 2 | -------------------------------------------------------------------------------- /bin/server: -------------------------------------------------------------------------------- 1 | foreman run bundle exec rails server 2 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | <%= render template: 'layouts/watt/minimal' %> 2 | -------------------------------------------------------------------------------- /app/assets/images/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/x.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | worker: bundle exec sidekiq -c ${SIDEKIQ_CONCURRENCY:-5} 3 | -------------------------------------------------------------------------------- /app/assets/images/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/facebook.png -------------------------------------------------------------------------------- /app/assets/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/twitter.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["shopify.ruby-lsp", "testdouble.vscode-standard-ruby"] 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/images/artsy-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/artsy-logo.jpg -------------------------------------------------------------------------------- /app/assets/images/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/instagram.png -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/assets/images/missing_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/missing_image.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | bin/detect-secrets 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | app/graphql/util/authorization_instrumentation.rb 2 | hokusai/*.yml 3 | node_modules 4 | **/*.text.erb 5 | *.md -------------------------------------------------------------------------------- /app/assets/images/artsy-take-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/artsy-take-photo.jpg -------------------------------------------------------------------------------- /app/assets/images/missing_photo_todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artsy/convection/main/app/assets/images/missing_photo_todo.png -------------------------------------------------------------------------------- /app/models/artist_standing_score.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ArtistStandingScore < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## for over-riding .env.shared 2 | 3 | # to use your local rabbitmq service. 4 | #RABBITMQ_URL=amqp://localhost:5672 5 | -------------------------------------------------------------------------------- /hokusai/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | services: 4 | convection: 5 | {% include 'templates/docker-compose-service.yml.j2' %} 6 | -------------------------------------------------------------------------------- /app/views/admin/submissions/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

New Submission

3 |
4 | 5 | <%= render 'form' %> 6 | -------------------------------------------------------------------------------- /app/graphql/types/base_enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseEnum < GraphQL::Schema::Enum 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/graphql/types/base_union.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseUnion < GraphQL::Schema::Union 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /hokusai/templates/docker-compose-service.yml.j2: -------------------------------------------------------------------------------- 1 | build: 2 | context: ../ 3 | args: 4 | BUNDLE_GITHUB__COM: ${BUNDLE_GITHUB__COM} -------------------------------------------------------------------------------- /app/graphql/types/base_scalar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseScalar < GraphQL::Schema::Scalar 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[ruby]": { 4 | "editor.defaultFormatter": "testdouble.vscode-standard-ruby" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/graphql/types/base_argument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseArgument < GraphQL::Schema::Argument 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/views/admin/submissions/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Edit Submission #<%= @submission.id %>

3 |
4 | 5 | <%= render 'form' %> 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "automerge": true, 3 | "major": { 4 | "automerge": false 5 | }, 6 | "extends": ["@artsy:app"], 7 | "assignees": ["jonallured"] 8 | } 9 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | local: 2 | service: Disk 3 | root: <%= Rails.root.join("storage") %> 4 | 5 | test: 6 | service: Disk 7 | root: <%= Rails.root.join("tmp/storage") %> 8 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 5 | load Gem.bin_path("bundler", "bundle") 6 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sentry.init do |config| 4 | config.dsn = Convection.config.sentry_dsn if Convection.config.sentry_dsn 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/offer_sort_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class OfferSortType < SortType 5 | generate_values(Offer.column_names) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fabricators/offer_response_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:offer_response) do 4 | offer { Fabricate(:offer) } 5 | intended_state "accepted" 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/graphiql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | GraphiQL::Rails.config.headers["Authorization"] = 4 | lambda { |context| "Bearer #{context.session[:access_token]}" } 5 | -------------------------------------------------------------------------------- /db/migrate/20220124121120_add_email_to_admin_users.rb: -------------------------------------------------------------------------------- 1 | class AddEmailToAdminUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :admin_users, :email, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220310110820_add_source_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddSourceToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :source, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative "application" 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/initializers/graphql_rails_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | GraphQL::RailsLogger.configure do |config| 4 | config.white_list = {"Api::GraphqlController" => %w[execute]} 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/base_input_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseInputObject < GraphQL::Schema::InputObject 5 | argument_class Types::BaseArgument 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/graphql/types/consignment_sort_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class ConsignmentSortType < SortType 5 | generate_values(PartnerSubmission.column_names) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20211130121953_add_created_by_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddCreatedByToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :created_by, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/asset_type_type.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class AssetTypeType < Types::BaseEnum 3 | value("IMAGE", nil, value: "image") 4 | value("ADDITIONAL_FILE", nil, value: "additional_file") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /db/migrate/20171102162905_add_name_to_partner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddNameToPartner < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :partners, :name, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20220405085819_add_postal_code_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddPostalCodeToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :location_postal_code, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/graphql/types/base_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | module BaseInterface 5 | include GraphQL::Schema::Interface 6 | 7 | field_class Types::BaseField 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/graphql/types/submission_sort_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class SubmissionSortType < SortType 5 | generate_values(Submission.column_names - ["listed_artwork_ids"]) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/partner_submissions_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PartnerSubmissionsHelper 4 | def new_time_field_value(current_value) 5 | current_value.present? ? nil : Time.now.utc 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative "config/environment" 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.session_store :cookie_store, key: "_convection_session" 6 | -------------------------------------------------------------------------------- /db/migrate/20210707130204_add_notes_to_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddNotesToUser < ActiveRecord::Migration[6.1] 4 | def change 5 | add_reference :notes, :user, foreign_key: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20220406080052_add_country_code_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddCountryCodeToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :location_country_code, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !/log/.keep 2 | .DS_Store 3 | .byebug_history 4 | .env* 5 | !.env.example 6 | .ruby-gemset 7 | /.bundle 8 | /log/* 9 | /tmp 10 | latest.dump 11 | node_modules/ 12 | public 13 | yarn-error.log 14 | .ruby-version 15 | -------------------------------------------------------------------------------- /lib/tasks/submissions.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | task exchange_assigned_to_real_user: :environment do 4 | Submission.all.find_each do |submission| 5 | submission.exchange_assigned_to_real_user! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/services/user_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserService 4 | class << self 5 | def anonymize_email!(email) 6 | User.where(email: email).update_all(email: nil) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | -------------------------------------------------------------------------------- /db/migrate/20200211224225_add_partner_info_to_offers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPartnerInfoToOffers < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :offers, :partner_info, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/fabricators/artist_standing_score_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:artist_standing_score) do 4 | artist_id { Fabricate.sequence(:artist_id) } 5 | artist_score 0.69 6 | auction_score 0.72 7 | end 8 | -------------------------------------------------------------------------------- /app/views/shared/email/_email_header.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /db/migrate/20180129221644_add_counter_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCounterCache < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :submissions, :offers_count, :integer, default: 0 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180212170737_add_currency_to_ps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCurrencyToPs < ActiveRecord::Migration[5.1] 4 | def change 5 | rename_column :partner_submissions, :sale_currency, :currency 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180214173948_add_cancelled_reason.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCancelledReason < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :partner_submissions, :canceled_reason, :text 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200520221322_add_sale_location_to_offer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSaleLocationToOffer < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :offers, :sale_location, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/admin/admin_users/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to 'Back', admin_admin_users_path, class: 'btn btn-small btn-primary pull-right' %> 3 |

4 | Edit Admin 5 |

6 |
7 | 8 | <%= render 'form' %> 9 | -------------------------------------------------------------------------------- /app/views/admin/admin_users/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to 'Back', admin_admin_users_path, class: 'btn btn-small btn-primary pull-right' %> 3 |

4 | New Admin 5 |

6 |
7 | 8 | <%= render 'form' %> 9 | -------------------------------------------------------------------------------- /db/migrate/20170221184930_add_submission_to_asset.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSubmissionToAsset < ActiveRecord::Migration[5.0] 4 | def change 5 | add_reference :assets, :submission, foreign_key: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180411194253_add_override_email_to_offers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOverrideEmailToOffers < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :offers, :override_email, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190408181209_add_user_agent_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUserAgentToSubmissions < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :submissions, :user_agent, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210526211807_add_assets_asset_type_index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAssetsAssetTypeIndex < ActiveRecord::Migration[6.1] 4 | def change 5 | add_index :assets, %i[submission_id asset_type] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210529174358_add_publisher_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPublisherToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :publisher, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170602140738_add_admin_receipt_sent_at.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAdminReceiptSentAt < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :submissions, :admin_receipt_sent_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20171211221203_change_type_of_integer_cents.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeTypeOfIntegerCents < ActiveRecord::Migration[5.0] 4 | def change 5 | change_column :offers, :insurance_cents, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200305193734_add_assigned_to_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAssignedToToSubmissions < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :submissions, :assigned_to, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20201120231224_add_source_id_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSourceIdToSubmissions < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :submissions, :source_artwork_id, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210529175722_add_literature_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLiteratureToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :literature, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210529175840_add_exhibition_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddExhibitionToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :exhibition, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210628123858_add_cataloguer_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCataloguerToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :cataloguer, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20211209141138_add_name_and_size_fields_to_assets.rb: -------------------------------------------------------------------------------- 1 | class AddNameAndSizeFieldsToAssets < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :assets, :filename, :string 4 | add_column :assets, :size, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/helpers/dashboard_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DashboardHelper 4 | include ApplicationHelper 5 | 6 | def sum_up_approved_submissions(approved: 0, published: 0, **) 7 | approved + published 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20200604164331_add_published_at_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPublishedAtToSubmission < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :submissions, :published_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210406185140_add_starting_bid_cents_to_offers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddStartingBidCentsToOffers < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :offers, :starting_bid_cents, :bigint 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210529174611_add_artist_proofs_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddArtistProofsToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :artist_proofs, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170601203337_add_reminders_sent_count.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRemindersSentCount < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :submissions, :reminders_sent_count, :integer, default: 0 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190808142434_add_marked_as_deleted_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMarkedAsDeletedToSubmissions < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :submissions, :deleted_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /hokusai/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project-name: convection 3 | hokusai-required-version: ">=0.5.8" 4 | pre-deploy: "bundle exec rake db:migrate" 5 | git-remote: git@github.com:artsy/convection.git 6 | template-config-files: 7 | - s3://artsy-citadel/k8s/hokusai-vars.yml 8 | -------------------------------------------------------------------------------- /db/migrate/20171214192939_add_offer_sent_at.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOfferSentAt < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :offers, :sent_at, :datetime 6 | add_column :offers, :sent_by, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20201113001511_add_offer_responses_count_to_offers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOfferResponsesCountToOffers < ActiveRecord::Migration[6.0] 4 | def change 5 | add_column :offers, :offer_responses_count, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210529182121_add_condition_report_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddConditionReportToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :condition_report, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210602193423_add_signature_detail_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSignatureDetailToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :signature_detail, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20210624120342_add_rejection_reason_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRejectionReasonToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :rejection_reason, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 7 | -------------------------------------------------------------------------------- /db/migrate/20210527195938_add_attribution_class_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAttributionClassToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :attribution_class, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240508215653_add_listed_artwork_ids_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddListedArtworkIdsToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :listed_artwork_ids, :string, array: true, null: false, default: [] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /scripts/load_secrets_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CMD="$@" 4 | 5 | if [ ! -z "$SECRETS_FILE" ] 6 | then 7 | echo "SECRETS_FILE env var is defined. Sourcing secrets file..." 8 | source "$SECRETS_FILE" 9 | fi 10 | 11 | echo "Running command: $CMD" 12 | $CMD 13 | -------------------------------------------------------------------------------- /spec/fabricators/note_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:note) do 4 | gravity_user_id { Fabricate(:user).gravity_user_id } 5 | body { Fabricate.sequence(:email) { |i| "I'm note #{i}" } } 6 | submission { Fabricate(:submission) } 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20220117133849_add_source_artwork_id_index_to_submission_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSourceArtworkIdIndexToSubmissionTable < ActiveRecord::Migration[6.1] 4 | def change 5 | add_index :submissions, :source_artwork_id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240705092437_add_s3_bucket_and_path_to_assets.rb: -------------------------------------------------------------------------------- 1 | class AddS3BucketAndPathToAssets < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :assets, :s3_bucket, :string, null: true 4 | add_column :assets, :s3_path, :string, null: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/graphql/types/submission_source_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class SubmissionSourceType < Types::BaseEnum 5 | Submission::SOURCES.map do |source| 6 | value(source.upcase, nil, value: source) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/admin/offers/_offer_section.html.erb: -------------------------------------------------------------------------------- 1 | <% if offer %> 2 |
3 |
4 | Offer 5 |
6 | <%= render 'admin/offers/offer', offer: offer, truncated: true %> 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/admin/submissions/show.js.erb: -------------------------------------------------------------------------------- 1 | $('#edit').html("<%= j render 'admin/submissions/assigned_to'%>"); 2 | $('#edit_cataloguer').html("<%= j render 'admin/submissions/cataloguer'%>"); 3 | $('#edit_rejection_reason').html("<%= j render 'admin/submissions/rejection_reason'%>"); 4 | -------------------------------------------------------------------------------- /db/migrate/20210512082147_add_invoice_number_to_partner_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddInvoiceNumberToPartnerSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :partner_submissions, :invoice_number, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fabricators/offer_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:offer) do 4 | partner_submission { Fabricate(:partner_submission) } 5 | offer_type { "purchase" } 6 | state "sent" 7 | price_cents 120_000 8 | commission_percent 0.5 9 | end 10 | -------------------------------------------------------------------------------- /spec/fabricators/user_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:user) do 4 | gravity_user_id do 5 | Fabricate.sequence(:gravity_user_id) { |i| "user-id-#{i}" } 6 | end 7 | email { Fabricate.sequence(:email) { |i| "jon-jonson#{i}@test.com" } } 8 | end 9 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_submission_section.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Submission 4 |
5 | <%= render 'admin/submissions/submission', submission: submission, artist: artist, truncated: true %> 6 |
7 | -------------------------------------------------------------------------------- /spec/fabricators/partner_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:partner) do 4 | gravity_partner_id do 5 | Fabricate.sequence(:gravity_partner_id) { |i| "partner-id-#{i}" } 6 | end 7 | name { Fabricate.sequence(:name) { |i| "Gallery #{i}" } } 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/admin_user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe AdminUser, type: :model do 6 | it "creates an admin user" do 7 | Fabricate(:admin_user) 8 | 9 | expect(AdminUser.count).to eq(1) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171212160509_add_submission_id_to_offer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSubmissionIdToOffer < ActiveRecord::Migration[5.0] 4 | def change 5 | add_reference :offers, :submission, index: true 6 | add_foreign_key :offers, :submissions 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180125155145_add_offer_state_fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOfferStateFields < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :offers, :review_started_at, :datetime 6 | add_column :offers, :consigned_at, :datetime 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | # Limit to 0 to enable only security updates: 8 | open-pull-requests-limit: 0 9 | reviewers: 10 | - artsy/onyx-devs 11 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_rejection_reason.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= @submission.rejection_reason %> 3 |
4 | 5 | -------------------------------------------------------------------------------- /db/migrate/20220131105557_add_my_collection_artwork_id_field_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMyCollectionArtworkIdFieldToSubmission < ActiveRecord::Migration[6.1] 4 | def change 5 | add_column :submissions, :my_collection_artwork_id, :string 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fabricators/consignment_inquiry_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:consignment_inquiry) do 4 | email "test@email.com" 5 | gravity_user_id "gravity-user-id" 6 | message "The message" 7 | name "Test User" 8 | phone_number "+49283938382" 9 | end 10 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 5 | 6 | development: 7 | <<: *default 8 | database: convection_development 9 | 10 | test: 11 | <<: *default 12 | database: convection_test 13 | -------------------------------------------------------------------------------- /app/graphql/types/state_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class StateType < Types::BaseEnum 5 | description "Enum with all available submission states" 6 | 7 | Submission::STATES.map { |source| value(source.upcase, nil, value: source) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170518124302_add_edition_number_and_size.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddEditionNumberAndSize < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :submissions, :edition_number, :string 6 | add_column :submissions, :edition_size, :integer 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180126212637_denormalize_collector_email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DenormalizeCollectorEmail < ActiveRecord::Migration[5.1] 4 | def change 5 | add_column :submissions, :user_email, :string 6 | execute "create extension if not exists pg_trgm;" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20200108220043_add_demand_scores_to_estimate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDemandScoresToEstimate < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :submissions, :artist_score, :float 6 | add_column :submissions, :auction_score, :float 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fabricators/admin_user_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:admin_user) do 4 | gravity_user_id do 5 | Fabricate.sequence(:gravity_user_id) { |i| "user-id-#{i}" } 6 | end 7 | name { "jon-jonson" } 8 | assignee { false } 9 | cataloguer { false } 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/extensions/default_value_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Extensions 4 | class DefaultValueExtension < GraphQL::Schema::FieldExtension 5 | def after_resolve(value:, **_rest) 6 | value.nil? ? options[:default_value] : value 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/graphql/types/intended_state_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class IntendedStateType < Types::BaseEnum 5 | value("ACCEPTED", nil, value: "accepted") 6 | value("REJECTED", nil, value: "rejected") 7 | value("REVIEW", nil, value: "review") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240716094023_add_location_address_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddLocationAddressToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :submissions, :location_address, :string, null: true 4 | add_column :submissions, :location_address2, :string, null: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180821221301_add_minimum_price_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMinimumPriceToSubmissions < ActiveRecord::Migration[5.2] 4 | def change 5 | add_column :submissions, :minimum_price_cents, :bigint 6 | add_column :submissions, :currency, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20211121122634_change_gravity_user_id_to_not_unique.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeGravityUserIdToNotUnique < ActiveRecord::Migration[6.1] 4 | def change 5 | remove_index :users, [:gravity_user_id] 6 | add_index :users, [:gravity_user_id], unique: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/support/jwt_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def stub_jwt_header(user_id = nil) 4 | id = user_id || "userid" 5 | payload_data = {sub: id, aud: "convection"} 6 | token = JWT.encode payload_data, Convection.config.jwt_secret, "HS256" 7 | page.set_rack_session(access_token: token) 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/system_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SystemController < ApplicationController 4 | skip_before_action :verify_authenticity_token 5 | skip_before_action :require_artsy_authentication 6 | 7 | def up 8 | render json: {rails: true}, status: :ok 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_assigned_to.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= @submission.assigned_to == '' ? 'Unassigned' : AdminUser.find_by(gravity_user_id: @submission.assigned_to)&.name %> 3 |
4 | 5 | -------------------------------------------------------------------------------- /db/migrate/20211102083208_add_phone_and_name_fields_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPhoneAndNameFieldsToUsers < ActiveRecord::Migration[6.1] 4 | def change 5 | change_table :users, bulk: true do |t| 6 | t.string :name 7 | t.string :phone 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/convection_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ConvectionSchema < GraphQL::Schema 4 | max_depth 13 5 | max_complexity 300 6 | default_max_page_size 20 7 | 8 | mutation(Types::MutationType) 9 | query(Types::QueryType) 10 | 11 | use GraphQL::Pagination::Connections 12 | end 13 | -------------------------------------------------------------------------------- /app/views/admin/consignments/_consignment_section.html.erb: -------------------------------------------------------------------------------- 1 | <% if consignment %> 2 |
3 |
4 | Consignment 5 |
6 | <%= render 'admin/consignments/consignment', consignment: consignment, artist: artist %> 7 |
8 | <% end %> -------------------------------------------------------------------------------- /config/initializers/money.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Money.locale_backend = :i18n 4 | 5 | # The default rounding mode used to be `BigDecimal::ROUND_HALF_EVEN': 6 | # https://github.com/RubyMoney/money/pull/863/files#diff-a6d34f32282d8d431a585cdaf0aaf730L159 7 | Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN 8 | -------------------------------------------------------------------------------- /db/migrate/20230302151711_add_recipient_email_to_consignment_inquiry.rb: -------------------------------------------------------------------------------- 1 | class AddRecipientEmailToConsignmentInquiry < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :consignment_inquiries, :recipient_email, :string 4 | add_index :consignment_inquiries, [:recipient_email], unique: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/offer.js.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | # Enable complete button only if checkbox is checked 3 | $('input[name="terms_signed"]').on 'click', -> 4 | if $(this).is(':checked') 5 | $('.offer-consign-button').removeClass('disabled-button') 6 | else 7 | $('.offer-consign-button').addClass('disabled-button') 8 | -------------------------------------------------------------------------------- /app/graphql/extensions/nilable_field_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Extensions 4 | class NilableFieldExtension < GraphQL::Schema::FieldExtension 5 | def resolve(object:, arguments:, **_rest) 6 | yield(object, arguments, nil) 7 | rescue 8 | nil 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/graphql/types/base_object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseObject < GraphQL::Schema::Object 5 | field_class Types::BaseField 6 | 7 | def self.nilable_field(*args, **kwargs, &block) 8 | field(*args, nilable_field: true, **kwargs, &block) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/graphql/types/offer_connection_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class OfferEdgeType < GraphQL::Types::Relay::BaseEdge 5 | node_type(Types::OfferType) 6 | end 7 | 8 | class OfferConnectionType < GraphQL::PageCursorConnection 9 | edge_type(Types::OfferEdgeType) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/artsy_auth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ArtsyAuth.configure do |config| 4 | config.artsy_api_url = Convection.config.gravity_url 5 | config.callback_url = "/" 6 | config.application_id = Convection.config.gravity_app_id 7 | config.application_secret = Convection.config.gravity_app_secret 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180131182229_rename_submission_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameSubmissionTable < ActiveRecord::Migration[5.1] 4 | def change 5 | rename_column :submissions, :user_id, :ext_user_id 6 | 7 | add_reference :submissions, :user, foreign_key: true, type: :integer 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/admin/offers/_purchase_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 | <%= f.text_field :price_dollars, class: 'form-control' %> 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/admin/shared/_partner_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Partner info
3 |
4 | <%= offer.partner.name %> 5 |
6 | 7 |
8 | <%= markdown_formatted(offer.partner_info) %> 9 |
10 |
11 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # ActiveSupport::Reloader.to_prepare do 5 | # ApplicationController.renderer.defaults.merge!( 6 | # http_host: 'example.org', 7 | # https: false 8 | # ) 9 | # end 10 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_salesforce.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Salesforce
3 |
4 | 🗒️ Artwork <%= link_to @submission.salesforce_artwork[:Id], salesforce_url(@submission.salesforce_artwork[:Id]), class: "smaller-sidebar-link" %> 5 |
6 |
7 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += 7 | %i[password secret token _key crypt salt certificate otp ssn] 8 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_cataloguer.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= @submission.cataloguer == '' ? 'Unassigned' : AdminUser.find_by(gravity_user_id: @submission.cataloguer)&.name %> 3 |
4 | 5 | -------------------------------------------------------------------------------- /config/initializers/artsy-event_publisher.rb: -------------------------------------------------------------------------------- 1 | Artsy::EventPublisher.configure do |config| 2 | config.app_id = "artsy-convection" # identifies RabbitMQ connection 3 | config.enabled = ENV["RABBITMQ_URL"].present? # enable/disable publishing events 4 | config.rabbitmq_url = ENV["RABBITMQ_URL"] # required 5 | config.logger = Rails.logger 6 | end 7 | -------------------------------------------------------------------------------- /app/graphql/mutations/base_mutation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class BaseMutation < GraphQL::Schema::RelayClassicMutation 5 | argument_class Types::BaseArgument 6 | field_class Types::BaseField 7 | input_object_class Types::BaseInputObject 8 | object_class Types::BaseObject 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170516194441_add_status_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddStatusToSubmission < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :submissions, :edition, :boolean 6 | add_column :submissions, :state, :string 7 | add_column :submissions, :receipt_sent_at, :datetime 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170908222106_add_primary_asset_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPrimaryAssetToSubmission < ActiveRecord::Migration[5.0] 4 | def change 5 | add_reference :submissions, :primary_image, references: :assets, index: true 6 | add_foreign_key :submissions, :assets, column: :primary_image_id 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20210524185949_add_columns_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddColumnsToSubmission < ActiveRecord::Migration[6.1] 4 | def change 5 | change_table :submissions, bulk: true do |t| 6 | t.string :utm_source 7 | t.string :utm_medium 8 | t.string :utm_term 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20210622110240_add_coa_fields_to_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddCoaFieldsToSubmissions < ActiveRecord::Migration[6.1] 4 | def change 5 | change_table :submissions, bulk: true do |t| 6 | t.boolean :coa_by_authenticating_body 7 | t.boolean :coa_by_gallery 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/types/consignment_state_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class ConsignmentStateType < Types::BaseEnum 5 | value("OPEN", nil, value: "open") 6 | value("CANCELLED", nil, value: "cancelled") 7 | value("SOLD", nil, value: "sold") 8 | value("BOUGHT_IN", nil, value: "bought in") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/graphql/types/json_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class JsonType < Types::BaseScalar 5 | graphql_name "JSON" 6 | 7 | def self.coerce_input(value, _ctx) 8 | JSON.parse(value) 9 | end 10 | 11 | def self.coerce_result(value, _ctx) 12 | JSON.dump(value) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/graphql/types/submission_connection_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class SubmissionEdgeType < GraphQL::Types::Relay::BaseEdge 5 | node_type(Types::SubmissionType) 6 | end 7 | 8 | class SubmissionConnectionType < GraphQL::PageCursorConnection 9 | edge_type(Types::SubmissionEdgeType) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200826154051_drop_stale_offer_fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DropStaleOfferFields < ActiveRecord::Migration[6.0] 4 | def change 5 | change_table :offers, bulk: true do 6 | remove_column :offers, :accepted_at, :datetime 7 | remove_column :offers, :accepted_by, :string 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20211014123304_add_contact_information_fields_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddContactInformationFieldsToSubmission < ActiveRecord::Migration[6.1] 4 | def change 5 | change_table :submissions, bulk: true do |t| 6 | t.string :user_name 7 | t.string :user_phone 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | // Created this file based on these docs: 2 | // https://github.com/rails/sprockets/blob/master/UPGRADING.md#manifestjs 3 | 4 | //= link_tree ../images 5 | //= link_directory ../javascripts .js 6 | //= link_directory ../stylesheets .css 7 | 8 | //= link graphiql/rails/application.js 9 | //= link graphiql/rails/application.css 10 | -------------------------------------------------------------------------------- /app/graphql/types/consignment_connection_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class ConsignmentEdgeType < GraphQL::Types::Relay::BaseEdge 5 | node_type(Types::ConsignmentType) 6 | end 7 | 8 | class ConsignmentConnectionType < GraphQL::PageCursorConnection 9 | edge_type(Types::ConsignmentEdgeType) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/admin/offers/_offers_section.html.erb: -------------------------------------------------------------------------------- 1 | <% if offers && offers.length > 0 %> 2 |
3 |
4 | Offers 5 |
6 | <% @offers.each do |offer| %> 7 | <%= render 'admin/offers/offer', offer: offer, truncated: true %> 8 | <% end %> 9 |
10 | <% end %> -------------------------------------------------------------------------------- /bin/schema_check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | mv _schema.graphql _schema.graphql.bak 6 | rake graphql:schema:idl 7 | 8 | # either we err and trap and move files back 9 | trap "mv _schema.graphql.bak _schema.graphql" ERR 10 | diff -q _schema.graphql _schema.graphql.bak 11 | # or we don't err and we move files back 12 | mv _schema.graphql.bak _schema.graphql 13 | -------------------------------------------------------------------------------- /config/initializers/markdown_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MarkdownParser 4 | def self.render(string) 5 | parser.render(string) 6 | end 7 | 8 | def self.parser 9 | @parser ||= initialize_parser 10 | end 11 | 12 | def self.initialize_parser 13 | Redcarpet::Markdown.new(Redcarpet::Render::HTML.new) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/time_zone_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe "Time zones" do 6 | it "handles daylight saving time properly" do 7 | utc = "2020-12-01T00:00:00+00:00" 8 | est = "2020-11-30T19:00:00-05:00" 9 | expect(Time.parse(utc).in_time_zone("America/New_York").iso8601).to eq est 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170814141232_create_partners.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreatePartners < ActiveRecord::Migration[5.0] 4 | def change 5 | create_table :partners do |t| 6 | t.string :external_partner_id, null: false 7 | 8 | t.timestamps 9 | end 10 | add_index :partners, %i[external_partner_id], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20171101190725_update_primary_image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdatePrimaryImage < ActiveRecord::Migration[5.0] 4 | def change 5 | remove_foreign_key :submissions, column: :primary_image_id 6 | add_foreign_key :submissions, 7 | :assets, 8 | column: :primary_image_id, 9 | on_delete: :nullify 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sidekiq::Extensions.enable_delay! 4 | 5 | if Rails.env.development? 6 | redis_config = {url: "redis://localhost:6379/#{ENV.fetch("REDIS_DB", 0)}"} 7 | 8 | Sidekiq.configure_server { |config| config.redis = redis_config } 9 | 10 | Sidekiq.configure_client { |config| config.redis = redis_config } 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200108210457_create_artist_standing_scores.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateArtistStandingScores < ActiveRecord::Migration[5.2] 4 | def change 5 | create_table :artist_standing_scores do |t| 6 | t.string :artist_id 7 | t.float :artist_score 8 | t.float :auction_score 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20200625153607_create_notes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateNotes < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :notes do |t| 6 | t.string :gravity_user_id, null: false 7 | t.text :body, null: false 8 | t.references :submission, foreign_key: true 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180131172252_add_users_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUsersTable < ActiveRecord::Migration[5.1] 4 | def change 5 | create_table :users do |t| 6 | t.string :gravity_user_id, null: false 7 | t.string :email 8 | 9 | t.timestamps 10 | end 11 | 12 | add_index :users, %i[gravity_user_id], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | include UtmParamsHelper 5 | default from: "Artsy " 6 | default bcc: Convection.config.bcc_email_address 7 | layout "mailer" 8 | 9 | protected 10 | 11 | def smtpapi(opts = {}) 12 | headers["X-SMTPAPI"] = opts.to_json 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/admin/notes/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with model: [:admin, note], local: true do |f| %> 2 | <%= f.hidden_field :submission_id %> 3 | <%= f.text_area :body, required: "required" %> 4 | <%= f.label :add_note_to_user do %> 5 | <%= f.check_box :add_note_to_user %> 6 | Add a note to the user 7 | <% end %> 8 | <%= f.submit 'Create', style: "float:right;" %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/views/admin/dashboard/_row.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to filtered_path, class: ['list-group-item dashboard_subcategory_row', section_name.parameterize] do %> 2 |
3 |

4 | <%= section_name %> : <%= items_count || 0 %> 5 |

6 | 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /app/views/layouts/_head.html.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag '//fast.fonts.net/cssapi/f7f47a40-b25b-44ee-9f9c-cfdfc8bb2741.css' %> 2 | <%= stylesheet_link_tag 'application', media: 'all' %> 3 | <%= javascript_include_tag 'application' %> 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/models/concerns/reference_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ReferenceId 4 | extend ActiveSupport::Concern 5 | 6 | included { before_create :create_reference_id } 7 | 8 | def create_reference_id 9 | loop do 10 | self.reference_id = SecureRandom.hex(5) 11 | break unless self.class.unscoped.exists?(reference_id: reference_id) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/mail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActionMailer::Base.smtp_settings = { 4 | user_name: Convection.config.smtp_user, 5 | password: Convection.config.smtp_password, 6 | address: Convection.config.smtp_address, 7 | port: Convection.config.smtp_port, 8 | domain: Convection.config.smtp_domain, 9 | authentication: :plain, 10 | enable_starttls_auto: true 11 | } 12 | -------------------------------------------------------------------------------- /db/migrate/20170503151447_change_image_urls_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeImageUrlsType < ActiveRecord::Migration[5.0] 4 | def self.up 5 | change_column :assets, 6 | :image_urls, 7 | "jsonb USING CAST(image_urls AS jsonb)", 8 | default: {} 9 | end 10 | 11 | def self.down 12 | change_column :assets, :image_urls, :string 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20180226122325_use_bigint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UseBigint < ActiveRecord::Migration[5.1] 4 | def change 5 | change_column :partner_submissions, :sale_price_cents, :bigint 6 | change_column :offers, :low_estimate_cents, :bigint 7 | change_column :offers, :high_estimate_cents, :bigint 8 | change_column :offers, :price_cents, :bigint 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/concerns/currency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Currency 4 | extend ActiveSupport::Concern 5 | 6 | SUPPORTED = %w[USD EUR GBP CAD HKD].freeze 7 | 8 | included do 9 | validates :currency, inclusion: {in: SUPPORTED}, allow_nil: true 10 | 11 | before_validation :set_currency 12 | end 13 | 14 | def set_currency 15 | self.currency ||= "USD" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20221205131817_create_consignment_inquiries.rb: -------------------------------------------------------------------------------- 1 | class CreateConsignmentInquiries < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :consignment_inquiries do |t| 4 | t.string :email, index: true 5 | t.string :gravity_user_id, index: true 6 | t.text :message 7 | t.string :name 8 | t.string :phone_number 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170717153030_add_approved_by_to_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddApprovedByToSubmission < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :submissions, :approved_by, :string 6 | add_column :submissions, :approved_at, :datetime 7 | add_column :submissions, :rejected_by, :string 8 | add_column :submissions, :rejected_at, :datetime 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class UsersController < RestController 5 | before_action :require_trusted_app 6 | def anonymize_user_email 7 | param! :email, String, required: true 8 | email = params[:email] 9 | UserService.anonymize_email!(email) 10 | render json: {result: "ok"}, status: :created 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/requests/system_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "support/gravity_helper" 5 | 6 | describe "System Up Endpoint" do 7 | describe "GET up" do 8 | it "returns valid JSON indicating the app is alive" do 9 | get "/system/up" 10 | expect(response.status).to eq 200 11 | expect(JSON.parse(response.body)["rails"]).to eq true 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fabricators/partner_submission_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:partner_submission) do 4 | submission { Fabricate(:submission) } 5 | partner { Fabricate(:partner) } 6 | end 7 | 8 | Fabricator(:consignment, from: :partner_submission) do 9 | submission { Fabricate(:submission) } 10 | partner { Fabricate(:partner) } 11 | accepted_offer { Fabricate(:offer, state: "accepted") } 12 | end 13 | -------------------------------------------------------------------------------- /app/graphql/resolvers/submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SubmissionResolver < BaseResolver 4 | include Resolvers::Submissionable 5 | 6 | def run 7 | check_submission_presence! 8 | 9 | unless matching_user(submission, @arguments&.[](:session_id)) || admin? || partner? 10 | raise GraphQL::ExecutionError, "Submission Not Found" 11 | end 12 | 13 | submission 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/admin/shared/_sort_label.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to filters.merge(sort: sort_field, direction: (filters[:direction] == 'desc' ? 'asc' : 'desc')) do %> 2 |
3 | <%= label %> 4 | 5 | <%= image_tag(image_url("#{filters[:sort] == sort_field && filters[:direction] || 'asc'}_arrow.svg"), id: 'arrow') %> 6 | 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /bin/pull_data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | environment="staging" 6 | dump_file="$environment.dump" 7 | target_database="convection_development" 8 | 9 | url=$(hokusai $environment env get DATABASE_URL | cut -d '=' -f 2) 10 | 11 | dropdb $target_database --if-exists 12 | createdb $target_database 13 | pg_dump $url -O -Fc -x -f $dump_file 14 | pg_restore $dump_file -Fc --no-owner -d $target_database 15 | 16 | rm $dump_file 17 | -------------------------------------------------------------------------------- /db/migrate/20211019105312_change_edition_size_type_to_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ChangeEditionSizeTypeToString < ActiveRecord::Migration[6.1] 4 | def self.up 5 | change_table :submissions do |t| 6 | t.change :edition_size, :string 7 | end 8 | end 9 | 10 | def self.down 11 | change_table :submissions do |t| 12 | t.change :edition_size, :integer 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/requests/admin/submissions/index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe "Submission Index" do 6 | describe "GET /" do 7 | it "returns the basic index page" do 8 | allow(ArtsyAdminAuth).to receive(:valid?).and_return(true) 9 | get "/" 10 | expect(response.status).to eq 301 11 | expect(response).to redirect_to "/admin" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSubmissionResolver < BaseResolver 4 | def run 5 | submission = 6 | SubmissionService.create_submission( 7 | @arguments, 8 | @context[:current_user], 9 | is_convection: false, 10 | access_token: @context[:jwt_token] 11 | ) 12 | 13 | {consignment_submission: submission} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/offer_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OfferResponse < ApplicationRecord 4 | INTENDED_STATES = [Offer::ACCEPTED, Offer::REJECTED, Offer::REVIEW].freeze 5 | 6 | belongs_to :offer, counter_cache: true 7 | 8 | validates :intended_state, inclusion: {in: INTENDED_STATES} 9 | validates :rejection_reason, 10 | inclusion: { 11 | in: Offer::REJECTION_REASONS 12 | }, 13 | allow_nil: true 14 | end 15 | -------------------------------------------------------------------------------- /app/views/admin/offers/_purchase.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Offer type 4 |
5 |
6 | <%= @offer.offer_type %> 7 |
8 |
9 | 10 |
11 |
12 | Price 13 |
14 |
15 | <%= @offer.price_display %> 16 |
17 |
18 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_listed_artworks.html.erb: -------------------------------------------------------------------------------- 1 | <% if @submission.listed_artwork_ids.any? %> 2 |
3 |
Listed Artworks
4 | <% @submission.listed_artwork_ids.each do |artwork_id| %> 5 | 8 | <% end %> 9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /db/migrate/20211201142602_remove_name_and_phone_from_user.rb: -------------------------------------------------------------------------------- 1 | class RemoveNameAndPhoneFromUser < ActiveRecord::Migration[6.1] 2 | def change 3 | change_table :users, bulk: true do |t| 4 | remove_column :users, :name, :string 5 | remove_column :users, :phone, :string 6 | remove_column :users, :session_id, :string 7 | end 8 | change_table :submissions, bulk: true do |t| 9 | t.string :session_id 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convection", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "sync-schema": "rake graphql:schema:idl && cp _schema.graphql ../metaphysics/src/data/convection.graphql", 6 | "prepare": "husky install" 7 | }, 8 | "devDependencies": { 9 | "husky": "^7.0.0", 10 | "lint-staged": "10.5.4", 11 | "prettier": "2.2.1" 12 | }, 13 | "lint-staged": { 14 | "*.{js,jsx,ts,tsx,css,md}": "prettier --write" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminUser < ApplicationRecord 4 | has_many :submissions, 5 | dependent: :nullify, 6 | inverse_of: :admin, 7 | foreign_key: "admin_id" 8 | 9 | validates :gravity_user_id, presence: true, uniqueness: true 10 | validates :name, presence: true, uniqueness: true 11 | 12 | scope :assignees, -> { where(assignee: true) } 13 | scope :cataloguers, -> { where(cataloguer: true) } 14 | end 15 | -------------------------------------------------------------------------------- /app/views/layouts/mailer_no_footer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 |
14 | <%= yield %> 15 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /app/views/admin/offer_responses/_offer_responses_section.html.erb: -------------------------------------------------------------------------------- 1 | <% if offer_responses.present? && offer_responses.any? %> 2 |
3 |
4 | Offer responses 5 |
6 | <% offer_responses.order(created_at: :desc).each do |offer_response| %> 7 | <%= render 'admin/offer_responses/offer_response_section', offer_response: offer_response %> 8 | <% end %> 9 |
10 | <% end %> -------------------------------------------------------------------------------- /db/migrate/20201111212135_add_offers_response_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOffersResponseModel < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :offer_responses do |t| 6 | t.references :offer, foreign_key: true, index: true 7 | t.string :intended_state, null: false 8 | t.string :phone_number 9 | t.text :comments 10 | t.string :rejection_reason 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20220125100834_add_external_id_to_submissions.rb: -------------------------------------------------------------------------------- 1 | class AddExternalIdToSubmissions < ActiveRecord::Migration[6.1] 2 | def change 3 | enable_extension "pgcrypto" unless extension_enabled?("pgcrypto") # need to enable pgcrypto to use `gen_random_uuid()` function 4 | 5 | add_column :submissions, 6 | :uuid, 7 | :uuid, 8 | default: "gen_random_uuid()", 9 | null: false 10 | add_index :submissions, :uuid, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 6 | 7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 8 | # Rails.backtrace_cleaner.remove_silencers! 9 | -------------------------------------------------------------------------------- /app/graphql/types/attribution_class_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class AttributionClassType < Types::BaseEnum 5 | # these values correspond to the attribution_class enum keys in the submission model 6 | 7 | value("UNIQUE", nil, value: "unique") 8 | value("LIMITED_EDITION", nil, value: "limited_edition") 9 | value("OPEN_EDITION", nil, value: "open_edition") 10 | value("UNKNOWN_EDITION", nil, value: "unknown_edition") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/views/admin/notes/_note.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 |

11 | <%= note.body %> 12 |

13 |
14 |
-------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define an application-wide HTTP permissions policy. For further 3 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 4 | # 5 | # Rails.application.config.permissions_policy do |f| 6 | # f.camera :none 7 | # f.gyroscope :none 8 | # f.microphone :none 9 | # f.usb :none 10 | # f.fullscreen :self 11 | # f.payment :self, "https://secure.example.com" 12 | # end 13 | -------------------------------------------------------------------------------- /db/migrate/20171218214858_add_reject_accept_fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRejectAcceptFields < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :offers, :rejection_reason, :string 6 | add_column :offers, :rejection_note, :text 7 | add_column :offers, :rejected_by, :string 8 | add_column :offers, :rejected_at, :datetime 9 | 10 | add_column :offers, :accepted_by, :string 11 | add_column :offers, :accepted_at, :datetime 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170823171400_add_partner_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPartnerSubmission < ActiveRecord::Migration[5.0] 4 | def change 5 | create_table :partner_submissions do |t| 6 | t.references :submission, foreign_key: true, index: true 7 | t.references :partner, foreign_key: true, index: true 8 | t.datetime :notified_at 9 | t.timestamps 10 | end 11 | 12 | rename_column :partners, :external_partner_id, :gravity_partner_id 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | 9 | Submission.create([ 10 | {artist_id: "4dc98ef49a96300001003179", title: "Foo", category: "Print", state: "submitted"} 11 | ]) 12 | -------------------------------------------------------------------------------- /app/models/partner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Partner < ApplicationRecord 4 | include PgSearch::Model 5 | 6 | default_scope { order("name ASC") } 7 | pg_search_scope :search_by_name, 8 | against: :name, 9 | using: { 10 | tsearch: { 11 | prefix: true 12 | } 13 | } 14 | 15 | has_many :partner_submissions, dependent: :destroy 16 | has_many :offers, through: :partner_submissions 17 | 18 | validates :gravity_partner_id, presence: true, uniqueness: true 19 | end 20 | -------------------------------------------------------------------------------- /app/services/offer_response_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OfferResponseService 4 | class OfferResponseError < StandardError 5 | end 6 | 7 | def self.create_offer_response(offer_id, offer_response_params = {}) 8 | offer = Offer.find(offer_id) 9 | 10 | offer_response = offer.offer_responses.new(offer_response_params) 11 | offer_response.save! 12 | offer_response 13 | rescue ActiveRecord::RecordNotFound => e 14 | raise OfferResponseError, e.message 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tasks/partners.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :partners do 4 | desc "Sends daily email to partners with newly approved submissons." 5 | task daily_digest: :environment do 6 | puts "[#{Time.now.utc}] Generating daily partner digest for #{Partner.count} partners ..." 7 | PartnerSubmissionService.daily_digest 8 | end 9 | 10 | task update: :environment do 11 | puts "[#{Time.now.utc}] Updating partners ..." 12 | PartnerUpdateService.update_partners_from_gravity 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/note.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Note < ApplicationRecord 4 | validates :body, presence: true 5 | belongs_to :submission 6 | belongs_to :user 7 | 8 | def author 9 | if defined?(@author) 10 | @author 11 | else 12 | @author = 13 | gravity_user_id && 14 | ( 15 | begin 16 | Gravity.client.user(id: gravity_user_id)._get 17 | rescue Faraday::ResourceNotFound 18 | nil 19 | end 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20210712125843_create_admin_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateAdminUsers < ActiveRecord::Migration[6.1] 4 | def change 5 | create_table :admin_users do |t| 6 | t.string :name, index: {unique: true} 7 | t.string :gravity_user_id, index: {unique: true} 8 | t.boolean :super_admin, index: true, default: false 9 | t.boolean :assignee, index: true, default: false 10 | t.boolean :cataloguer, index: true, default: false 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yarjuf" 4 | 5 | RSpec.configure do |config| 6 | config.filter_run focus: true 7 | config.run_all_when_everything_filtered = true 8 | 9 | config.expect_with :rspec do |expectations| 10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 11 | end 12 | 13 | config.mock_with :rspec do |mocks| 14 | mocks.verify_partial_doubles = true 15 | end 16 | 17 | config.expect_with :rspec do |c| 18 | c.syntax = %i[expect] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/graphql_helper.rb: -------------------------------------------------------------------------------- 1 | def stub_graphql_artwork_request(id = nil) 2 | mock_artwork_data = { 3 | condition: { 4 | displayText: "Excellent", description: "Like new" 5 | }, 6 | isFramed: true, 7 | framedHeight: 30, 8 | framedWidth: 30, 9 | framedMetric: "cm" 10 | } 11 | 12 | stub = stub_request(:post, Convection.config.metaphysics_api_url) 13 | stub.with(body: /#{Regexp.quote(id)}/) if id.present? 14 | stub.to_return(status: 200, body: {data: {artwork: mock_artwork_data}}.to_json, headers: {}) 15 | end 16 | -------------------------------------------------------------------------------- /lib/tasks/partner_submissions.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :partner_submissions do 4 | desc "Cancel all consignments that were open before a certain date" 5 | task :cancel_open, [:date] => :environment do |_task, args| 6 | raise "Please supply a date until which open consignments should be considered to canceled!" unless args[:date] 7 | 8 | datetime = args[:date].to_datetime 9 | PartnerSubmission.consigned.where("state = ? AND created_at < ?", "open", datetime).update_all(state: "canceled") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class UsersController < ApplicationController 5 | expose(:users) do 6 | matching_users = User.all 7 | if params[:term].present? 8 | matching_users = matching_users.search(params[:term]) 9 | end 10 | matching_users.page(page).per(size) 11 | end 12 | 13 | expose(:term) { params[:term] } 14 | 15 | def index 16 | respond_to { |format| format.json { render json: users || [] } } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/graphql/types/offer_response_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class OfferResponseType < Types::BaseObject 5 | description "Consignment Offer Response" 6 | 7 | field :id, ID, "Uniq ID for this offer response", null: false 8 | field :intended_state, Types::IntendedStateType, null: false 9 | 10 | field :phone_number, String, null: true 11 | field :comments, String, null: true 12 | field :rejection_reason, String, null: true 13 | 14 | field :offer, Types::OfferType, null: false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/initializers/unleash.rb: -------------------------------------------------------------------------------- 1 | module Convection 2 | mattr_accessor :unleash 3 | end 4 | 5 | Convection.unleash = 6 | Unleash::Client.new( 7 | url: Convection.config[:unleash_url], 8 | app_name: "convection", 9 | custom_http_headers: { 10 | Authorization: Convection.config[:unleash_token] 11 | }, 12 | disable_client: Convection.config[:unleash_url].blank?, 13 | instance_id: Socket.gethostname, 14 | logger: Rails.logger, 15 | environment: Rails.env, 16 | metrics_interval: 60, 17 | refresh_interval: 60 18 | ) 19 | -------------------------------------------------------------------------------- /app/graphql/types/date_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class DateType < Types::BaseScalar 5 | description "Date in YYYY-MM-DD format" 6 | DATE_FORMAT = "%Y-%m-%d" 7 | 8 | def self.coerce_input(input_value, _context) 9 | # Parse the incoming object into a DateTime 10 | Date.strptime(input_value, DATE_FORMAT) 11 | end 12 | 13 | def self.coerce_result(ruby_value, _context) 14 | # It's transported as a string, so stringify it 15 | ruby_value.strftime(DATE_FORMAT) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/metaql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | require "uri" 5 | 6 | class Metaql 7 | module Schema 8 | def self.execute(query:, access_token:, variables: {}) 9 | response = 10 | Net::HTTP.post( 11 | URI(Convection.config.metaphysics_api_url), 12 | {query: query, variables: variables}.to_json, 13 | "X-ACCESS-TOKEN" => access_token, 14 | "Content-Type" => "application/json" 15 | ) 16 | 17 | JSON.parse(response.body, symbolize_names: true) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/models/consignment_inquiry.rb: -------------------------------------------------------------------------------- 1 | class ConsignmentInquiry < ApplicationRecord 2 | # the email of user sending the inquiry 3 | validates :email, format: {with: URI::MailTo::EMAIL_REGEXP} 4 | # recipient_email is an optional email of a collector services team member to deliver the inquiry to. On Pulse if this is absent, the inquiry will be delivered to sell@artsy.net 5 | validates :recipient_email, format: {with: URI::MailTo::EMAIL_REGEXP}, allow_nil: true 6 | validates :name, presence: {message: "is required"} 7 | validates :message, presence: {message: "is required"} 8 | end 9 | -------------------------------------------------------------------------------- /hokusai/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | services: 4 | convection: 5 | command: ./hokusai/ci.sh 6 | environment: 7 | - RAILS_ENV=test 8 | - DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL=true 9 | - DATABASE_URL=postgresql://postgres:@convection-postgres/convection_test 10 | {% include 'templates/docker-compose-service.yml.j2' %} 11 | depends_on: 12 | - convection-postgres 13 | convection-postgres: 14 | image: postgres:14.12-alpine 15 | environment: 16 | - POSTGRES_DB=convection_test 17 | - POSTGRES_HOST_AUTH_METHOD=trust 18 | -------------------------------------------------------------------------------- /app/views/admin/assets/_gemini_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= multiple && 'multiple' %>/> 10 |
11 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = "1.0" 7 | 8 | # Add additional assets to the asset load path 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 13 | Rails.application.config.assets.precompile += %w[emails.css] 14 | -------------------------------------------------------------------------------- /lib/gravql.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | require "uri" 5 | 6 | class Gravql 7 | module Schema 8 | def self.execute(query:, variables: {}) 9 | response = 10 | Net::HTTP.post( 11 | URI("#{Convection.config.gravity_api_url}/graphql"), 12 | {query: query, variables: variables}.to_json, 13 | "X-XAPP-TOKEN" => Convection.config.gravity_xapp_token, 14 | "Content-Type" => "application/json" 15 | ) 16 | 17 | JSON.parse(response.body, symbolize_names: true) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) { wrap_parameters format: %i[json] } 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/helpers/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe ApplicationHelper, type: :helper do 6 | describe "markdown_formatted" do 7 | it "returns nil if passed a nil" do 8 | html = helper.markdown_formatted(nil) 9 | expect(html).to eq nil 10 | end 11 | 12 | it "returns parsed and rendered HTML" do 13 | text = "this is a note\r\nit has a line break" 14 | html = helper.markdown_formatted(text) 15 | expect(html).to eq "

this is a note\nit has a line break

\n" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20211116122633_add_session_id_to_user_and_set_gravity_id_nullable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddSessionIdToUserAndSetGravityIdNullable < ActiveRecord::Migration[6.1] 4 | def change 5 | reversible do |dir| 6 | change_table :users do |t| 7 | dir.up do 8 | t.change :gravity_user_id, :string, null: true 9 | t.string :session_id, null: true 10 | end 11 | 12 | dir.down do 13 | t.change :gravity_user_id, :string, null: false 14 | t.remove :session_id 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/gravql_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | HEADERS = { 4 | "X-XAPP-TOKEN" => "xapp_token", 5 | "Content-Type" => "application/json" 6 | }.freeze 7 | 8 | def stub_graphql_request(query, body) 9 | stub_request(:post, "#{Convection.config.gravity_api_url}/graphql") 10 | .to_return(body: body.to_json) 11 | .with(body: hash_including("query" => /#{query}/), headers: HEADERS) 12 | end 13 | 14 | def stub_gravql_artists(body:) 15 | stub_graphql_request("artistsDetails", body) 16 | end 17 | 18 | def stub_gravql_match_partners(body:) 19 | stub_graphql_request("matchPartners", body) 20 | end 21 | -------------------------------------------------------------------------------- /app/assets/images/desc_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /config/puma.config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env puma 2 | 3 | directory "/app" 4 | environment ENV.fetch("RAILS_ENV") { "development" } 5 | pidfile "/shared/pids/puma.pid" 6 | state_path "/shared/pids/puma.state" 7 | threads ENV.fetch("PUMA_THREAD_MIN") { 0 }.to_i, ENV.fetch("PUMA_THREAD_MAX") { 16 }.to_i 8 | bind ENV.fetch("PUMA_BIND") { "tcp://0.0.0.0:3000" } 9 | activate_control_app "unix:///shared/sockets/pumactl.sock", { no_token: true } 10 | workers ENV.fetch("PUMA_WORKERS") { 1 }.to_i 11 | worker_timeout ENV.fetch("PUMA_WORKER_TIMEOUT") { 60 }.to_i 12 | worker_boot_timeout ENV.fetch("PUMA_WORKER_BOOT_TIMEOUT") { 60 }.to_i 13 | prune_bundler 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | README.md 4 | 5 | # 6 | # OS X 7 | # 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # 13 | # Rails 14 | # 15 | .env* 16 | *.rbc 17 | capybara-*.html 18 | log 19 | tmp 20 | db/*.sqlite3 21 | db/*.sqlite3-journal 22 | public/system 23 | coverage/ 24 | spec/tmp 25 | **.orig 26 | rerun.txt 27 | 28 | # TODO Comment out these rules if you are OK with secrets being uploaded to the repo 29 | #config/initializers/secret_token.rb 30 | #config/secrets.yml 31 | 32 | ## Environment normalisation: 33 | .bundle 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | -------------------------------------------------------------------------------- /app/assets/javascripts/consignment.js.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | updateCanceledReasonDisplay = ($selectInput) -> 3 | if $selectInput.val() == 'canceled' 4 | $('.canceled-reason-field').removeClass('hidden') 5 | else 6 | $('.canceled-reason-field').addClass('hidden') 7 | 8 | if $('select[name="partner_submission[state]"]').length > 0 9 | $selectInput = $('select[name="partner_submission[state]"]') 10 | updateCanceledReasonDisplay($selectInput) 11 | 12 | # Show canceled reason input only if "canceled" is selected as the state 13 | $selectInput.on 'change', -> 14 | updateCanceledReasonDisplay($selectInput) 15 | -------------------------------------------------------------------------------- /app/assets/images/asc_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/graphql/mutations/add_user_to_submission_mutation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class AddUserToSubmissionMutation < Mutations::BaseMutation 5 | argument :id, ID, required: true 6 | 7 | field :consignment_submission, Types::SubmissionType, null: true 8 | 9 | def resolve(arguments) 10 | resolve_options = { 11 | arguments: arguments, 12 | context: context, 13 | object: object 14 | } 15 | 16 | resolver = AddUserToSubmissionResolver.new(**resolve_options) 17 | 18 | raise resolver.error unless resolver.valid? 19 | 20 | resolver.run 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/graphql/mutations/remove_asset_from_consignment_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class RemoveAssetFromConsignmentSubmission < Mutations::BaseMutation 5 | argument :sessionID, String, required: false 6 | argument :assetID, String, required: false 7 | 8 | field :asset, Types::AssetType, null: true 9 | 10 | def resolve(arguments) 11 | resolve_options = { 12 | arguments: arguments, 13 | context: context, 14 | object: object 15 | } 16 | resolver = RemoveAssetFromSubmissionResolver.new(**resolve_options) 17 | 18 | resolver.run 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_created_by.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Created By
3 |
4 | <%= creator_icon(@submission) %> 5 | <%= creator_title(@submission) %> 6 | <% if @submission.admin %> 7 | <%= mail_to @submission.admin.email, @submission.admin.email, class: "smaller-sidebar-link" %> 8 | <% elsif @submission.user&.gravity_user_id.present? %> 9 | <%= link_to @submission.name.presence || @submission.user&.gravity_user_id, user_management_url(@submission.user&.gravity_user_id), class: "smaller-sidebar-link" %> 10 | <% end %> 11 |
12 |
13 | -------------------------------------------------------------------------------- /app/graphql/types/base_field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class BaseField < GraphQL::Schema::Field 5 | argument_class Types::BaseArgument 6 | 7 | def initialize( 8 | *args, 9 | nilable_field: false, 10 | default_value: nil, 11 | **kwargs, 12 | &block 13 | ) 14 | super(*args, **kwargs, &block) 15 | 16 | extension(Extensions::NilableFieldExtension) if nilable_field 17 | 18 | unless default_value.nil? 19 | extension( 20 | Extensions::DefaultValueExtension, 21 | default_value: default_value 22 | ) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/jwt_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class JwtMiddleware 4 | def initialize(app) 5 | @app = app 6 | end 7 | 8 | def call(env) 9 | if env["HTTP_AUTHORIZATION"] 10 | token = parse_header env["HTTP_AUTHORIZATION"] 11 | env["JWT_TOKEN"] = token 12 | 13 | begin 14 | env["JWT_PAYLOAD"], _headers = 15 | JWT.decode(token, Convection.config.jwt_secret, "HS256") 16 | rescue JWT::DecodeError 17 | Rails.logger.info "Unable to verify JWT: #{token}" 18 | end 19 | end 20 | @app.call env 21 | end 22 | 23 | def parse_header(header) 24 | header.split.last 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/views/shared/email/_offer_note.html.erb: -------------------------------------------------------------------------------- 1 | <% if @offer.notes.present? %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 |
19 | 20 | 21 | <% end %> 22 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Adapted from https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server 4 | 5 | workers Integer(ENV["WEB_CONCURRENCY"] || 2) 6 | threads_count = Integer(ENV["RAILS_MAX_THREADS"] || 5) 7 | threads threads_count, threads_count 8 | 9 | preload_app! 10 | 11 | rackup DefaultRackup 12 | port ENV["PORT"] || 3_000 13 | environment ENV["RACK_ENV"] || "development" 14 | 15 | on_worker_boot do 16 | # Worker specific setup for Rails 4.1+ 17 | # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot 18 | ActiveRecord::Base.establish_connection 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class GraphqlController < BaseController 5 | def execute 6 | result = 7 | ConvectionSchema.execute(query, variables: variables, context: context) 8 | render json: result, status: :ok 9 | end 10 | 11 | private 12 | 13 | def query 14 | params[:query] 15 | end 16 | 17 | def variables 18 | params[:variables] 19 | end 20 | 21 | def context 22 | { 23 | current_application: current_app, 24 | current_user: current_user, 25 | current_user_roles: current_user_roles, 26 | jwt_token: jwt_token 27 | } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/graphql/resolvers/remove_asset_from_submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveAssetFromSubmissionResolver < BaseResolver 4 | def run 5 | asset = Asset.find_by(id: @arguments[:asset_id]) 6 | raise(GraphQL::ExecutionError, "Asset Not Found") unless asset 7 | 8 | submission = asset.submission 9 | raise(GraphQL::ExecutionError, "Submission Not Found") unless submission 10 | 11 | unless matching_user(submission, @arguments&.[](:session_id)) || admin? 12 | raise(GraphQL::ExecutionError, "Submission Not Found") 13 | end 14 | 15 | asset.destroy! 16 | 17 | SubmissionService.notify_user(submission.id) if submission.submitted? 18 | 19 | {asset: asset} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/admin/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class DashboardController < ApplicationController 5 | include GraphqlHelper 6 | 7 | expose(:grouped_submissions) do 8 | DashboardReportingQuery::Submission.grouped_by_state 9 | end 10 | 11 | expose(:unreviewed_submissions) do 12 | DashboardReportingQuery::Submission.unreviewed_user_submissions( 13 | @current_user 14 | ) 15 | end 16 | 17 | expose(:grouped_offers) { DashboardReportingQuery::Offer.grouped_by_state } 18 | 19 | expose(:grouped_consignments) do 20 | DashboardReportingQuery::Consignment.grouped_by_state_and_partner 21 | end 22 | 23 | def index 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fabricators/submission_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:submission) do 4 | user { Fabricate(:user) } 5 | artist_id { Fabricate.sequence(:artist_id) } 6 | my_collection_artwork_id { Fabricate.sequence(:my_collection_artwork_id) } 7 | uuid { SecureRandom.uuid } 8 | title { Fabricate.sequence(:title) { |i| "The Last Supper #{i}" } } 9 | year 2_010 10 | medium "oil on paper" 11 | category { Submission::CATEGORIES.first } 12 | height 10 13 | width 12 14 | dimensions_metric "in" 15 | signature false 16 | authenticity_certificate false 17 | location_address "123 Fake St" 18 | location_address2 "Apt 2" 19 | location_city "New York" 20 | location_state "New York" 21 | location_country "USA" 22 | end 23 | -------------------------------------------------------------------------------- /app/services/partner_update_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PartnerUpdateService 4 | class << self 5 | def update_partners_from_gravity 6 | Partner.all.each { |partner| delay.update_partner!(partner.id) } 7 | end 8 | 9 | def update_partner!(partner_id) 10 | partner = Partner.find_by(id: partner_id) 11 | if partner.blank? 12 | Rails.logger.info "No partner found for #{partner_id}" 13 | return 14 | end 15 | 16 | gravity_partner = Gravity.client.partner(id: partner.gravity_partner_id) 17 | new_partner_name = gravity_partner.name 18 | unless partner.name == new_partner_name 19 | partner.update!(name: new_partner_name) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/graphql/types/consignment_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class ConsignmentType < Types::BaseObject 5 | description "Consignment" 6 | 7 | field :submission_id, ID, null: false 8 | field :submissionID, ID, null: true, method: :submission_id # Alias for MPv2 compatability 9 | field :sale_date, String, null: true 10 | field :sale_name, String, null: true 11 | field :state, Types::ConsignmentStateType, null: true 12 | field :id, ID, "Uniq ID for this consignment", null: false 13 | field :currency, String, null: true 14 | field :internalID, ID, null: true, method: :id 15 | field :sale_price_cents, Integer, null: true 16 | field :submission, Types::SubmissionType, null: false 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, '\1en' 9 | # inflect.singular /^(ox)en/i, '\1' 10 | # inflect.irregular 'person', 'people' 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym 'RESTful' 17 | # end 18 | -------------------------------------------------------------------------------- /lib/gravity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Gravity 4 | class << self 5 | def client 6 | @client ||= 7 | Hyperclient.new(Convection.config.gravity_api_url) do |client| 8 | client.headers["X-XAPP-TOKEN"] = Convection.config.gravity_xapp_token 9 | client.headers["ACCEPT"] = "application/vnd.artsy-v2+format" 10 | end 11 | end 12 | 13 | def fetch_all(object, link_sym, params = {}) 14 | items = [] 15 | cursor = object.send(link_sym, params)._get 16 | loop do 17 | new_items = cursor.try(link_sym) 18 | items += new_items if new_items 19 | cursor = cursor.try(:next) 20 | break if new_items.blank? 21 | end 22 | items 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /config/initializers/datadog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ddtrace" 4 | 5 | Datadog.configure do |c| 6 | c.tracer( 7 | enabled: Convection.config[:datadog_trace_agent_hostname].present?, 8 | hostname: Convection.config[:datadog_trace_agent_hostname], 9 | distributed_tracing: true, 10 | debug: Convection.config[:datadog_debug] 11 | ) 12 | c.use :rails, 13 | service_name: "convection", 14 | controller_service: "convection.controller", 15 | cache_service: "convection.cache" 16 | c.use :redis, service_name: "convection.redis" 17 | c.use :http, service_name: "convection.http", distributed_tracing: true 18 | c.use :sidekiq, service_name: "convection.sidekiq" 19 | c.use :sneakers, service_name: "convection.sneakers" 20 | end 21 | -------------------------------------------------------------------------------- /hokusai/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | retry() { 9 | max_attempts="${1}"; shift 10 | retry_delay_seconds="${1}"; shift 11 | cmd="${@}" 12 | attempt_num=1 13 | 14 | until ${cmd}; do 15 | (( attempt_num >= max_attempts )) && { 16 | echo "Attempt ${attempt_num} failed and there are no more attempts left!" 17 | return 1 18 | } 19 | echo "Attempt ${attempt_num} failed! Trying again in ${retry_delay_seconds} seconds..." 20 | attempt_num=$[ attempt_num + 1 ] 21 | sleep ${retry_delay_seconds} 22 | done 23 | 24 | bundle exec rake db:schema:load 25 | } 26 | 27 | retry 1>&2 ${MAX_ATTEMPTS:-5} ${RETRY_DELAY_SECONDS:-1} psql ${DATABASE_URL} -c '\l' 28 | 29 | bundle exec rake 30 | -------------------------------------------------------------------------------- /lib/s3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class S3 4 | DEFAULT_EXPIRES_IN = 180 # seconds 5 | 6 | attr_accessor :aws_client, :s3 7 | 8 | def initialize 9 | @aws_client = Aws::S3::Client.new(region: Convection.config[:aws_region], access_key_id: Convection.config[:aws_access_key_id], secret_access_key: Convection.config[:aws_secret_access_key]) 10 | @s3 = Aws::S3::Resource.new(client: @aws_client) 11 | end 12 | 13 | def object(bucket:, object_path:) 14 | bucket = s3.bucket(bucket) 15 | bucket.object(object_path) 16 | end 17 | 18 | def presigned_url(bucket:, object_path:, expires_in: DEFAULT_EXPIRES_IN) 19 | bucket = s3.bucket(bucket) 20 | obj = bucket.object(object_path) 21 | obj.presigned_url(:get, expires_in: 60) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/services/partner_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PartnerService 4 | class << self 5 | def fetch_partner_contacts!(partner) 6 | gravity_partner_id = partner.gravity_partner_id 7 | partner_communication = 8 | Gravity 9 | .client 10 | .partner_communications( 11 | name: Convection.config.consignment_communication_name 12 | ) 13 | .first 14 | 15 | partner_contacts = 16 | Gravity.fetch_all( 17 | partner_communication, 18 | :partner_contacts, 19 | partner_id: gravity_partner_id 20 | ) 21 | 22 | raise "No contacts for #{partner.id}" unless partner_contacts.any? 23 | 24 | partner_contacts.map(&:email) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/detect-secrets: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | NO_COLOR='\033[0m' 8 | 9 | HELP="${RED}command not found: detect-secrets${NO_COLOR} 10 | 11 | To install the command line tool re-run: ${GREEN}https://github.com/artsy/potential/blob/main/scripts/setup${NO_COLOR} 12 | To learn more about this tool: ${GREEN}https://www.notion.so/artsy/Detect-Secrets-cd11d994dabf45f6a3c18e07acb5431c${NO_COLOR} 13 | 14 | You can bypass this hook using --no-verify option. ${RED}USE AT YOUR OWN RISK!${NO_COLOR}" 15 | 16 | if which detect-secrets > /dev/null; test $? != 0; then 17 | echo "${HELP}" 18 | exit 1 19 | else 20 | detect-secrets-hook --baseline .secrets.baseline $(git diff --staged --name-only) 21 | fi 22 | echo "${GREEN}No secrets detected!${NO_COLOR}" 23 | -------------------------------------------------------------------------------- /app/graphql/resolvers/update_submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdateSubmissionResolver < BaseResolver 4 | include Resolvers::Submissionable 5 | 6 | def run 7 | check_submission_presence! 8 | 9 | unless matching_user(submission, @arguments&.[](:session_id)) || admin? 10 | raise(GraphQL::ExecutionError, "Submission Not Found") 11 | end 12 | 13 | SubmissionService.update_submission( 14 | submission, 15 | @arguments.except(:id, :external_id, :session_id), 16 | current_user: nil, 17 | is_convection: false, 18 | access_token: @context[:jwt_token] 19 | ) 20 | 21 | {consignment_submission: submission} 22 | end 23 | 24 | def valid? 25 | @error = compute_error 26 | @error.nil? 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | *= require select2 16 | */ 17 | -------------------------------------------------------------------------------- /app/views/admin/partner_submissions/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Submissions for <%= @partner.name %> included in next digest

3 |
4 | 5 |
6 |
7 |
8 |
9 | <% @partner_submissions.map(&:submission).each do |submission| %> 10 | <%= render 'admin/submissions/submission', submission: submission, artist: @artist_details&.dig(submission.artist_id) %> 11 | <% end %> 12 |
13 |
14 |
15 | <%= render 'shared/watt/paginator', total_items_count: @partner_submissions.total_count, items_count: @partner_submissions.length, per_page: size, current_page: page, base_url: admin_partner_submissions_url(@filters) %> 16 |
17 | -------------------------------------------------------------------------------- /app/models/concerns/reload_uuid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This module adds a reload UUID functionality to a model. Without it: 4 | # 5 | # submission = Submission.create! 6 | # submission.uuid # => nil 7 | # submission.reload.uuid # => 03f32677-8c0c-4e86-b722-5ed3d53a087c 8 | # 9 | # With it: 10 | # 11 | # submission = Submission.create! 12 | # submission.uuid # => 03f32677-8c0c-4e86-b722-5ed3d53a087c 13 | # 14 | # See this discussion for details: https://github.com/rails/rails/issues/17605 15 | module ReloadUuid 16 | extend ActiveSupport::Concern 17 | 18 | included do 19 | after_commit :reload_uuid, on: :create 20 | 21 | def reload_uuid 22 | if attributes.has_key? "uuid" 23 | self[:uuid] = self.class.where(id: id).pick(:uuid) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/types/sort_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Types::SortType do 6 | describe "prepare" do 7 | context "with an ascending sort" do 8 | it "returns the correct sort column and direction" do 9 | prepare_callback = Types::SortType.prepare 10 | sort_order = prepare_callback.call("CREATED_AT_ASC", nil) 11 | expect(sort_order).to eq("created_at" => "asc") 12 | end 13 | end 14 | 15 | context "with a descending sort" do 16 | it "returns the correct sort column and direction" do 17 | prepare_callback = Types::SortType.prepare 18 | sort_order = prepare_callback.call("CREATED_AT_DESC", nil) 19 | expect(sort_order).to eq("created_at" => "desc") 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "config/application" 4 | require "graphql/rake_task" 5 | require "sneakers/tasks" 6 | 7 | Rails.application.load_tasks 8 | 9 | GraphQL::RakeTask.new(schema_name: "ConvectionSchema", idl_outfile: "_schema.graphql") 10 | 11 | if Rails.env.development? || Rails.env.test? 12 | desc "prints out the schema file" 13 | task print_schema: :environment do 14 | require "graphql/schema/printer" 15 | puts GraphQL::Schema::Printer.new(ConvectionSchema).print_schema 16 | end 17 | 18 | desc "check for schema drift" 19 | task schema_check: :environment do 20 | system "./bin/schema_check" 21 | abort "schema-check failed" unless $CHILD_STATUS.exitstatus.zero? 22 | end 23 | 24 | Rake::Task[:default].clear 25 | task default: %i[schema_check standard spec] 26 | end 27 | -------------------------------------------------------------------------------- /app/views/shared/email/_submission.html.erb: -------------------------------------------------------------------------------- 1 |

Submission ID:
<%= @submission.id %>

2 | 3 |

Artist:
<%= @artist.name %>

4 | 5 |

Title:
<%= @submission.title %>

6 | 7 |

Category:
<%= @submission.category %>

8 | 9 |

Medium:
<%= @submission.medium %>

10 | 11 |

Year:
<%= @submission.year %>

12 | 13 |

Signed?:
<%= @submission.signature %>

14 | 15 | <% if @submission.provenance %> 16 |

Provenance:
<%= @submission.provenance %>

17 | <% end %> 18 | 19 |

Location:
<%= formatted_location(@submission) %>

20 | 21 | <% if formatted_dimensions(@submission) %> 22 |

Dimensions:
<%= formatted_dimensions(@submission) %>

23 | <% end %> 24 | 25 | <% @submission.processed_images.each do |image| %> 26 | <%= image_tag image.image_urls['square'] %> 27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/services/consignment_inquiry_service.rb: -------------------------------------------------------------------------------- 1 | class ConsignmentInquiryService 2 | def self.post_consignment_created_event(consignment_inquiry) 3 | Artsy::EventPublisher.publish( 4 | "consignments", 5 | "consignmentinquiry.created", 6 | verb: "created", 7 | subject: {id: consignment_inquiry.gravity_user_id}, 8 | object: {id: consignment_inquiry.id.to_s, root_type: consignment_inquiry.class.name}, 9 | properties: { 10 | email: consignment_inquiry.email, # email of user sending inquiry 11 | recipient_email: consignment_inquiry.recipient_email, # optional email of team member (or Pulse will deliver to sell@artsy.net if absent) 12 | message: consignment_inquiry.message, 13 | name: consignment_inquiry.name, 14 | phone_number: consignment_inquiry.phone_number 15 | } 16 | ) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/admin/partner_submissions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class PartnerSubmissionsController < ApplicationController 5 | include GraphqlHelper 6 | 7 | def index 8 | @filters = {notified_at: params[:notified_at]} 9 | notified_at = params[:notified_at].presence 10 | 11 | @partner = Partner.find(params[:partner_id]) 12 | 13 | partner_submissions = @partner.partner_submissions 14 | if params[:notified_at] 15 | partner_submissions = 16 | partner_submissions.where(notified_at: notified_at) 17 | end 18 | @partner_submissions = partner_submissions.page(page).per(size) 19 | 20 | artists_ids = 21 | @partner_submissions.filter_map { |p_s| p_s.submission&.artist_id } 22 | @artist_details = artists_names_query(artists_ids) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/graphql/types/asset_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class AssetType < Types::BaseObject 5 | description "Submission Asset" 6 | 7 | field :id, ID, "Uniq ID for this asset", null: false 8 | field :asset_type, String, "type of this Asset", null: false, method: :document_path 9 | field :document_path, String, "path to document", null: true 10 | field :gemini_token, String, "gemini token for asset", null: true 11 | field :image_urls, GraphQL::Types::JSON, "known image urls", null: true 12 | field :filename, String, "original image name", null: true 13 | field :size, String, null: true 14 | field :submission_id, ID, null: false 15 | field :submissionID, ID, null: true, method: :submission_id # Alias for MPv2 compatability 16 | field :s3_path, String, null: true 17 | field :s3_bucket, String, null: true 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/fabricators/asset_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Fabricator(:asset) 4 | 5 | Fabricator(:image, from: :asset) do 6 | asset_type "image" 7 | gemini_token { Fabricate.sequence(:gemini_token) { |i| "gemini-#{i}" } } 8 | image_urls do 9 | Fabricate.sequence(:gemini_token) do |i| 10 | { 11 | square: "https://placekitten.com/#{i}/#{i}.jpg", 12 | thumbnail: "https://placekitten.com/#{i}/#{i}.jpg", 13 | large: "https://placekitten.com/#{i}/#{i}.jpg" 14 | } 15 | end 16 | end 17 | end 18 | 19 | Fabricator(:unprocessed_image, from: :asset) do 20 | asset_type "image" 21 | gemini_token { Fabricate.sequence(:gemini_token) { |i| "gemini-#{i}" } } 22 | end 23 | 24 | Fabricator(:additional_file, from: :asset) do 25 | asset_type "additional_file" 26 | s3_bucket "bucket" 27 | s3_path "path" 28 | filename "filename.pdf" 29 | end 30 | -------------------------------------------------------------------------------- /app/graphql/types/sort_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class SortType < Types::BaseEnum 5 | DIRECTIONS = %w[asc desc].freeze 6 | 7 | def self.generate_values(columns) 8 | columns.sort.each do |column_name| 9 | asc_value, desc_value = 10 | DIRECTIONS.map do |direction| 11 | [column_name, direction].join("_").upcase 12 | end 13 | 14 | value asc_value, "sort by #{column_name} in ascending order" 15 | value desc_value, "sort by #{column_name} in descending order" 16 | end 17 | end 18 | 19 | def self.prepare 20 | lambda do |sort, _context| 21 | return unless sort 22 | 23 | match_data = sort.downcase.match(/(.*)_(asc|desc)/) 24 | column_name, direction = match_data.captures 25 | {column_name => direction} 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/tasks/admin_user.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :admin_user do 4 | task load_admins_emails: :environment do 5 | puts "Admins email addresses loading started." 6 | 7 | AdminUser.all.find_each do |admin_user| 8 | next if admin_user.email.present? 9 | 10 | user = ( 11 | begin 12 | Gravity.client.user(id: admin_user.gravity_user_id).user_detail._get 13 | rescue Faraday::ResourceNotFound 14 | puts "Can't find user with the id: #{admin_user.gravity_user_id}." 15 | nil 16 | end 17 | ) 18 | 19 | if user&.email.blank? 20 | puts "Can't find email for the user: #{admin_user.gravity_user_id}." 21 | next 22 | end 23 | 24 | admin_user.email = user&.email 25 | admin_user.save! 26 | end 27 | 28 | puts "Admins email addresses loading finished." 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | group :livereload do 4 | guard "livereload", port: "5003", grace_period: 0.5 do 5 | watch(%r{app/assets/.+}) 6 | watch(%r{app/controllers/.+}) 7 | watch(%r{app/helpers/.+}) 8 | watch(%r{app/views/.+}) 9 | end 10 | end 11 | 12 | group :rspec do 13 | guard :rspec, cmd: "bundle exec rspec" do 14 | watch("spec/spec_helper.rb") { "spec" } 15 | watch("app/controllers/application_controller.rb") { "spec/controllers" } 16 | watch(%r{^spec/.+_spec\.rb$}) 17 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 18 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 19 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 20 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/graphql/mutations/create_offer_response_mutation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class CreateOfferResponseMutation < Mutations::BaseMutation 5 | argument :offer_id, ID, required: true 6 | argument :intended_state, Types::IntendedStateType, required: true 7 | 8 | argument :phone_number, String, required: false 9 | argument :comments, String, required: false 10 | argument :rejection_reason, String, required: false 11 | 12 | field :consignment_offer_response, Types::OfferResponseType, null: true 13 | 14 | def resolve(arguments) 15 | resolve_options = { 16 | arguments: arguments, 17 | context: context, 18 | object: object 19 | } 20 | resolver = CreateOfferResponseResolver.new(**resolve_options) 21 | raise resolver.error unless resolver.valid? 22 | 23 | resolver.run 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/workers/sneakers/my_collection_artworks_worker.rb: -------------------------------------------------------------------------------- 1 | class MyCollectionArtworksWorker 2 | include Sneakers::Worker 3 | 4 | from_queue "convection_my_collection_artworks", 5 | exchange: "my_collection_artworks", 6 | exchange_type: :topic, 7 | routing_key: ["my_collection_artwork.updated"] 8 | 9 | def work_with_params(message, delivery_info, _metadata) 10 | payload = JSON.parse(message, symbolize_names: true) 11 | logger.info("RABBITMQ: [MyCollectionArtworksWorker] received a message: routing key - #{delivery_info[:routing_key]}") 12 | 13 | case delivery_info[:routing_key] 14 | when "my_collection_artwork.updated" 15 | MyCollectionArtworkUpdatedService.new(payload).notify_admin! 16 | else 17 | logger.info("RABBITMQ: [MyCollectionArtworksWorker] ignoring a message: routing key - #{delivery_info[:routing_key]}") 18 | end 19 | 20 | ack! 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tasks/offers.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :offers do 4 | desc "Lapse all offers that were sent before a certain date" 5 | task :lapse_sent, [:date] => :environment do |_task, args| 6 | raise "Please supply a date until which sent offers should be considered to lapse!" unless args[:date] 7 | 8 | datetime = args[:date].to_datetime 9 | Offer.where("state = ? AND (sent_at < ? OR sent_at IS NULL)", "sent", datetime).update_all(state: "lapsed") 10 | end 11 | 12 | desc "Lapse all offers that were reviewed before a certain date" 13 | task :lapse_review, [:date] => :environment do |_task, args| 14 | raise "Please supply a date until which reviewed offers should be considered to lapse!" unless args[:date] 15 | 16 | datetime = args[:date].to_datetime 17 | Offer.where("state = ? AND review_started_at < ?", "review", datetime).update_all(state: "lapsed") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/concerns/percentize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Percentize 4 | extend ActiveSupport::Concern 5 | 6 | # Call like: 7 | # percentize :commission_percent # => defines commission_percent_whole 8 | # 9 | class_methods do 10 | def percentize(*method_names) 11 | method_names.each do |method_name| 12 | attribute "#{method_name}_whole".to_sym 13 | 14 | define_method "#{method_name}_whole" do 15 | return if self[method_name].blank? 16 | 17 | (self[method_name] * 100).round(2) 18 | end 19 | 20 | define_method "#{method_name}_whole=" do |percent_whole| 21 | if percent_whole.blank? 22 | self[method_name] = nil 23 | else 24 | percentage = percent_whole.to_f / 100 25 | self[method_name] = percentage 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/admin/offers/_net_price.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Offer type 4 |
5 |
6 | <%= @offer.offer_type %> 7 |
8 |
9 | 10 |
11 |
12 | Price 13 |
14 |
15 | <%= @offer.price_display %> 16 |
17 |
18 | 19 |
20 |
21 | Sale period start 22 |
23 |
24 | <%= @offer.sale_period_start&.strftime('%b %-d %Y') %> 25 |
26 |
27 | 28 |
29 |
30 | Sale period end 31 |
32 |
33 | <%= @offer.sale_period_end&.strftime('%b %-d %Y') %> 34 |
35 |
36 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_offer_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateOfferResolver < BaseResolver 4 | def valid? 5 | return true if admin? || trusted_application? 6 | 7 | bad_argument_error = 8 | GraphQL::ExecutionError.new("Can't access createConsignmentOffer") 9 | @error = bad_argument_error 10 | false 11 | end 12 | 13 | def run 14 | offer = 15 | OfferService.create_offer(submission_id, partner_id, offer_attributes) 16 | 17 | {consignment_offer: offer} 18 | end 19 | 20 | private 21 | 22 | def submission_id 23 | @arguments[:submission_id] 24 | end 25 | 26 | def partner_id 27 | gravity_partner_id = @arguments[:gravity_partner_id] 28 | partner = Partner.find_by(gravity_partner_id: gravity_partner_id) 29 | partner.id 30 | end 31 | 32 | def offer_attributes 33 | @arguments.except(:submission_id, :gravity_partner_id) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/views/admin/offers/_net_price_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 | <%= f.text_field :price_dollars, class: 'form-control' %> 8 |
9 |
10 |
11 | 12 |
13 |
14 | 17 |
18 | <%= f.date_field :sale_period_start, class: 'form-control' %> 19 |
20 |
21 |
22 | 23 |
24 |
25 | 28 |
29 | <%= f.date_field :sale_period_end, class: 'form-control' %> 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /docs/scripts.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | In the `/bin` folder there are a number of scripts to help one work with 4 | convection, here are some basic descriptions of the ones we've added: 5 | 6 | * `console` - open a rails console with proper env setup 7 | * `pull_data` - pull postgres data from staging 8 | * `server` - start a server with proper env setup 9 | * `worker` - start a worker with proper env setup 10 | 11 | To envoke any of these tasks, you can do this: 12 | 13 | ``` 14 | $ ./bin/console 15 | ``` 16 | 17 | So very friendly to scripting. 18 | 19 | ## Pull Data from Staging 20 | 21 | In order to get one's local postgres database in sync with staging, you can run 22 | the `pull_data` task: 23 | 24 | ``` 25 | $ ./bin/pull_data 26 | ``` 27 | 28 | This will: 29 | 30 | * drop your dev database 31 | * create an empty dev database 32 | * dump the staging database 33 | * restore the staging database 34 | 35 | Note: you will need to be connected the VPN in order for this to work. 36 | -------------------------------------------------------------------------------- /config/initializers/sneakers.rb: -------------------------------------------------------------------------------- 1 | Sneakers.configure( 2 | connection: Bunny.new( 3 | Convection.config.rabbitmq_consume_url, 4 | tls: Convection.config.rabbitmq_client_cert.present?, 5 | verify_peer: Convection.config.rabbitmq_verify_peer, 6 | tls_cert: Convection.config.rabbitmq_client_cert, 7 | tls_key: Convection.config.rabbitmq_client_key, 8 | tls_ca_certificates: ([Convection.config.rabbitmq_ca_cert] if Convection.config.rabbitmq_ca_cert.present?), 9 | logger: Rails.logger, 10 | automatically_recover: Convection.config.sneakers_auto_recover 11 | ), # Overrides connection preferences with our own bunny (necessary for tls) 12 | log: Rails.logger, 13 | daemonize: false, 14 | pid_path: "tmp/pids/sneakers.pid", 15 | workers: 1, 16 | threads: ENV.fetch("RAILS_MAX_THREADS", 5), # match default database pool size. 17 | prefetch: ENV.fetch("RAILS_MAX_THREADS", 5) # match threads size (docs recommend this) 18 | ) 19 | Sneakers.logger.level = Logger::INFO 20 | -------------------------------------------------------------------------------- /app/mailers/admin_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminMailer < ApplicationMailer 4 | helper :submissions, :url 5 | 6 | def submission(submission:, user:, artist:) 7 | warn_submissions_suspended(name: __method__, submission_id: submission&.id) 8 | end 9 | 10 | def submission_approved(submission:, artist:) 11 | warn_submissions_suspended(name: __method__, submission_id: submission&.id) 12 | end 13 | 14 | def submission_resubmitted(submission:, artist:) 15 | warn_submissions_suspended(name: __method__, submission_id: submission&.id) 16 | end 17 | 18 | def artwork_updated(submission:, artwork_data:, changes: nil, image_added: nil) 19 | warn_submissions_suspended(name: __method__, submission_id: submission&.id) 20 | end 21 | 22 | def warn_submissions_suspended(name:, submission_id:) 23 | Rails.logger.warn "[Consignments suspended] Declining to deliver admin email `#{name}` for Submission #{submission_id || ""}" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/views/admin/assets/_s3_form.html.erb: -------------------------------------------------------------------------------- 1 | <% signer = S3UploadSigner.new.as_json %> 2 | 3 |
enctype='multipart/form-data' method='post' class='single-padding-top' }> 4 | /> 5 | /> 6 | /> 7 | /> 8 | /> 9 | 10 | /> 11 | <%= multiple && 'multiple' %>/> 12 |
13 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_collector_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Collector info
3 |
4 | <% if @partner_name %> 5 |
6 | <%= @partner_name%> 7 |
8 | <% end %> 9 |
10 | <% if submission.user %> 11 | <%= link_to submission.name.presence || submission.user.gravity_user_id, user_management_url(submission.user.gravity_user_id), class: "smaller-sidebar-link" %> 12 | <% else %> 13 | <%= submission.name %> 14 | <% end %> 15 |
16 | 19 |
20 | <%= submission.email %> 21 |
22 |
23 | <%= submission.phone %> 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | github_user=$(git config --global --get github.user) 6 | labels="Merge On Green,lib update" 7 | team="artsy/collector-experience" 8 | 9 | message=$(cat <<"END" 10 | Library Updates 11 | 12 | This commit updates our libraries like this: 13 | 14 | ``` 15 | $ bundle update 16 | $ yarn upgrade --latest 17 | ``` 18 | 19 | It was automatically run with this script: 20 | 21 | ``` 22 | $ ./bin/update 23 | ``` 24 | END 25 | ) 26 | 27 | current_branch=$(git branch --show-current) 28 | if [ "$current_branch" != "main" ]; then 29 | echo "must be on main branch" 30 | exit 1 31 | fi 32 | 33 | today=$(date +'%Y-%m-%d') 34 | branch_name="updates-$today" 35 | 36 | git fetch --all --quiet 37 | git checkout -b $branch_name 38 | bundle update 39 | yarn upgrade --latest 40 | bundle exec rake 41 | git add . 42 | git commit --message "$message" 43 | git push origin $branch_name 44 | hub pull-request --message "$message" --reviewer $team --assign $github_user --labels "$labels" 45 | -------------------------------------------------------------------------------- /db/migrate/20170210232121_create_submissions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSubmissions < ActiveRecord::Migration[5.0] 4 | def change 5 | create_table :submissions do |t| 6 | t.string :user_id, index: true 7 | t.boolean :qualified 8 | t.string :artist_id 9 | t.string :title 10 | t.string :medium 11 | t.string :year 12 | t.string :category 13 | t.string :height 14 | t.string :width 15 | t.string :depth 16 | t.string :dimensions_metric 17 | t.boolean :signature 18 | t.boolean :authenticity_certificate 19 | t.text :provenance 20 | t.string :location_city 21 | t.string :location_state 22 | t.string :location_country 23 | t.date :deadline_to_sell 24 | t.text :additional_info 25 | t.timestamps 26 | end 27 | 28 | create_table :assets do |t| 29 | t.string :asset_type 30 | t.string :gemini_token 31 | t.string :image_urls 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/artsy_admin_auth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ArtsyAdminAuth 4 | CONSIGNEMNTS_MANAGER = "consignments_manager" 5 | CONSIGNMENTS_REPRESENTATIVE = "consignments_representative" 6 | 7 | class << self 8 | def decode_token(token) 9 | return nil if token.blank? 10 | 11 | decoded_token, _headers = JWT.decode(token, Convection.config.jwt_secret) 12 | decoded_token 13 | end 14 | 15 | def valid?(token, additional_roles = []) 16 | allowed_roles = [CONSIGNEMNTS_MANAGER] + additional_roles 17 | decoded_token = decode_token(token) 18 | return false if decoded_token.nil? 19 | 20 | roles = decoded_token.fetch("roles", "").split(",") 21 | allowed_roles.any? { |role| roles.include?(role) } 22 | end 23 | 24 | def decode_user(token) 25 | decoded_token = decode_token(token) 26 | decoded_token&.fetch("sub", nil) 27 | end 28 | 29 | def consignments_manager?(token) 30 | valid?(token) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/assets/images/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lock 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/graphql/resolvers/add_user_to_submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUserToSubmissionResolver < BaseResolver 4 | include Resolvers::Submissionable 5 | 6 | def run 7 | check_submission_presence! 8 | 9 | # early no-op if already claimed by the current user 10 | if submitted_by_current_user?(submission) 11 | return {consignment_submission: submission} 12 | end 13 | 14 | if submission.user_id 15 | raise(GraphQL::ExecutionError, "Submission already has a user") 16 | end 17 | 18 | if submission.state != Submission::DRAFT 19 | raise(GraphQL::ExecutionError, "Submission must be in a draft state to claim") 20 | end 21 | 22 | SubmissionService.add_user_to_submission( 23 | submission, 24 | @context[:current_user], 25 | @context[:jwt_token] 26 | ) 27 | 28 | {consignment_submission: submission} 29 | end 30 | 31 | def submission 32 | @submission ||= Submission.find_by(uuid: submission_id) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /db/migrate/20171207210014_add_offers_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddOffersTable < ActiveRecord::Migration[5.0] 4 | def change 5 | create_table :offers do |t| 6 | t.references :partner_submission, foreign_key: true, index: true 7 | t.string :offer_type 8 | t.datetime :sale_period_start 9 | t.datetime :sale_period_end 10 | t.datetime :sale_date 11 | t.string :sale_name 12 | t.integer :low_estimate_cents 13 | t.integer :high_estimate_cents 14 | t.string :currency 15 | t.text :notes 16 | t.float :commission_percent 17 | t.integer :price_cents 18 | t.integer :shipping_cents 19 | t.integer :photography_cents 20 | t.integer :other_fees_cents 21 | t.float :other_fees_percent 22 | t.float :insurance_percent 23 | t.float :insurance_cents 24 | t.string :state 25 | t.string :created_by_id 26 | t.string :reference_id, index: true 27 | t.timestamps 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /hokusai/development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | services: 4 | convection: 5 | env_file: 6 | - ../.env.shared 7 | - ../.env 8 | {% include 'templates/docker-compose-service.yml.j2' %} 9 | ports: 10 | - 3000:3000 11 | volumes: 12 | - ../:/app 13 | depends_on: 14 | - convection-postgres 15 | convection-sneakers: 16 | {% include 'templates/docker-compose-service-dev.yml.j2' %} 17 | command: ["bundle", "exec", "rake", "sneakers:run"] 18 | env_file: 19 | - ../.env.development 20 | - ../.env.shared 21 | - ../.env 22 | depends_on: 23 | - convection-rabbitmq 24 | - convection-postgres 25 | volumes: 26 | - ../:/app 27 | convection-postgres: 28 | image: postgres:14.12-alpine 29 | environment: 30 | - POSTGRES_HOST_AUTH_METHOD=trust 31 | convection-rabbitmq: 32 | image: rabbitmq:3.6.6-management-alpine 33 | ports: 34 | - "${CONVECTION_RABBIT_PORT:-}:5672" 35 | - "${CONVECTION_RABBIT_UI_PORT:-}:15672" 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.2-alpine3.13 2 | 3 | ENV LANG C.UTF-8 4 | ARG BUNDLE_GITHUB__COM 5 | WORKDIR /app 6 | 7 | RUN apk update && apk --no-cache --quiet add \ 8 | bash \ 9 | build-base \ 10 | chromium \ 11 | chromium-chromedriver \ 12 | dumb-init \ 13 | git \ 14 | nodejs \ 15 | postgresql-dev \ 16 | postgresql-client \ 17 | yarn \ 18 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ 19 | && adduser -D -g '' deploy 20 | 21 | RUN gem install bundler && \ 22 | bundle config --global frozen 1 23 | 24 | COPY Gemfile Gemfile.lock ./ 25 | RUN bundle install -j4 && \ 26 | mkdir -p /shared/pids && \ 27 | mkdir /shared/sockets && \ 28 | chown -R deploy:deploy /shared 29 | 30 | COPY package.json yarn.lock ./ 31 | RUN yarn install 32 | 33 | COPY . ./ 34 | 35 | RUN bundle exec rake assets:precompile && \ 36 | chown -R deploy:deploy ./ 37 | 38 | USER deploy 39 | 40 | ENTRYPOINT ["/usr/bin/dumb-init", "./scripts/load_secrets_and_run.sh"] 41 | CMD ["bundle", "exec", "puma", "-C", "config/puma.config"] 42 | -------------------------------------------------------------------------------- /app/helpers/utm_params_helper.rb: -------------------------------------------------------------------------------- 1 | module UtmParamsHelper 2 | def utm_params(source:, campaign:, **args) 3 | initial = {utm_campaign: campaign, utm_medium: "email", utm_source: source} 4 | transformed_args = args.each_with_object({}) { |(key, value), obj| obj["utm_#{key}".to_sym] = value } 5 | initial.merge(transformed_args) 6 | end 7 | 8 | def offer_utm_params(offer) 9 | case offer.offer_type 10 | when Offer::AUCTION_CONSIGNMENT 11 | utm_params(source: "sendgrid", campaign: "sell", term: "cx", content: "auction-offer") 12 | when Offer::NET_PRICE 13 | utm_params(source: "sendgrid", campaign: "sell", term: "cx", content: "net-price-offer") 14 | when Offer::RETAIL 15 | utm_params(source: "sendgrid", campaign: "sell", term: "cx", content: "retail-offer") 16 | when Offer::PURCHASE 17 | utm_params(source: "sendgrid", campaign: "sell", term: "cx", content: "purchase-offer") 18 | else 19 | utm_params(source: "sendgrid", campaign: "sell") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/graphql/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class MutationType < GraphQL::Schema::Object 5 | description "Mutation root for this schema" 6 | 7 | field :add_asset_to_consignment_submission, 8 | mutation: Mutations::AddAssetToConsignmentSubmission 9 | field :remove_asset_from_consignment_submission, 10 | mutation: Mutations::RemoveAssetFromConsignmentSubmission 11 | field :add_assets_to_consignment_submission, 12 | mutation: Mutations::AddAssetsToConsignmentSubmission 13 | field :create_consignment_offer, mutation: Mutations::CreateOfferMutation 14 | field :create_consignment_offer_response, 15 | mutation: Mutations::CreateOfferResponseMutation 16 | field :create_consignment_submission, 17 | mutation: Mutations::CreateSubmissionMutation 18 | field :update_consignment_submission, 19 | mutation: Mutations::UpdateSubmissionMutation 20 | field :add_user_to_submission, 21 | mutation: Mutations::AddUserToSubmissionMutation 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: dbfe20513c9ab2b0731869ffb2682de7a818eb1333a01e7e39ff3991d5a636cc475cd06a5a443ded38b1b257bd14a48b6900431100d8b03443f5114357d7d66a 15 | 16 | test: 17 | secret_key_base: b082433b99434f0b5ebc7d8ca28dc7ca40a7c9cb9df3c77c3e465aaeea7c65cd7f04117672f2a405c8cfc6bf5544e961915e5eac9b1f4febd6fa61b2daf3b1d4 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/controllers/api/consignment_contoller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Api::ConsignmentsController, type: :controller do 6 | before do 7 | allow_any_instance_of(Api::ConsignmentsController).to receive( 8 | :require_authentication 9 | ) 10 | end 11 | 12 | describe "#update" do 13 | let(:partner) { Fabricate(:partner, name: "Gagosian Gallery") } 14 | let(:submission) do 15 | Fabricate( 16 | :submission, 17 | state: "approved", 18 | artist_id: "artistId", 19 | consigned_partner_submission_id: consignment.id 20 | ) 21 | end 22 | let(:consignment) do 23 | Fabricate( 24 | :partner_submission, 25 | state: "open", 26 | partner: partner, 27 | sale_price_cents: 50 28 | ) 29 | end 30 | let(:offer) do 31 | Fabricate( 32 | :offer, 33 | state: "sent", 34 | partner_submission: consignment, 35 | offer_type: "purchase" 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/requests/api/graphql/extensions/default_value_extension_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | module DefaultValueExtensionSpec 6 | class Query < GraphQL::Schema::Object 7 | field :name, String, null: false do 8 | extension(Extensions::DefaultValueExtension, default_value: "Name") 9 | end 10 | 11 | def name 12 | @@name_value 13 | end 14 | end 15 | 16 | class ConnectionSchema < GraphQL::Schema 17 | query(Query) 18 | end 19 | 20 | describe "Extensions::DefaultValueExtension" do 21 | before(:each) { Query.class_variable_set(:@@name_value, nil) } 22 | 23 | it "changes nil value to default" do 24 | res = ConnectionSchema.execute("{ name }") 25 | 26 | expect(res["data"]["name"]).to eq "Name" 27 | end 28 | 29 | it "doesnt use default value if not nil" do 30 | Query.class_variable_set(:@@name_value, "own name") 31 | res = ConnectionSchema.execute("{ name }") 32 | 33 | expect(res["data"]["name"]).to eq "own name" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/services/consignment_inquiry_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe ConsignmentInquiryService do 6 | let(:consignment_inquiry) do 7 | Fabricate( 8 | :consignment_inquiry, 9 | gravity_user_id: "guid", 10 | name: "foo", 11 | email: "test@example.com", 12 | message: "bar" 13 | ) 14 | end 15 | 16 | describe "#post_consignment_created_event" do 17 | it "publishes consignment inquiry event" do 18 | expect(Artsy::EventPublisher).to receive(:publish).once.with( 19 | "consignments", 20 | "consignmentinquiry.created", 21 | verb: "created", 22 | subject: {id: "guid"}, 23 | object: {id: consignment_inquiry.id.to_s, root_type: "ConsignmentInquiry"}, 24 | properties: hash_including( 25 | email: "test@example.com", 26 | name: "foo", 27 | message: "bar" 28 | ) 29 | ) 30 | ConsignmentInquiryService.post_consignment_created_event(consignment_inquiry) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/services/my_collection_artwork_updated_service.rb: -------------------------------------------------------------------------------- 1 | class MyCollectionArtworkUpdatedService 2 | attr_accessor :submission, :object, :changes, :image_added 3 | 4 | def initialize(payload) 5 | @submission = Submission.find(payload[:object][:submission_id]) 6 | @object = payload[:object] 7 | @changes = payload[:properties][:changes] 8 | @image_added = payload[:properties][:image_added] 9 | end 10 | 11 | def notify_admin! 12 | return unless Convection.config.enable_myc_artwork_updated_email 13 | 14 | if !submission.assigned_to 15 | Rails.logger.info("[MyCollectionArtworkUpdatedService] No admin assigned to the submission #{submission.id}.") 16 | return 17 | end 18 | 19 | if submission.state != "approved" 20 | Rails.logger.info("[MyCollectionArtworkUpdatedService] Submission #{submission.id} is not in submitted state - skipping notification.") 21 | return 22 | end 23 | 24 | AdminMailer.artwork_updated(submission: submission, artwork_data: object, changes: changes, image_added: image_added).deliver_now 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /hokusai/rubyrep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: convection-rubyrep 6 | namespace: default 7 | spec: 8 | strategy: 9 | rollingUpdate: 10 | maxSurge: 1 11 | maxUnavailable: 0 12 | type: RollingUpdate 13 | template: 14 | metadata: 15 | labels: 16 | app: convection 17 | component: rubyrep 18 | layer: data 19 | name: convection-rubyrep 20 | spec: 21 | containers: 22 | - name: rubyrep-convection 23 | image: artsy/rubyrep 24 | args: ["/rubyrep-2.0.1/rubyrep", "--verbose", "replicate", "-c", "/mnt/default.conf"] 25 | imagePullPolicy: Always 26 | resources: 27 | requests: 28 | cpu: 200m 29 | memory: 256Mi 30 | volumeMounts: 31 | - name: rubyrep-convection 32 | mountPath: /mnt/default.conf 33 | subPath: default.conf 34 | volumes: 35 | - name: rubyrep-convection 36 | configMap: 37 | name: rubyrep-convection 38 | -------------------------------------------------------------------------------- /app/controllers/api/consignment_inquiries_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ConsignmentInquiriesController < RestController 3 | before_action :ensure_trusted_app_or_user, only: %i[create] 4 | 5 | def create 6 | param! :email, String, required: true 7 | param! :name, String, required: true 8 | param! :message, String, required: true 9 | param! :phone_number, String, required: false 10 | param! :gravity_user_id, String, required: false 11 | param! :recipient_email, String, required: false 12 | consignment_inquiry = ConsignmentInquiry.create!(consignment_inquiry_params) 13 | ConsignmentInquiryService.post_consignment_created_event(consignment_inquiry) 14 | render json: consignment_inquiry.to_json, status: :created 15 | end 16 | 17 | private 18 | 19 | def consignment_inquiry_params 20 | params 21 | .permit( 22 | :email, 23 | :gravity_user_id, 24 | :message, 25 | :name, 26 | :phone_number, 27 | :recipient_email 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/models/offer_responses_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "support/gravity_helper" 5 | 6 | describe OfferResponse do 7 | context "intended_state" do 8 | it "allows only certain intended_states" do 9 | expect(OfferResponse.new(intended_state: "blah")).not_to be_valid 10 | expect(OfferResponse.new(intended_state: Offer::ACCEPTED)).to be_valid 11 | expect(OfferResponse.new(intended_state: Offer::SENT)).not_to be_valid 12 | end 13 | 14 | it "is required" do 15 | expect(OfferResponse.new).not_to be_valid 16 | end 17 | end 18 | 19 | context "rejection_reason" do 20 | it "allows only certain rejection_reasons" do 21 | expect( 22 | OfferResponse.new( 23 | intended_state: Offer::REJECTED, 24 | rejection_reason: "Low estimate" 25 | ) 26 | ).to be_valid 27 | expect( 28 | OfferResponse.new( 29 | intended_state: Offer::REJECTED, 30 | rejection_reason: "meow" 31 | ) 32 | ).not_to be_valid 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/api/consignments_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class ConsignmentsController < RestController 5 | before_action :require_authentication 6 | 7 | def update_sale_info 8 | sale = Gravity.client.sale(id: params[:sale_id])._get 9 | sale_artworks = 10 | Gravity.client.sale_artworks(sale_id: params[:sale_id]).sale_artworks 11 | artworks = sale_artworks.map { |sa| [sa, sa.artwork, sa.artwork.id] } 12 | artwork_ids = artworks.map(&:third) 13 | submissions = Submission.where(source_artwork_id: artwork_ids) 14 | 15 | artworks.each do |sale_artwork, artwork, artwork_id| 16 | submission = submissions.find_by(source_artwork_id: artwork_id) 17 | next unless submission 18 | 19 | SubmissionService.update_submission_info(artwork, submission) 20 | PartnerSubmissionService.update_consignment_info( 21 | sale, 22 | sale_artwork, 23 | submission 24 | ) 25 | end 26 | 27 | render json: {result: "ok"}, status: :created 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Artsy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/graphql/resolvers/create_offer_response_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateOfferResponseResolver < BaseResolver 4 | def valid? 5 | return true if user? 6 | 7 | bad_argument_error = 8 | GraphQL::ExecutionError.new("Can't access createConsignmentOfferResponse") 9 | @error = bad_argument_error 10 | false 11 | end 12 | 13 | def run 14 | offer = Offer.find(offer_id) 15 | matching_user = 16 | offer.submission&.user&.gravity_user_id == @context[:current_user] 17 | 18 | raise GraphQL::ExecutionError, "Offer not found" unless matching_user 19 | 20 | offer_response = offer.offer_responses.create!(offer_response_attributes) 21 | 22 | {consignment_offer_response: offer_response} 23 | rescue ActiveRecord::RecordNotFound 24 | raise GraphQL::ExecutionError, "Offer not found" 25 | rescue ActiveRecord::RecordInvalid => e 26 | raise GraphQL::ExecutionError, e.message 27 | end 28 | 29 | private 30 | 31 | def offer_id 32 | @arguments[:offer_id] 33 | end 34 | 35 | def offer_response_attributes 36 | @arguments.except(:offer_id) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/graphql/resolvers/offers_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OffersResolver < BaseResolver 4 | BadArgumentError = GraphQL::ExecutionError.new("Can't find partner.") 5 | InvalidSortError = GraphQL::ExecutionError.new("Invalid sort column.") 6 | 7 | def valid? 8 | @error = compute_error 9 | @error.nil? 10 | end 11 | 12 | def run 13 | partner.offers.where(conditions).order(sort_order) 14 | end 15 | 16 | private 17 | 18 | def compute_error 19 | return BadArgumentError unless admin? || partner? 20 | 21 | InvalidSortError if invalid_sort? 22 | end 23 | 24 | def invalid_sort? 25 | return false if @arguments[:sort].blank? 26 | 27 | column_name = @arguments[:sort].keys.first 28 | Offer.column_names.exclude?(column_name) 29 | end 30 | 31 | def partner 32 | Partner.find_by(gravity_partner_id: @arguments[:gravity_partner_id]) 33 | end 34 | 35 | def conditions 36 | {state: @arguments[:states]}.compact 37 | end 38 | 39 | def sort_order 40 | default_sort = {id: :desc} 41 | return default_sort unless @arguments[:sort] 42 | 43 | @arguments[:sort] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/views/admin/offers/_retail.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | Offer type 4 |
5 |
6 | <%= @offer.offer_type %> 7 |
8 |
9 | 10 |
11 |
12 | Retail price 13 |
14 |
15 | <%= @offer.price_display %> 16 |
17 |
18 | 19 |
20 |
21 | Commission % 22 |
23 |
24 | <%= @offer.commission_percent_whole %> 25 |
26 |
27 | 28 |
29 |
30 | Sale period start 31 |
32 |
33 | <%= @offer.sale_period_start&.strftime('%b %-d %Y') %> 34 |
35 |
36 | 37 |
38 |
39 | Sale period end 40 |
41 |
42 | <%= @offer.sale_period_end&.strftime('%b %-d %Y') %> 43 |
44 |
45 | -------------------------------------------------------------------------------- /app/graphql/types/category_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class CategoryType < Types::BaseEnum 5 | value("PAINTING", nil, value: "Painting") 6 | value("SCULPTURE", nil, value: "Sculpture") 7 | value("PHOTOGRAPHY", nil, value: "Photography") 8 | value("PRINT", nil, value: "Print") 9 | value( 10 | "DRAWING_COLLAGE_OR_OTHER_WORK_ON_PAPER", 11 | nil, 12 | value: "Drawing, Collage or other Work on Paper" 13 | ) 14 | value("MIXED_MEDIA", nil, value: "Mixed Media") 15 | value("PERFORMANCE_ART", nil, value: "Performance Art") 16 | value("INSTALLATION", nil, value: "Installation") 17 | value("VIDEO_FILM_ANIMATION", nil, value: "Video/Film/Animation") 18 | value("ARCHITECTURE", nil, value: "Architecture") 19 | value( 20 | "FASHION_DESIGN_AND_WEARABLE_ART", 21 | nil, 22 | value: "Fashion Design and Wearable Art" 23 | ) 24 | value("JEWELRY", nil, value: "Jewelry") 25 | value("DESIGN_DECORATIVE_ART", nil, value: "Design/Decorative Art") 26 | value("TEXTILE_ARTS", nil, value: "Textile Arts") 27 | value("OTHER", nil, value: "Other") 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/controllers/api/consignment_inquiries_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Api::ConsignmentInquiriesController, type: :controller do 6 | before do 7 | allow_any_instance_of(Api::ConsignmentInquiriesController).to receive( 8 | :ensure_trusted_app_or_user 9 | ) 10 | end 11 | 12 | describe "#create" do 13 | it "creates a ConsignmentInquiry" do 14 | allow(Artsy::EventPublisher).to receive(:publish) 15 | expect { 16 | post :create, 17 | params: { 18 | email: "user@email.com", 19 | name: "User Test", 20 | message: "This is the message" 21 | } 22 | }.to change(ConsignmentInquiry, :count).by(1) 23 | end 24 | 25 | it "posts events when created" do 26 | expect(Artsy::EventPublisher).to receive(:publish).with( 27 | "consignments", 28 | "consignmentinquiry.created", 29 | anything 30 | ) 31 | post :create, 32 | params: { 33 | email: "user@email.com", 34 | name: "User Test", 35 | message: "This is the message" 36 | } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/graphql/resolvers/consignments_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ConsignmentsResolver < BaseResolver 4 | BadArgumentError = GraphQL::ExecutionError.new("Can't find partner.") 5 | InvalidSortError = GraphQL::ExecutionError.new("Invalid sort column.") 6 | 7 | def valid? 8 | @error = compute_error 9 | @error.nil? 10 | end 11 | 12 | def run 13 | partner.partner_submissions.where(conditions).order(sort_order) 14 | end 15 | 16 | private 17 | 18 | def compute_error 19 | return BadArgumentError unless admin? || partner? 20 | 21 | InvalidSortError if invalid_sort? 22 | end 23 | 24 | def invalid_sort? 25 | return false if @arguments[:sort].blank? 26 | 27 | column_name = @arguments[:sort].keys.first 28 | PartnerSubmission.column_names.exclude?(column_name) 29 | end 30 | 31 | def partner 32 | Partner.find_by(gravity_partner_id: @arguments[:gravity_partner_id]) 33 | end 34 | 35 | def conditions 36 | {state: ["sold", "bought in"]} 37 | end 38 | 39 | def sort_order 40 | default_sort = {id: :desc} 41 | return default_sort unless @arguments[:sort] 42 | 43 | @arguments[:sort] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/views/shared/email/_offer_type.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 18 | 19 |
10 | 11 | 12 | 15 | 16 |
17 |
20 |
21 |
22 | 23 | 24 | 36 | 37 |
25 | 26 | 27 | 33 | 34 |
35 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /app/graphql/mutations/add_asset_to_consignment_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class UploadSourceType < Types::BaseInputObject 5 | argument :bucket, String, required: false 6 | argument :key, String, required: false 7 | end 8 | 9 | class AddAssetToConsignmentSubmission < Mutations::BaseMutation 10 | argument :gemini_token, String, required: false 11 | argument :submissionID, ID, required: false 12 | argument :external_submission_id, ID, required: false 13 | argument :sessionID, String, required: false 14 | argument :asset_type, String, required: false 15 | argument :filename, String, required: false 16 | argument :size, String, required: false 17 | argument :source, UploadSourceType, required: false 18 | 19 | field :asset, Types::AssetType, null: true 20 | 21 | def resolve(arguments) 22 | resolve_options = { 23 | arguments: arguments, 24 | context: context, 25 | object: object 26 | } 27 | resolver = AddAssetToSubmissionResolver.new(**resolve_options) 28 | raise resolver.error unless resolver.valid? 29 | 30 | resolver.run 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class BaseController < ApplicationController 5 | skip_before_action :verify_authenticity_token 6 | skip_before_action :require_artsy_authentication 7 | skip_before_action :set_current_user 8 | 9 | private 10 | 11 | # For now, require that signature is valid by verifying payload w/ secret. 12 | # It must have 'aud', with 'sub' optional to be authenticated. 13 | # 14 | # If it has both 'aud' and 'sub', then it is user-scoped, with the user_id in 'sub'. 15 | # All authorization middleware should grant access as appropriate. 16 | def jwt_payload 17 | @jwt_payload ||= request.env["JWT_PAYLOAD"] 18 | end 19 | 20 | def current_app 21 | @current_app ||= jwt_payload&.fetch("aud", nil) 22 | end 23 | 24 | def current_user 25 | @current_user ||= jwt_payload&.fetch("sub", nil) 26 | end 27 | 28 | def current_user_roles 29 | @current_user_roles ||= 30 | jwt_payload&.fetch("roles", nil)&.split(",")&.map(&:to_sym) || [] 31 | end 32 | 33 | def jwt_token 34 | @jwt_token ||= request.env["JWT_TOKEN"] 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/graphql/mutations/add_assets_to_consignment_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutations 4 | class UploadSourcesType < Types::BaseInputObject 5 | argument :buckets, [String], required: false 6 | argument :keys, [String], required: false 7 | end 8 | 9 | class AddAssetsToConsignmentSubmission < Mutations::BaseMutation 10 | argument :gemini_tokens, [String], required: false 11 | argument :submissionID, ID, required: false 12 | argument :external_submission_id, ID, required: false 13 | argument :sessionID, String, required: false 14 | argument :asset_type, String, required: false 15 | argument :filename, String, required: false 16 | argument :size, String, required: false 17 | argument :sources, UploadSourcesType, required: false 18 | 19 | field :assets, [Types::AssetType], null: true 20 | 21 | def resolve(arguments) 22 | resolve_options = { 23 | arguments: arguments, 24 | context: context, 25 | object: object 26 | } 27 | resolver = AddAssetsToSubmissionResolver.new(**resolve_options) 28 | raise resolver.error unless resolver.valid? 29 | 30 | resolver.run 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/views/admin/offers/_interested.html.erb: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /spec/services/partner_update_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "support/gravity_helper" 5 | 6 | describe PartnerUpdateService do 7 | describe "#update_partners_from_gravity" do 8 | let!(:partner1) do 9 | Fabricate(:partner, gravity_partner_id: "phillips", name: "Phillips") 10 | end 11 | let!(:partner2) do 12 | Fabricate( 13 | :partner, 14 | gravity_partner_id: "gagosian", 15 | name: "Gagosian Gallery" 16 | ) 17 | end 18 | let!(:partner3) do 19 | Fabricate(:partner, gravity_partner_id: "pace", name: "Pace Gallery") 20 | end 21 | 22 | before do 23 | stub_gravity_root 24 | stub_gravity_partner(id: "phillips", name: "Phillips New") 25 | stub_gravity_partner(id: "gagosian", name: "Gagosian Gallery") 26 | stub_gravity_partner(id: "pace", name: "Pace Gallery") 27 | end 28 | 29 | it "updates partners if they have a new name" do 30 | PartnerUpdateService.update_partners_from_gravity 31 | expect(partner1.reload.name).to eq "Phillips New" 32 | expect(partner2.reload.name).to eq "Gagosian Gallery" 33 | expect(partner3.reload.name).to eq "Pace Gallery" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/views/admin/offers/_retail_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 | <%= f.text_field :price_dollars, class: 'form-control' %> 8 |
9 |
10 |
11 | 12 |
13 |
14 | 17 |
18 | <%= f.text_field :commission_percent_whole, class: 'form-control' %> 19 |
20 |
21 |
22 | 23 |
24 |
25 | 28 |
29 | <%= f.date_field :sale_period_start, class: 'form-control' %> 30 |
31 |
32 |
33 | 34 |
35 |
36 | 39 |
40 | <%= f.date_field :sale_period_end, class: 'form-control' %> 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /app/views/admin/submissions/_state_actions.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Actions
3 | <% if @submission.approved? || @submission.published? %> 4 | <%= link_to 'Create Offer', new_step_0_admin_offers_path(submission_id: @submission.id), class: 'btn btn-primary btn-small btn-full-width' %> 5 |
6 | <%= link_to 'List Artwork', '#', class: 'btn btn-secondary btn-small btn-full-width', data: { 'remodal-target' => 'list-artwork-modal' } %> 7 |
8 | <% end %> 9 | <% @actions.each do |action| %> 10 |
11 | <% if action[:state] == 'rejected' %> 12 | <%= link_to(action[:text], '#', { 'data-remodal-target' => 'reject-reasons-modal', class: action[:class] }) %> 13 | <% else %> 14 | <%= link_to(action[:text], admin_submission_path(@submission, submission: { state: action[:state] }), method: :put, class: action[:class], data: { confirm: action[:confirm] }) %> 15 | <% end %> 16 |
17 | <% end %> 18 | <% if @submission.reviewed? %> 19 |
20 | <%= reviewer_byline(@submission) %> 21 |
22 | <% end %> 23 |
24 | -------------------------------------------------------------------------------- /app/graphql/types/offer_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Types 4 | class OfferType < Types::BaseObject 5 | description "Consignment Offer" 6 | 7 | field :id, ID, "Uniq ID for this offer", null: false 8 | 9 | field :commission_percent_whole, Integer, null: true 10 | field :created_at, GraphQL::Types::ISO8601DateTime, null: true 11 | field :created_by_id, ID, null: true 12 | field :currency, String, null: true 13 | field :deadline_to_consign, String, null: true 14 | field :high_estimate_cents, Integer, null: true 15 | field :insurance_info, String, null: true 16 | field :low_estimate_cents, Integer, null: true 17 | field :notes, String, null: true 18 | field :offer_type, String, null: true 19 | field :other_fees_info, String, null: true 20 | field :partner_info, String, null: true 21 | field :photography_info, String, null: true 22 | field :sale_date, String, null: true 23 | field :sale_name, String, null: true 24 | field :sale_location, String, null: true 25 | field :shipping_info, String, null: true 26 | field :state, String, null: true 27 | field :starting_bid_cents, Integer, null: true 28 | 29 | field :submission, Types::SubmissionType, null: false 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/services/notification_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe NotificationService do 6 | let(:submission) do 7 | Fabricate( 8 | :submission, 9 | artist_id: "artistid", 10 | user: Fabricate(:user, gravity_user_id: "userid"), 11 | title: "My Artwork", 12 | medium: "painting", 13 | year: "1992", 14 | height: "12", 15 | width: "14", 16 | dimensions_metric: "in", 17 | location_city: "New York", 18 | category: "Painting", 19 | state: "submitted" 20 | ) 21 | end 22 | 23 | describe "#post_submission_event" do 24 | it "publishes submission event" do 25 | expect(Artsy::EventPublisher).to receive(:publish).once.with( 26 | "consignments", 27 | "submission.submitted", 28 | verb: "submitted", 29 | subject: {id: "userid", display: "userid (New York)"}, 30 | object: {id: submission.id.to_s, display: "#{submission.id} (submitted)"}, 31 | properties: hash_including( 32 | medium: "painting", 33 | category: "Painting", 34 | year: "1992" 35 | ) 36 | ) 37 | NotificationService.post_submission_event(submission.id, "submitted") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/admin/notes_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Admin 4 | class NotesController < ApplicationController 5 | def create 6 | submission = Submission.find(params.dig(:note, :submission_id)) 7 | note = 8 | if params.dig(:note, :add_note_to_user) == "0" 9 | submission.notes.new(note_params) 10 | else 11 | submission.user&.notes&.new(note_params) 12 | end 13 | path = admin_submission_path(submission) 14 | 15 | if note&.save 16 | redirect_to path, notice: "Note has successfully been created." 17 | else 18 | redirect_to path, 19 | alert: 20 | "Could not create note: #{ 21 | note&.errors&.full_messages&.join(", ") || 22 | "User does not exist" 23 | }" 24 | end 25 | end 26 | 27 | def authorized_artsy_token?(token) 28 | # Allow access on edit/destructive actions to consignment reps (default: read-only). 29 | ArtsyAdminAuth.valid?(token, [ArtsyAdminAuth::CONSIGNMENTS_REPRESENTATIVE]) 30 | end 31 | 32 | private 33 | 34 | def note_params 35 | params.require(:note).permit(:body).merge(gravity_user_id: @current_user) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/views/admin/offer_responses/_offer_response_section.html.erb: -------------------------------------------------------------------------------- 1 | <% truncated = false if local_assigns[:truncated].nil? %> 2 | 3 |
4 | <% if offer_response.intended_state == Offer::ACCEPTED %> 5 |
6 | Accept offer 7 |
8 | <% elsif offer_response.intended_state == Offer::REVIEW %> 9 |
10 | Interested in offer 11 |
12 | <% else %> 13 |
14 | Reject offer 15 |
16 | <% if offer_response.rejection_reason.present? %> 17 |
18 | Reason: <%= offer_response.rejection_reason %> 19 |
20 | <% end %> 21 | <% end %> 22 | 23 | <% unless truncated %> 24 | <% if offer_response.comments.present? %> 25 |
26 | Comments: <%= offer_response.comments %> 27 |
28 | <% end %> 29 | 30 | <% if offer_response.phone_number.present? %> 31 |
32 | Phone number: <%= offer_response.phone_number %> 33 |
34 | <% end %> 35 | 36 |
37 | Date: <%= offer_response.created_at.strftime('%b %-d %Y') %> 38 |
39 | <% end %> 40 |
41 | -------------------------------------------------------------------------------- /app/views/layouts/_nav.html.erb: -------------------------------------------------------------------------------- 1 | class='brand'> 2 | <%= render '/shared/watt/svgs/mark' %> 3 | 4 | 5 | 8 | 9 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require bootstrap 16 | //= require watt/underscore 17 | //= require watt/jquery-ui-1.10.3.custom.js 18 | //= require watt/flash 19 | //= require watt/autocomplete 20 | //= require jquery.fileupload.js 21 | //= require watt/modals 22 | //= require watt/remodal 23 | //= require gemini/gemini_upload 24 | //= require select2 25 | 26 | //= require new_asset 27 | //= require new_submission 28 | //= require new_partner 29 | //= require new_offer 30 | //= require offer 31 | //= require consignment 32 | //= require match_submission 33 | //= require match_offer 34 | //= require match_consignment 35 | //= require search_bar 36 | -------------------------------------------------------------------------------- /db/migrate/20220128111006_add_foreign_key_from_submission_to_admin_user.rb: -------------------------------------------------------------------------------- 1 | class AddForeignKeyFromSubmissionToAdminUser < ActiveRecord::Migration[6.1] 2 | def up 3 | add_column :submissions, :admin_id, :bigint 4 | add_foreign_key :submissions, :admin_users, column: :admin_id 5 | 6 | populate_admin_id_field 7 | 8 | remove_column :submissions, :created_by, :string 9 | end 10 | 11 | def down 12 | add_column :submissions, :created_by, :string 13 | 14 | populate_created_by_field_with_email 15 | 16 | remove_foreign_key :submissions, :admin_users, column: :admin_id 17 | remove_column :submissions, :admin_id, :bigint 18 | end 19 | 20 | def populate_admin_id_field 21 | Submission.all.select { |x| !x[:created_by].blank? }.each do |submission| 22 | user = AdminUser.find_by(email: submission[:created_by]) 23 | 24 | submission.update(admin_id: user.id) unless user.nil? 25 | end 26 | end 27 | 28 | def populate_created_by_field_with_email 29 | Submission 30 | .where 31 | .not(admin_id: nil) 32 | .each do |submission| 33 | user = AdminUser.find_by(id: submission.admin_id) 34 | 35 | unless user.nil? 36 | submission[:created_by] = user.email 37 | submission.save! 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/graphql/resolvers/offer_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OfferResolver < BaseResolver 4 | BadArgumentError = GraphQL::ExecutionError.new("Can't access offer") 5 | 6 | def valid? 7 | @error = compute_error 8 | @error.nil? 9 | end 10 | 11 | def run 12 | if partner? || admin? && partner.present? 13 | return partner.offers.find(@arguments[:id]) 14 | end 15 | 16 | offer = Offer.find(@arguments[:id]) 17 | validate_user(offer) 18 | validate_offer_state(offer) 19 | offer 20 | rescue ActiveRecord::RecordNotFound 21 | raise GraphQL::ExecutionError, "Offer not found" 22 | end 23 | 24 | private 25 | 26 | def compute_error 27 | return BadArgumentError unless admin? || partner? || user? 28 | end 29 | 30 | def validate_user(offer) 31 | matching_user = 32 | offer.submission&.user&.gravity_user_id == @context[:current_user] 33 | return if matching_user || admin? 34 | 35 | raise(GraphQL::ExecutionError, "Offer not found") 36 | end 37 | 38 | def validate_offer_state(offer) 39 | return true if admin? 40 | return if offer.state != Offer::DRAFT 41 | 42 | raise(GraphQL::ExecutionError, "Offer not found") 43 | end 44 | 45 | def partner 46 | Partner.find_by(gravity_partner_id: @arguments[:gravity_partner_id]) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 24 | 25 | 26 | 29 | 30 |
22 | <%= yield %> 23 |
27 | <%= render 'shared/email/email_footer' %> 28 |
31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/graphql/resolvers/add_asset_to_submission_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddAssetToSubmissionResolver < BaseResolver 4 | include Resolvers::Submissionable 5 | 6 | def run 7 | check_submission_presence! 8 | 9 | unless matching_user(submission, @arguments&.[](:session_id)) || admin? 10 | raise(GraphQL::ExecutionError, "Submission Not Found") 11 | end 12 | 13 | @arguments[:asset_type] ||= "image" 14 | 15 | asset = submission.assets.create!(asset_params) 16 | SubmissionService.notify_user(submission.id) if submission.submitted? 17 | 18 | {asset: asset} 19 | end 20 | 21 | private 22 | 23 | # overwrites Resolvers::Submissionable 24 | def submission_id 25 | @arguments[:submission_id] 26 | end 27 | 28 | # overwrites Resolvers::Submissionable 29 | def external_submission_id 30 | @arguments[:external_submission_id] 31 | end 32 | 33 | def asset_params 34 | # remove session_id and external_submission_id from arguments 35 | # and remap source[:bucket] to s3_bucket and source[:key] to s3_path 36 | @arguments.except(:session_id, :external_submission_id) 37 | .tap do |params| 38 | if (source = params.delete(:source)) 39 | params[:s3_bucket] = source[:bucket] 40 | params[:s3_path] = source[:key] 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/graphql/resolvers/concerns/resolvers/submissionable.rb: -------------------------------------------------------------------------------- 1 | # Include this module in any of Resolvers if you want to perform 2 | # a submission lookup and arguments validation based on 3 | # submission id/external_id. 4 | 5 | # Lookup is performed by @arguments[:id]/@arguments[:external_id] by 6 | # default, be can be overwritten be redefining `submission_id` and 7 | # `external_submission_id` in your resolvers. 8 | 9 | module Resolvers::Submissionable 10 | IdsNotPassed = 11 | GraphQL::ExecutionError.new("Neither id nor externalId have been passed.") 12 | 13 | def submission 14 | @submission ||= 15 | Submission.find_by(uuid: submission_id) || 16 | Submission.find_by(id: submission_id) || 17 | Submission.find_by(uuid: external_submission_id) 18 | end 19 | 20 | def valid? 21 | @error = compute_error 22 | @error.nil? 23 | end 24 | 25 | def check_submission_presence! 26 | raise(GraphQL::ExecutionError, "Submission Not Found") unless submission 27 | end 28 | 29 | private 30 | 31 | def submission_id 32 | @arguments[:id] 33 | end 34 | 35 | def external_submission_id 36 | @arguments[:external_id] 37 | end 38 | 39 | def compute_error 40 | return IdsNotPassed unless ids_passed? 41 | end 42 | 43 | def ids_passed? 44 | submission_id.present? || external_submission_id.present? 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/models/demand_calculator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NullArtistStandingScore 4 | def artist_score 5 | 0 6 | end 7 | 8 | def auction_score 9 | 0 10 | end 11 | end 12 | 13 | class DemandCalculator 14 | CATEGORY_MODIFIERS = { 15 | "Default" => 0.5, 16 | "Painting" => 1, 17 | "Print" => 0.75 18 | }.freeze 19 | 20 | def self.score(artist_id, category) 21 | new(artist_id, category).score 22 | end 23 | 24 | def initialize(artist_id, category) 25 | @artist_id = artist_id 26 | @category = category 27 | @artist_standing_score = 28 | ArtistStandingScore 29 | .where(artist_id: artist_id) 30 | .order(created_at: :asc) 31 | .limit(1) 32 | .first || NullArtistStandingScore.new 33 | end 34 | 35 | def score 36 | {artist_score: artist_score, auction_score: auction_score} 37 | end 38 | 39 | private 40 | 41 | def artist_score 42 | calculate_demand_score(@artist_standing_score.artist_score) 43 | end 44 | 45 | def auction_score 46 | calculate_demand_score(@artist_standing_score.auction_score) 47 | end 48 | 49 | def calculate_demand_score(base_score) 50 | return 0 unless base_score.positive? 51 | 52 | modifier = 53 | CATEGORY_MODIFIERS.fetch(@category, CATEGORY_MODIFIERS["Default"]) 54 | base_score * modifier 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/api/callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class CallbacksController < RestController 5 | before_action :require_token 6 | 7 | def gemini 8 | param! :access_token, String, required: true 9 | param! :image_url, Hash, required: true 10 | param! :metadata, Hash, required: true 11 | param! :token, String, required: true 12 | 13 | submission = 14 | Submission.find_by(uuid: gemini_params[:metadata][:id]) || 15 | Submission.find(gemini_params[:metadata][:id]) 16 | 17 | asset = 18 | submission.assets.detect { |a| a.gemini_token == gemini_params[:token] } 19 | 20 | unless asset && asset.gemini_token == gemini_params[:token] 21 | raise ActiveRecord::RecordNotFound 22 | end 23 | 24 | asset.update_image_urls!(gemini_params) 25 | render json: asset.reload.to_json, status: :ok 26 | end 27 | 28 | private 29 | 30 | def gemini_params 31 | params.permit( 32 | :access_token, 33 | :token, 34 | image_url: %i[square large medium thumbnail], 35 | metadata: %i[id _type] 36 | ) 37 | end 38 | 39 | def require_token 40 | unless params[:access_token] == Convection.config.access_token 41 | raise ApplicationController::NotAuthorized 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/api/assets_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class AssetsController < RestController 5 | before_action :require_authentication 6 | before_action :set_submission_and_asset, only: %i[show] 7 | before_action :set_submission, only: %i[create index] 8 | before_action :require_authorized_submission 9 | 10 | def index 11 | param! :submission_id, String, required: true 12 | 13 | assets = @submission.assets.limit(10) 14 | render json: assets.to_json, status: :ok 15 | end 16 | 17 | def show 18 | param! :id, String, required: true 19 | 20 | render json: @asset.to_json, status: :ok 21 | end 22 | 23 | def create 24 | param! :asset_type, String, default: "image" 25 | param! :gemini_token, String, required: true 26 | param! :submission_id, String, required: true 27 | 28 | asset = @submission.assets.create!(asset_params) 29 | SubmissionService.notify_user(@submission.id) if @submission.submitted? 30 | render json: asset.to_json, status: :created 31 | end 32 | 33 | private 34 | 35 | def set_submission_and_asset 36 | @asset = Asset.find(params[:id]) 37 | @submission = @asset.submission 38 | end 39 | 40 | def asset_params 41 | params.permit(:asset_type, :gemini_token, :submission_id) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | include PgSearch::Model 5 | 6 | before_save :set_gravity_user_id 7 | 8 | has_many :submissions, dependent: :nullify 9 | has_many :notes, dependent: :nullify 10 | 11 | pg_search_scope :search, against: :email, using: {tsearch: {prefix: true}} 12 | 13 | def gravity_user 14 | return @gravity_user if defined?(@gravity_user) 15 | return nil unless gravity_user_id 16 | 17 | @gravity_user = 18 | gravity_user_id && 19 | ( 20 | begin 21 | Gravity.client.user(id: gravity_user_id)._get 22 | rescue Faraday::ResourceNotFound 23 | nil 24 | end 25 | ) 26 | end 27 | 28 | def name 29 | gravity_user.try(:name) 30 | end 31 | 32 | def email 33 | self[:email] || user_detail&.email 34 | end 35 | 36 | def phone 37 | user_detail&.phone 38 | end 39 | 40 | def user_detail 41 | gravity_user&.user_detail&._get 42 | rescue Faraday::ResourceNotFound 43 | nil 44 | end 45 | 46 | def unique_code_for_digest 47 | created_at.to_i % 100_000 + id + (submissions.first&.id || 0) 48 | end 49 | 50 | def self.anonymous 51 | User.find_or_create_by(gravity_user_id: "anonymous") 52 | end 53 | 54 | def set_gravity_user_id 55 | self.gravity_user_id = nil if gravity_user_id.blank? 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy 5 | # For further information see the following documentation 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 7 | 8 | # Rails.application.config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | 16 | # # Specify URI for violation reports 17 | # # policy.report_uri "/csp-violation-report-endpoint" 18 | # end 19 | 20 | # If you are using UJS then enable automatic nonce generation 21 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 22 | 23 | # Set the nonce only to specific directives 24 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 25 | 26 | # Report CSP violations to a specified URI 27 | # For further information see the following documentation: 28 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 29 | # Rails.application.config.content_security_policy_report_only = true 30 | -------------------------------------------------------------------------------- /app/models/concerns/dollarize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dollarize 4 | extend ActiveSupport::Concern 5 | 6 | # Call like: 7 | # dollarize :low_estimate_cents # => defines low_estimate_dollars and low_estimate_display 8 | # 9 | class_methods do 10 | def dollarize(*method_names) 11 | method_names.each do |method_name| 12 | attribute method_name.to_s.gsub(/_cents$/, "_dollars").to_sym 13 | 14 | define_method method_name.to_s.gsub(/_cents$/, "_dollars") do 15 | return if self[method_name].blank? 16 | 17 | self[method_name] / 100 18 | end 19 | 20 | define_method "#{ 21 | method_name.to_s.gsub(/_cents$/, "_dollars") 22 | }=" do |dollars| 23 | cents = 24 | if dollars.blank? 25 | nil 26 | elsif dollars.is_a?(String) 27 | dollars.delete(",").to_f * 100 28 | else 29 | dollars.to_f * 100 30 | end 31 | 32 | self[method_name] = cents 33 | end 34 | 35 | define_method method_name.to_s.gsub(/_cents$/, "_display") do 36 | return if self[method_name].blank? 37 | 38 | currency = self.currency || "USD" 39 | Money.new(self[method_name], currency).format(no_cents: true) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/models/partner_submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PartnerSubmission < ApplicationRecord 4 | include ReferenceId 5 | include PgSearch::Model 6 | include Currency 7 | include Dollarize 8 | include Percentize 9 | 10 | pg_search_scope :search, 11 | against: %i[id reference_id], 12 | associated_against: { 13 | partner: %i[name] 14 | }, 15 | using: { 16 | tsearch: { 17 | prefix: true, 18 | negation: true 19 | }, 20 | trigram: { 21 | only: %i[id reference_id], 22 | threshold: 0.9 23 | } 24 | } 25 | 26 | belongs_to :partner 27 | belongs_to :submission 28 | has_many :offers, dependent: :destroy 29 | belongs_to :accepted_offer, class_name: "Offer" 30 | 31 | STATES = [ 32 | "open", 33 | "sold", 34 | "bought in", 35 | "canceled", 36 | "withdrawn - pre-launch", 37 | "withdrawn - post-launch" 38 | ].freeze 39 | 40 | scope :group_by_day, -> { group("date_trunc('day', notified_at) ") } 41 | scope :consigned, -> { where.not(accepted_offer_id: nil) } 42 | 43 | validates :state, inclusion: {in: STATES}, allow_nil: true 44 | 45 | before_validation :set_state, on: :create 46 | 47 | dollarize :sale_price_cents 48 | 49 | percentize :partner_commission_percent, :artsy_commission_percent 50 | 51 | def set_state 52 | self.state ||= "open" 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/gravity_v1.rb: -------------------------------------------------------------------------------- 1 | class GravityV1 2 | class GravityError < StandardError; end 3 | 4 | def self.get(url, params: {}, token: nil) 5 | base_url = "#{Convection.config.gravity_url}#{url}" 6 | uri = URI.parse(base_url) 7 | uri.query = URI.encode_www_form(params) 8 | http = Net::HTTP.new(uri.host, uri.port) 9 | http.use_ssl = true 10 | req = Net::HTTP::Get.new(uri.request_uri, headers(token)) 11 | response = http.request(req) 12 | raise GravityV1::GravityError, response.body unless response.is_a? Net::HTTPSuccess 13 | 14 | JSON.parse(response.body) 15 | end 16 | 17 | def self.post(url, params: {}, token: nil) 18 | base_url = "#{Convection.config.gravity_url}#{url}" 19 | uri = URI.parse(base_url) 20 | http = Net::HTTP.new(uri.host, uri.port) 21 | http.use_ssl = true 22 | req = Net::HTTP::Post.new("#{uri.path}?#{uri.query}", headers(token)) 23 | req.body = params.to_json 24 | response = http.request(req) 25 | raise GravityV1::GravityError, response.body unless response.is_a? Net::HTTPSuccess 26 | 27 | JSON.parse(response.body) 28 | end 29 | 30 | def self.headers(token) 31 | { 32 | "X-XAPP-TOKEN" => Convection.config.gravity_xapp_token, 33 | "X-ACCESS-TOKEN" => token, 34 | "Accept" => "application/json", 35 | "User-Agent" => "Convection", 36 | "Content-Type" => "application/json" 37 | } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/models/consignment_inquiry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe ConsignmentInquiry do 6 | context "Validations" do 7 | it "validates email format" do 8 | expect do 9 | ConsignmentInquiry.create!( 10 | email: "emailisnotvalid", 11 | message: "Is a valid message", 12 | name: "Valid Name" 13 | ) 14 | end.to raise_error "Validation failed: Email is invalid" 15 | end 16 | it "validates recipient email format" do 17 | expect do 18 | ConsignmentInquiry.create!( 19 | email: "validemail@email.com", 20 | message: "Is a valid message", 21 | name: "Valid Name", 22 | recipient_email: "notavalidemail" 23 | ) 24 | end.to raise_error "Validation failed: Recipient email is invalid" 25 | end 26 | it "requires name" do 27 | expect do 28 | ConsignmentInquiry.create!( 29 | email: "email@email.com", 30 | message: "Is a valid message" 31 | ) 32 | end.to raise_error "Validation failed: Name is required" 33 | end 34 | it "requires message" do 35 | expect do 36 | ConsignmentInquiry.create!( 37 | email: "email@email.com", 38 | name: "Valid Name" 39 | ) 40 | end.to raise_error "Validation failed: Message is required" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/assets/javascripts/new_partner.js.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | # Partner autocomplete 3 | updatePartnerSelectionsForm = (selection) -> 4 | $( "#partner-selections-form #gravity_partner_id" ).val( selection.id ) 5 | $( "#partner-selections-form #name" ).val( selection.given_name ) 6 | $( "#partner-selections-form #partner-search" ).val( selection.given_name ) 7 | $( "#partner-selections-form #partner-name-display" ).html('Selected partner: ' + selection.given_name) 8 | enableDisableButton() 9 | 10 | enableDisableButton = () -> 11 | if $('#partner-selections-form #gravity_partner_id').val().length > 0 12 | $('#partner-selections-form #partner-search-submit').removeClass('disabled-button') 13 | else 14 | $('#partner-selections-form #partner-search-submit').addClass('disabled-button') 15 | 16 | if $('#partner-selections-form').length != 0 17 | enableDisableButton() 18 | $('#partner-selections-form #partner-search').autocomplete( 19 | source: (request, response) -> 20 | $.getJSON('/match_partner', term: request.term, response) 21 | select: (event, ui) -> 22 | updatePartnerSelectionsForm(ui.item) 23 | false 24 | ).data("ui-autocomplete")._renderItem = (ul, item) -> 25 | $( "
  • " ) 26 | .attr( "data-value", item.value ) 27 | .append( "#{item.given_name}" ) 28 | .appendTo( ul ) 29 | --------------------------------------------------------------------------------