├── 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 | <%= 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 |
2 | <%= @submission.assigned_to == '' ? 'Unassigned' : AdminUser.find_by(gravity_user_id: @submission.assigned_to)&.name %>
3 |
4 |
2 | <%= @submission.cataloguer == '' ? 'Unassigned' : AdminUser.find_by(gravity_user_id: @submission.cataloguer)&.name %>
3 |
4 | "
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 |
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 |
14 | <%= yield %>
15 |
16 |
17 |
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 |
4 | <% if note.user %>
5 | User note
6 | <% end %>
7 | <%= note.author.try(:name) %> added a note at <%= note.updated_at.in_time_zone('Eastern Time (US & Canada)').to_formatted_s(:long)%>
8 |
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 |
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 |
10 | <%= label %>
11 |
12 |
13 |
14 |
15 | <%= markdown_formatted(@offer.notes) %>
16 |
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 |
4 | Net sale price
5 |
6 |
7 | <%= f.text_field :price_dollars, class: 'form-control' %>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Sale period start
16 |
17 |
18 | <%= f.date_field :sale_period_start, class: 'form-control' %>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Sale period end
27 |
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 |
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 |
10 |
11 |
12 |
13 | Offer Type
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%= formatted_offer_type(@offer) %>
29 | <% if include_description == true %>
30 | <%= offer_type_description(@offer) %>
31 | <% end %>
32 |
33 |
34 |
35 |
36 |
37 |
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 |
2 |
7 |
8 | <%= link_to '', '#', id: 'interested-close'%>
9 |
10 |
11 |
12 |
13 | <%= form_for(@offer, url: admin_offer_path, method: :put) do |f| %>
14 | <%= f.hidden_field :state, value: 'review' %>
15 |
16 |
17 |
18 |
19 | <%= f.text_field :override_email, placeholder: 'Override partner email', class: 'form-control' %>
20 |
21 |
22 |
23 |
24 |
25 |
26 | <%= f.submit 'Save and Send', class: 'btn btn-primary btn-full-width' %>
27 |
28 |
29 | <% end %>
30 |
31 |
32 |
33 |
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 |
4 | Retail price
5 |
6 |
7 | <%= f.text_field :price_dollars, class: 'form-control' %>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Commission %
16 |
17 |
18 | <%= f.text_field :commission_percent_whole, class: 'form-control' %>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Sale period start
27 |
28 |
29 | <%= f.date_field :sale_period_start, class: 'form-control' %>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Sale period end
38 |
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 |
6 | <%= link_to 'Convection', root_url %>
7 |
8 |
9 |
10 | <%= text_field_tag 'term', {}, class: "form-control clearable",
11 | placeholder: 'Search by ID or title', id: 'search-bar' %>
12 | <%= link_to 'Submissions', admin_submissions_url %>
13 | <%= link_to 'Offers', admin_offers_url %>
14 | <%= link_to 'Consignments', admin_consignments_url %>
15 | <%= link_to 'Partners', admin_partners_url %>
16 | <%= link_to 'Admins', admin_admin_users_url if super_admin_user?(@current_user) %>
17 | <%= link_to 'log out', '/sign_out' %>
18 |
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 |
22 | <%= yield %>
23 |
24 |
25 |
26 |
27 | <%= render 'shared/email/email_footer' %>
28 |
29 |
30 |
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 | $( "