├── .github └── stale.yml ├── .rspec ├── .github_changelog_generator ├── doc └── tables.png ├── spec ├── support │ ├── active_model_mocks.rb │ ├── shoulda.rb │ └── testing_support.rb ├── fixtures │ └── thinking-cat.jpg ├── models │ ├── spree │ │ ├── product_spec.rb │ │ ├── digital_spec.rb │ │ ├── drm_record_spec.rb │ │ ├── stock │ │ │ └── splitter │ │ │ │ └── digital_splitter_spec.rb │ │ ├── variant_spec.rb │ │ ├── line_item_spec.rb │ │ ├── digital_link_spec.rb │ │ └── order_spec.rb │ └── calculator │ │ └── digital_delivery_spec.rb ├── controllers │ ├── admin │ │ ├── orders_controller_spec.rb │ │ └── digitals_controller_spec.rb │ └── digitals_controller_spec.rb └── spec_helper.rb ├── lib ├── solidus_digital │ ├── version.rb │ ├── testing_support │ │ └── factories.rb │ └── engine.rb ├── solidus_digital.rb ├── generators │ └── solidus_digital │ │ └── install │ │ └── install_generator.rb └── spree │ └── solidus_digital_configuration.rb ├── .rubocop.yml ├── CHANGELOG.md ├── .gem_release.yml ├── bin ├── rake ├── setup ├── rails ├── rails-sandbox ├── console ├── rails-engine └── sandbox ├── Rakefile ├── app ├── assets │ ├── javascripts │ │ └── spree │ │ │ ├── backend │ │ │ └── solidus_digital.js │ │ │ └── frontend │ │ │ └── solidus_digital.js │ └── stylesheets │ │ └── spree │ │ ├── backend │ │ └── solidus_digital.css │ │ └── frontend │ │ └── solidus_digital.css ├── views │ └── spree │ │ ├── digitals │ │ ├── _digital.html.erb │ │ └── unauthorized.html.erb │ │ ├── admin │ │ └── digitals │ │ │ ├── _digital.html.erb │ │ │ ├── index.html.erb │ │ │ └── _form.html.erb │ │ ├── shared │ │ └── _digital_download_links.html.erb │ │ └── order_mailer │ │ └── confirm_email.text.erb ├── models │ ├── solidus_digital │ │ └── permission_sets │ │ │ ├── digital_management.rb │ │ │ └── digital_display.rb │ └── spree │ │ ├── digital.rb │ │ ├── calculator │ │ └── shipping │ │ │ └── digital_delivery.rb │ │ ├── drm_record.rb │ │ ├── stock │ │ └── splitter │ │ │ └── digital_splitter.rb │ │ └── digital_link.rb ├── overrides │ └── spree │ │ ├── shared │ │ └── add_digital_downloads_to_invoice.rb │ │ └── admin │ │ └── shared │ │ ├── _order_submenu │ │ └── add_digital_versions_to_admin_product_tabs.html.erb.deface │ │ └── _product_tabs │ │ └── add_digital_versions_to_admin_product_tabs.html.erb.deface ├── decorators │ ├── models │ │ └── solidus_digital │ │ │ └── spree │ │ │ ├── product_decorator.rb │ │ │ ├── line_item_decorator.rb │ │ │ ├── variant_decorator.rb │ │ │ └── order_decorator.rb │ └── controllers │ │ └── solidus_digital │ │ └── spree │ │ └── admin │ │ └── orders_controller_decorator.rb ├── controllers │ └── spree │ │ ├── admin │ │ └── digitals_controller.rb │ │ └── digitals_controller.rb └── services │ └── spree │ └── digital_links_creator.rb ├── db └── migrate │ ├── 20170308121840_add_drm_flag_to_digitals.rb │ ├── 20111207121840_rename_digital_to_namespace.rb │ ├── 20170308134726_create_drm_records.rb │ └── 20110410134726_create_digitals.rb ├── .gitignore ├── config ├── routes.rb └── locales │ ├── ja.yml │ ├── it.yml │ ├── de.yml │ ├── es.yml │ └── en.yml ├── Versionfile ├── LICENSE.md ├── solidus_digital.gemspec ├── LICENSE ├── Gemfile ├── .circleci └── config.yml ├── .rubocop_todo.yml └── README.md /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | issues=false 2 | exclude-labels=infrastructure 3 | -------------------------------------------------------------------------------- /doc/tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio-contrib/solidus_digital/HEAD/doc/tables.png -------------------------------------------------------------------------------- /spec/support/active_model_mocks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/active_model/mocks' 4 | -------------------------------------------------------------------------------- /lib/solidus_digital/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | VERSION = '1.7.6' 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/thinking-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio-contrib/solidus_digital/HEAD/spec/fixtures/thinking-cat.jpg -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - solidus_dev_support/rubocop 5 | 6 | AllCops: 7 | NewCops: disable 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See https://github.com/solidusio-contrib/solidus_digital/releases or OLD_CHANGELOG.md for older versions. 4 | -------------------------------------------------------------------------------- /.gem_release.yml: -------------------------------------------------------------------------------- 1 | bump: 2 | recurse: false 3 | file: 'lib/solidus_digital/version.rb' 4 | message: Bump SolidusDigital to %{version} 5 | tag: true 6 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "rubygems" 5 | require "bundler/setup" 6 | 7 | load Gem.bin_path("rake", "rake") 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | gem install bundler --conservative 7 | bundle update 8 | bin/rake clobber 9 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if %w[g generate].include? ARGV.first 4 | exec "#{__dir__}/rails-engine", *ARGV 5 | else 6 | exec "#{__dir__}/rails-sandbox", *ARGV 7 | end 8 | -------------------------------------------------------------------------------- /lib/solidus_digital.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spree/solidus_digital_configuration' 4 | require 'solidus_digital/version' 5 | require 'solidus_digital/engine' 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require 'solidus_dev_support/rake_tasks' 5 | SolidusDevSupport::RakeTasks.install 6 | 7 | task default: 'extension:specs' 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/solidus_digital.js: -------------------------------------------------------------------------------- 1 | // Placeholder manifest file. 2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js' -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/solidus_digital.js: -------------------------------------------------------------------------------- 1 | // Placeholder manifest file. 2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/solidus_digital.css: -------------------------------------------------------------------------------- 1 | /* 2 | Placeholder manifest file. 3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css' 4 | */ 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/solidus_digital.css: -------------------------------------------------------------------------------- 1 | /* 2 | Placeholder manifest file. 3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css' 4 | */ 5 | -------------------------------------------------------------------------------- /spec/support/shoulda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shoulda-matchers' 4 | 5 | Shoulda::Matchers.configure do |config| 6 | config.integrate do |with| 7 | with.test_framework :rspec 8 | with.library :rails 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170308121840_add_drm_flag_to_digitals.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDrmFlagToDigitals < ActiveRecord::Migration[4.2] 4 | def change 5 | add_column :spree_digitals, :drm, :boolean, default: false, null: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | \#* 3 | *~ 4 | .#* 5 | .DS_Store 6 | .idea 7 | .project 8 | .sass-cache 9 | coverage 10 | Gemfile.lock 11 | Gemfile-local 12 | tmp 13 | nbproject 14 | pkg 15 | *.swp 16 | spec/dummy 17 | spec/examples.txt 18 | /sandbox 19 | .rvmrc 20 | .ruby-version 21 | .ruby-gemset 22 | -------------------------------------------------------------------------------- /app/views/spree/digitals/_digital.html.erb: -------------------------------------------------------------------------------- 1 | <% if digital.attachment_file_name.present? %> 2 | <%= "DRM" if digital.drm %> <%= digital.attachment_file_name %> (<%= number_to_human_size(digital.attachment_file_size) %>) 3 | <% else %> 4 | <%= I18n.t('spree.digitals.broken_file') %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /db/migrate/20111207121840_rename_digital_to_namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RenameDigitalToNamespace < ActiveRecord::Migration[4.2] 4 | def change 5 | rename_table :digitals, :spree_digitals 6 | rename_table :digital_links, :spree_digital_links 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/spree/admin/digitals/_digital.html.erb: -------------------------------------------------------------------------------- 1 | <% if digital.attachment_file_name.present? %> 2 | <%= "DRM" if digital.drm %> <%= digital.attachment_file_name %> (<%= number_to_human_size(digital.attachment_file_size) %>) 3 | <% else %> 4 | <%= I18n.t('spree.digitals.broken_file') %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/models/solidus_digital/permission_sets/digital_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module PermissionSets 5 | class DigitalManagement < ::Spree::PermissionSets::Base 6 | def activate! 7 | can :manage, ::Spree::Digital 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/solidus_digital/permission_sets/digital_display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module PermissionSets 5 | class DigitalDisplay < ::Spree::PermissionSets::Base 6 | def activate! 7 | can [:display, :admin], ::Spree::Digital 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/overrides/spree/shared/add_digital_downloads_to_invoice.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Deface::Override.new( 4 | virtual_path: "spree/shared/_order_details", 5 | name: "add_digital_downloads_to_invoice", 6 | insert_bottom: "td[data-hook='order_item_description']", 7 | partial: "spree/shared/digital_download_links" 8 | ) 9 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/shared/_order_submenu/add_digital_versions_to_admin_product_tabs.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | <% if can?(:update, @order) && @order.digital? %> 3 |
  • 4 | <%= link_to I18n.t('spree.digitals.reset_downloads'), reset_digitals_admin_order_url(@order) %> 5 |
  • 6 | <% end %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/views/spree/digitals/unauthorized.html.erb: -------------------------------------------------------------------------------- 1 | <% if defined?(SpreeFancy) %> 2 | <% content_for :subheader do %> 3 |

    <%= I18n.t('spree.digitals.unauthorized.unauthorized') %>

    4 | <% end %> 5 | <% else %> 6 |

    <%= I18n.t('spree.digitals.unauthorized.unauthorized') %>

    7 | <% end %> 8 | 9 | <%= I18n.t('spree.digitals.unauthorized.explained') %> 10 | -------------------------------------------------------------------------------- /bin/rails-sandbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | app_root = 'sandbox' 4 | 5 | unless File.exist? "#{app_root}/bin/rails" 6 | warn 'Creating the sandbox app...' 7 | Dir.chdir "#{__dir__}/.." do 8 | system "#{__dir__}/sandbox" or begin 9 | warn 'Automatic creation of the sandbox app failed' 10 | exit 1 11 | end 12 | end 13 | end 14 | 15 | Dir.chdir app_root 16 | exec 'bin/rails', *ARGV 17 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/shared/_product_tabs/add_digital_versions_to_admin_product_tabs.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= content_tag :li, class: ('active' if current == 'Digital Versions') do %> 4 | <%= link_to t('spree.digitals.digital_versions'), admin_product_digitals_path(@product) %> 5 | <% end if can?(:admin, Spree::Digital) %> 6 | -------------------------------------------------------------------------------- /app/decorators/models/solidus_digital/spree/product_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Spree 5 | module ProductDecorator 6 | def self.prepended(base) 7 | base.class_eval do 8 | has_many :digitals, through: :variants_including_master 9 | end 10 | end 11 | 12 | ::Spree::Product.prepend self 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Spree::Core::Engine.routes.draw do 4 | namespace :admin do 5 | resources :products do 6 | resources :digitals 7 | end 8 | 9 | resources :orders do 10 | member do 11 | get :reset_digitals 12 | end 13 | end 14 | end 15 | 16 | get '/digital/:secret', to: 'digitals#show', as: 'digital', constraints: { secret: /[a-zA-Z0-9]{30}/ } 17 | end 18 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require "bundler/setup" 6 | require "solidus_digital" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"]) 11 | 12 | # (If you use this, don't forget to add pry to your Gemfile!) 13 | # require "pry" 14 | # Pry.start 15 | 16 | require "irb" 17 | IRB.start(__FILE__) 18 | -------------------------------------------------------------------------------- /spec/support/testing_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module TestingSupport 5 | module Helpers 6 | def image(filename) 7 | File.open(SolidusDigital::Engine.root + "spec/fixtures" + filename) 8 | end 9 | 10 | def upload_image(filename) 11 | fixture_file_upload(image(filename).path) 12 | end 13 | end 14 | end 15 | end 16 | 17 | RSpec.configure do |config| 18 | config.include SolidusDigital::TestingSupport::Helpers 19 | end 20 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | broken_file: 注意! このファイルが壊れている! 3 | current_file: 現在のバージョン 4 | delete_file: このファイルを削除 5 | delete_file_confirmation: 本当に「%{filename}」を削除しても宜しいですか? 6 | digital_shipping: ダウンロードリンクを%{email}に送ります 7 | digital_versions: デジタル版 8 | new_file: 新しいファイル 9 | file_is_not_ready: File is processing, please stand by 10 | spree: 11 | digitals: 12 | unauthorized: 13 | explained: This download link has expired. 14 | unauthorized: Unauthorized 15 | upload: アップロード 16 | -------------------------------------------------------------------------------- /bin/rails-engine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/solidus_digital/engine', __dir__) 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 11 | 12 | require 'rails/all' 13 | require 'rails/engine/commands' 14 | -------------------------------------------------------------------------------- /db/migrate/20170308134726_create_drm_records.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDrmRecords < ActiveRecord::Migration[4.2] 4 | def change 5 | create_table :spree_drm_records do |t| 6 | t.integer :digital_id 7 | t.integer :line_item_id 8 | t.string :attachment_file_name 9 | t.string :attachment_content_type 10 | t.integer :attachment_file_size 11 | t.timestamps 12 | end 13 | add_index :spree_drm_records, :digital_id 14 | add_index :spree_drm_records, :line_item_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/decorators/controllers/solidus_digital/spree/admin/orders_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Spree 5 | module Admin 6 | module OrdersControllerDecorator 7 | def reset_digitals 8 | load_order 9 | @order.reset_digital_links! 10 | flash[:notice] = I18n.t('spree.digitals.downloads_reset') 11 | redirect_to spree.edit_admin_order_path(@order) 12 | end 13 | 14 | ::Spree::Admin::OrdersController.prepend self 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/models/spree/product_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Product do 6 | let(:product) { create(:product) } 7 | let(:digitals) { 3.times.map { create(:digital) } } 8 | let!(:variants) do 9 | digitals.map { |d| create(:variant, product: product, digitals: [d]) } 10 | end 11 | 12 | context 'digitals' do 13 | it 'returns the digitals from the variants' do 14 | product_digitals = product.digitals 15 | digitals.each { |d| expect(product_digitals).to include(d) } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Versionfile: -------------------------------------------------------------------------------- 1 | "0.7.x" => { :ref => "eddaea63959586d123007cbca3be5bf9c5edb1a7" } 2 | "1.0.x" => { :ref => "a8c27750ef1cf9d0ad1a2a6ebe33307da900a5c1" } 3 | "1.1.x" => { :branch => "1-1-stable" } 4 | "1.2.x" => { :ref => "9360c635039aaeeee18026b830aa96cc7587cd0d" } 5 | "1.3.x" => { :branch => "1-3-stable" } 6 | "2.0.x" => { :branch => "2-0-stable" } 7 | "2.1.x" => { :branch => "2-1-stable" } 8 | "2.2.x" => { :branch => "2-2-stable" } 9 | "2.3.x" => { :branch => "2-3-stable" } 10 | "2.4.x" => { :branch => "2-4-stable" } 11 | "3.0.x" => { :branch => "3-0-stable" } 12 | "3.1.x" => { :branch => "master" } 13 | -------------------------------------------------------------------------------- /spec/models/spree/digital_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Digital do 6 | context 'validation' do 7 | it { is_expected.to belong_to(:variant) } 8 | end 9 | 10 | describe "#destroy" do 11 | it "destroys associated digital_links" do 12 | digital = create(:digital) 13 | 3.times { digital.digital_links.create!(line_item: create(:line_item)) } 14 | expect(Spree::DigitalLink.count).to eq(3) 15 | expect { 16 | digital.destroy 17 | }.to change(Spree::DigitalLink, :count).by(-3) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/decorators/models/solidus_digital/spree/line_item_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Spree 5 | module LineItemDecorator 6 | def self.prepended(base) 7 | base.class_eval do 8 | has_many :digital_links, dependent: :destroy 9 | end 10 | end 11 | 12 | def digital? 13 | variant.digital? || variant.product.master.digital? 14 | end 15 | 16 | def create_digital_links 17 | ::Spree::DigitalLinksCreator.new(self).create_digital_links 18 | end 19 | 20 | ::Spree::LineItem.prepend self 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/spree/admin/digitals/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => "Digital Versions"} %> 2 | 3 | <% if @product.has_variants? %> 4 | <% for variant in @product.variants_including_master do %> 5 | <%= render 'form', :variant => variant %> 6 | <% end %> 7 | <% else %> 8 | <%= t('spree.digitals.product_no_variants') %> 9 | <% if @product.master.digital? %> 10 | <% if can?(:display, Spree::Digital) %> 11 | <%= t('spree.digitals.product_with_variants') %> 12 | <%= render @product.master.digitals %> 13 | <% end %> 14 | <% end %> 15 | <%= render 'form', :variant => @product.master %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/models/spree/digital.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | class Digital < ApplicationRecord 5 | belongs_to :variant 6 | has_many :digital_links, dependent: :destroy 7 | has_many :drm_records, dependent: :destroy 8 | 9 | has_attached_file :attachment, path: ":rails_root/private/digitals/:id/:basename.:extension", s3_permissions: :private, s3_headers: { content_disposition: 'attachment' } 10 | do_not_validate_attachment_file_type :attachment 11 | validates_attachment_presence :attachment 12 | 13 | def cloud? 14 | attachment.options[:storage] == :s3 15 | end 16 | 17 | def create_drm_record(line_item) 18 | drm_records.create!(line_item: line_item) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/spree/shared/_digital_download_links.html.erb: -------------------------------------------------------------------------------- 1 | <% if @order.complete? && item.variant.digital? %> 2 |
    3 | 16 |
    17 | <% end %> 18 | -------------------------------------------------------------------------------- /app/models/spree/calculator/shipping/digital_delivery.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/spree/spree/issues/1439 4 | require_dependency 'spree/shipping_calculator' 5 | 6 | module Spree 7 | module Calculator::Shipping 8 | class DigitalDelivery < ShippingCalculator 9 | preference :amount, :decimal, default: 0 10 | preference :currency, :string, default: ->{ Spree::Config[:currency] } 11 | 12 | def self.description 13 | I18n.t('spree.digitals.digital_delivery') 14 | end 15 | 16 | def compute_package(_package = nil) 17 | preferred_amount 18 | end 19 | 20 | def available?(package) 21 | package.contents.all? { |content| content.variant.digital? } 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/spree/drm_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | class DrmRecord < ApplicationRecord 5 | belongs_to :digital 6 | belongs_to :line_item 7 | 8 | after_create :prepare_drm_mark 9 | 10 | has_attached_file :attachment, path: ":rails_root/private/digitals/drm/:id/:basename.:extension" 11 | do_not_validate_attachment_file_type :attachment 12 | 13 | if Paperclip::Attachment.default_options[:storage] == :s3 14 | attachment_definitions[:attachment][:s3_permissions] = :private 15 | attachment_definitions[:attachment][:s3_headers] = { content_disposition: 'attachment' } 16 | end 17 | 18 | private 19 | 20 | def prepare_drm_mark 21 | # TODO: implement DRM functionality, set new file for DRM record 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | broken_file: Attenzione! questo file sembra essere rotto 3 | current_file: File Corrente 4 | delete_file: Elimina questo file 5 | delete_file_confirmation: Sei sicuro di voler eliminare il file %{filename}? 6 | digital_delivery: Consegna Digitale 7 | digital_download: Download %{filename} ↓ 8 | digital_download_links: Link al Download Digitale 9 | file_is_not_ready: File is processing, please stand by 10 | digital_format: 11 | mp3: Audio MP3 12 | mobi: Kindle eBook 13 | epub: ePub eBook 14 | pdf: pdf eBook 15 | digital_versions: Versioni Digitali 16 | new_file: Nuovo File 17 | spree: 18 | digitals: 19 | unauthorized: 20 | explained: This download link has expired. 21 | unauthorized: Unauthorized 22 | solidus_digital: 23 | upload: Upload 24 | -------------------------------------------------------------------------------- /db/migrate/20110410134726_create_digitals.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDigitals < ActiveRecord::Migration[4.2] 4 | def self.up 5 | create_table :digitals do |t| 6 | t.integer :variant_id 7 | t.string :attachment_file_name 8 | t.string :attachment_content_type 9 | t.integer :attachment_file_size 10 | t.timestamps 11 | end 12 | add_index :digitals, :variant_id 13 | 14 | create_table :digital_links, force: true do |t| 15 | t.integer :digital_id 16 | t.integer :line_item_id 17 | t.string :secret 18 | t.integer :access_counter 19 | t.timestamps 20 | end 21 | add_index :digital_links, :digital_id 22 | add_index :digital_links, :line_item_id 23 | add_index :digital_links, :secret 24 | end 25 | 26 | def self.down 27 | drop_table :digitals 28 | drop_table :digital_links 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/solidus_digital/testing_support/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :digital, class: Spree::Digital do |f| 5 | f.variant { |p| p.association(:variant) } 6 | f.attachment_content_type { 'application/octet-stream' } 7 | f.attachment_file_name { "#{SecureRandom.hex(5)}.epub" } 8 | end 9 | 10 | factory :digital_link, class: Spree::DigitalLink do |f| 11 | f.digital { |p| p.association(:digital) } 12 | f.line_item { |p| p.association(:line_item) } 13 | end 14 | 15 | factory :digital_shipping_calculator, class: Spree::Calculator::Shipping::DigitalDelivery do 16 | after :create do |c| 17 | c.set_preference(:amount, 0) 18 | end 19 | end 20 | 21 | factory :digital_shipping_method, parent: :shipping_method do 22 | name { "Digital Delivery" } 23 | calculator { FactoryBot.build :digital_shipping_calculator } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/solidus_digital/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | class_option :auto_run_migrations, type: :boolean, default: false 7 | 8 | def self.exit_on_failure? 9 | true 10 | end 11 | 12 | def add_migrations 13 | run 'bin/rails railties:install:migrations FROM=solidus_digital' 14 | end 15 | 16 | def run_migrations 17 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Layout/LineLength 18 | if run_migrations 19 | run 'bin/rails db:migrate' 20 | else 21 | puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/decorators/models/solidus_digital/spree/variant_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Spree 5 | module VariantDecorator 6 | def self.prepended(base) 7 | base.class_eval do 8 | has_many :digitals 9 | after_save :destroy_digital, if: :deleted? 10 | end 11 | end 12 | 13 | # Is this variant to be downloaded by the customer? 14 | def digital? 15 | digitals.present? 16 | end 17 | 18 | private 19 | 20 | # :dependent => :destroy needs to be handled manually 21 | # spree does not delete variants, just marks them as deleted? 22 | # optionally keep digitals around for customers who require continued access to their purchases 23 | def destroy_digital 24 | digitals.map(&:destroy) unless ::Spree::DigitalConfiguration[:keep_digitals] 25 | end 26 | 27 | ::Spree::Variant.prepend self 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/digitals_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module Admin 5 | class DigitalsController < ResourceController 6 | belongs_to "spree/product", find_by: :slug 7 | 8 | def create 9 | invoke_callbacks(:create, :before) 10 | @object.attributes = permitted_resource_params 11 | 12 | if @object.valid? 13 | super 14 | else 15 | invoke_callbacks(:create, :fails) 16 | flash[:error] = @object.errors.full_messages.join(", ") 17 | redirect_to location_after_save 18 | end 19 | end 20 | 21 | protected 22 | 23 | def location_after_save 24 | spree.admin_product_digitals_path(@product) 25 | end 26 | 27 | def permitted_resource_params 28 | params.require(:digital).permit(permitted_digital_attributes) 29 | end 30 | 31 | def permitted_digital_attributes 32 | [:variant_id, :drm, :attachment] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/controllers/admin/orders_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Admin::OrdersController do 6 | context "with authorization" do 7 | stub_authorization! 8 | 9 | let(:order) do 10 | create(:completed_order_with_totals) do |o| 11 | create(:digital, variant: o.line_items.first.variant) 12 | end 13 | end 14 | let!(:digital_link) do 15 | create(:digital_link, access_counter: 3, line_item: order.line_items.first) 16 | end 17 | 18 | before do 19 | request.env["HTTP_REFERER"] = "http://localhost:3000" 20 | end 21 | 22 | describe '#reset_digitals' do 23 | it 'resets digitals for an order' do 24 | expect do 25 | get :reset_digitals, params: { id: order.number } 26 | digital_link.reload 27 | end.to change(digital_link, :access_counter).to(0) 28 | 29 | expect(response).to redirect_to(spree.edit_admin_order_path(order)) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/spree/stock/splitter/digital_splitter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module Stock 5 | module Splitter 6 | class DigitalSplitter < Spree::Stock::Splitter::Base 7 | def split(packages) 8 | split_packages = [] 9 | packages.each do |package| 10 | split_packages += split_by_digital(package) 11 | end 12 | return_next split_packages 13 | end 14 | 15 | private 16 | 17 | def split_by_digital(package) 18 | digitals = Hash.new { |hash, key| hash[key] = [] } 19 | package.contents.each do |item| 20 | digitals[item.variant.digital?] << item 21 | end 22 | hash_to_packages(digitals) 23 | end 24 | 25 | def hash_to_packages(digitals) 26 | packages = [] 27 | digitals.each_value do |contents| 28 | packages << build_package(contents) 29 | end 30 | packages 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/decorators/models/solidus_digital/spree/order_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusDigital 4 | module Spree 5 | module OrderDecorator 6 | def self.prepended(base) 7 | base.state_machine.after_transition to: :complete, do: :generate_digital_links, if: :some_digital? 8 | end 9 | 10 | # all products are digital 11 | def digital? 12 | line_items.all?(&:digital?) 13 | end 14 | 15 | def some_digital? 16 | line_items.any?(&:digital?) 17 | end 18 | 19 | def digital_line_items 20 | line_items.select(&:digital?) 21 | end 22 | 23 | def digital_links 24 | digital_line_items.map(&:digital_links).flatten 25 | end 26 | 27 | def reset_digital_links! 28 | digital_links.each(&:reset!) 29 | end 30 | 31 | private 32 | 33 | def generate_digital_links 34 | line_items.each { |li| li.create_digital_links if li.digital? } 35 | end 36 | 37 | ::Spree::Order.prepend self 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/services/spree/digital_links_creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::DigitalLinksCreator 4 | attr_reader :line_item 5 | 6 | delegate :quantity, :digital_links, :variant, :master, to: :line_item 7 | 8 | def initialize(line_item) 9 | @line_item = line_item 10 | end 11 | 12 | def create_digital_links 13 | digital_links.delete_all 14 | 15 | # include master variant digitals 16 | master = variant.product.master 17 | create_digital_links_for_variant(master) if master.digital? 18 | create_digital_links_for_variant(variant) unless variant.is_master 19 | end 20 | 21 | private 22 | 23 | def create_digital_links_for_variant(variant) 24 | variant.digitals.each do |digital| 25 | digital.create_drm_record(line_item) if digital.drm? 26 | digital_links_count.times { digital_links.create!(digital: digital) } 27 | end 28 | end 29 | 30 | def digital_links_count 31 | count = Spree::DigitalConfiguration[:digital_links_count] 32 | count == 'quantity' ? quantity : Integer(count).abs 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/spree/solidus_digital_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spree/preferences/configuration' 4 | 5 | module Spree 6 | class SpreeDigitalConfiguration < Preferences::Configuration 7 | # number of times a customer can download a digital file 8 | # nil - infinite number of clicks 9 | preference :authorized_clicks, :integer, default: 3 10 | 11 | # number of days after initial purchase the customer can download a file 12 | preference :authorized_days, :integer, default: 2 13 | 14 | # should digitals be kept around after the associated product is destroyed 15 | preference :keep_digitals, :boolean, default: false 16 | 17 | # number of seconds before an s3 link expires 18 | preference :s3_expiration_seconds, :integer, default: 10 19 | 20 | # number of digital links generated per line item 21 | # accepts: 'quantity' or Integer numbers 22 | # quantity - 'line_item.quantity' digital links will be generated 23 | # Integer 'number' - 'number' digital links will be generated 24 | preference :digital_links_count, :string, default: 'quantity' 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011-2015 halo. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | spree: 3 | controller: 4 | controller_name: Digitale Produkte 5 | digitals: 6 | broken_file: Achtung! Diese Datei ist korrupt 7 | current_file: Aktuelle Datei 8 | delete_file: Diese Datei löschen 9 | delete_file_confirmation: Sind Sie sicher, dass die Datei %{filename} gelöscht werden soll? 10 | digitals: Digitale Produkte 11 | digital_delivery: Digitale Auslieferung 12 | digital_download: Download %{filename} ↓ 13 | digital_download_links: Digitale Download Links 14 | file_is_not_ready: File is processing, please stand by 15 | digital_format: 16 | mp3: Audio MP3 17 | mobi: Kindle eBook 18 | epub: ePub eBook 19 | pdf: PDF eBook 20 | zip: ZIP-Datei 21 | digital_versions: Digitale Versionen 22 | downloads_reset: Digitale Downloads zurücksetzen 23 | files: Dateien 24 | new_file: Neue Datei 25 | reset_downloads: Digitale Downloads zurücksetzen 26 | unauthorized: 27 | explained: Der Download-Link ist nicht mehr gültig. 28 | unauthorized: Fehlende Berechtigung 29 | upload: Upload 30 | -------------------------------------------------------------------------------- /app/controllers/spree/digitals_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | class DigitalsController < Spree::BaseController 5 | rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found 6 | before_action :authorize_digital_link, only: :show 7 | 8 | def show 9 | if digital_link.cloud? 10 | redirect_to attachment.expiring_url(Spree::DigitalConfiguration[:s3_expiration_seconds]) 11 | else 12 | send_file attachment.path, filename: attachment.original_filename, type: attachment.content_type 13 | end 14 | end 15 | 16 | private 17 | 18 | def authorize_digital_link 19 | # don't authorize the link unless the file exists 20 | raise ActiveRecord::RecordNotFound if attachment.blank? 21 | 22 | render :unauthorized unless digital_link.file_exists? && digital_link.authorize! 23 | end 24 | 25 | def digital_link 26 | @link ||= DigitalLink.find_by!(secret: params[:secret]) 27 | end 28 | 29 | def attachment 30 | @attachment ||= digital_link.attachment 31 | end 32 | 33 | def resource_not_found 34 | render body: nil, status: :not_found 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | spree: 3 | digitals: 4 | broken_file: ¡Atención! Este archivo está corrupto 5 | current_file: Archivo actual 6 | delete_file: Borrar este archivo 7 | delete_file_confirmation: ¿Está seguro de que quiere borrar este fichero %{filename}? 8 | digital_delivery: Entrega digital 9 | digital_download: Descarga %{filename} 10 | digital_download_links: Enlaces para la descaga digital 11 | file_is_not_ready: File is processing, please stand by 12 | digital_format: 13 | mp3: Audio MP3 14 | mobi: Kindle eBook 15 | epub: ePub eBook 16 | pdf: pdf eBook 17 | digital_versions: Versiones Digitales 18 | downloads_reset: Reset de descargas digitales 19 | files: Archivos 20 | file_name: Nombre de Archivo 21 | new_file: Nuevo Archivo 22 | reset_downloads: Resetear descargas digitales 23 | unauthorized: 24 | explained: El enlace para la descarga ha expirado 25 | unauthorized: No estás autorizado 26 | upload: Subir 27 | product_no_variants: Este Producto no tiene variantes 28 | product_with_variants: "Este producto tiene versión digital:" 29 | -------------------------------------------------------------------------------- /spec/models/spree/drm_record_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | class SampleDrmMaker 6 | def initialize(drm_record) 7 | @drm_record = drm_record 8 | end 9 | 10 | def create! 11 | if @drm_record.digital.attachment.exists? 12 | @drm_record.attachment = @drm_record.digital.attachment 13 | end 14 | end 15 | end 16 | 17 | Spree::DrmRecord.class_eval do 18 | private 19 | 20 | def prepare_drm_mark 21 | SampleDrmMaker.new(self).create! 22 | end 23 | end 24 | 25 | RSpec.describe Spree::DrmRecord do 26 | describe "#create" do 27 | let(:sample_file) { File.open(SolidusDigital::Engine.root.join("spec", "fixtures", "thinking-cat.jpg")) } 28 | let(:digital) { create(:digital, drm: true, attachment: sample_file) } 29 | let(:digital_variant) { create(:variant, digitals: [digital]) } 30 | let(:line_item) { create(:line_item, variant: digital_variant) } 31 | 32 | it "creates drm marked attachment" do 33 | drm_record = digital.drm_records.create(line_item: line_item) 34 | 35 | expect(drm_record.attachment.present?).to eq(true) 36 | expect(drm_record.attachment_file_name).to eq("thinking-cat.jpg") 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/solidus_digital/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_core' 4 | require 'solidus_support' 5 | 6 | module SolidusDigital 7 | class Engine < Rails::Engine 8 | include SolidusSupport::EngineExtensions 9 | 10 | isolate_namespace ::Spree 11 | 12 | engine_name 'solidus_digital' 13 | 14 | initializer "solidus_digital.zeitwerk_ignore_deface_overrides", before: :eager_load! do |app| 15 | app.autoloaders.main.ignore(root.join('app/overrides')) 16 | end 17 | 18 | initializer "solidus_digital.preferences", before: "spree.environment" do |_app| 19 | ::Spree::DigitalConfiguration = ::Spree::SpreeDigitalConfiguration.new 20 | end 21 | 22 | initializer "solidus_digital.digital_shipping", after: "spree.environment" do |app| 23 | app.config.spree.calculators.shipping_methods << "Spree::Calculator::Shipping::DigitalDelivery" 24 | end 25 | 26 | initializer "solidus_digital.digital_splitter", after: "spree.environment" do |app| 27 | app.config.spree.stock_splitters << "Spree::Stock::Splitter::DigitalSplitter" 28 | end 29 | 30 | # use rspec for tests 31 | config.generators do |g| 32 | g.test_framework :rspec 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | spree: 3 | controller: 4 | controller_name: Digitals 5 | digitals: 6 | broken_file: Warning! this file is broken 7 | current_file: Current File 8 | delete_file: Delete this file 9 | delete_file_confirmation: Are you sure you want to delete the file %{filename}? 10 | digitals: Digitals 11 | digital_delivery: Digital Delivery 12 | digital_download: Download %{filename} ↓ 13 | digital_download_links: Digital Download Links 14 | file_is_not_ready: File is processing, please stand by 15 | digital_format: 16 | mp3: Audio MP3 17 | mobi: Kindle eBook 18 | epub: ePub eBook 19 | pdf: pdf eBook 20 | zip: ZIP file 21 | digital_versions: Digital Versions 22 | downloads_reset: Digital Downloads Reset 23 | files: Files 24 | new_file: New File 25 | reset_downloads: Reset Digital Downloads 26 | unauthorized: 27 | explained: This download link has expired. 28 | unauthorized: Unauthorized 29 | upload: Upload 30 | file_name: File name 31 | product_no_variants: This product has no variants. 32 | product_with_variants: "A digital version of this product currently exists:" 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Configure Rails Environment 4 | ENV['RAILS_ENV'] = 'test' 5 | 6 | # Run Coverage report 7 | require 'solidus_dev_support/rspec/coverage' 8 | 9 | # Create the dummy app if it's still missing. 10 | dummy_env = "#{__dir__}/dummy/config/environment.rb" 11 | system 'bin/rake extension:test_app' unless File.exist? dummy_env 12 | require dummy_env 13 | 14 | # Requires factories and other useful helpers defined in spree_core. 15 | require 'solidus_dev_support/rspec/feature_helper' 16 | 17 | # Requires supporting ruby files with custom matchers and macros, etc, 18 | # in spec/support/ and its subdirectories. 19 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f } 20 | 21 | # Requires factories defined in Solidus core and this extension. 22 | # See: lib/solidus_digital/testing_support/factories.rb 23 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusDigital::Engine) 24 | 25 | RSpec.configure do |config| 26 | config.infer_spec_type_from_file_location! 27 | config.use_transactional_fixtures = false 28 | config.include Devise::Test::ControllerHelpers, type: :controller 29 | 30 | if Spree.solidus_gem_version < Gem::Version.new('2.11') 31 | config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :system 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/models/spree/stock/splitter/digital_splitter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module Spree 6 | module Stock 7 | module Splitter 8 | RSpec.describe DigitalSplitter do 9 | subject { described_class.new(stock_location) } 10 | 11 | let(:item1) { create(:inventory_unit, variant: create(:digital).variant) } 12 | let(:item2) { create(:inventory_unit, variant: create(:variant)) } 13 | let(:item3) { create(:inventory_unit, variant: create(:variant)) } 14 | let(:item4) { create(:inventory_unit, variant: create(:digital).variant) } 15 | let(:item5) { create(:inventory_unit, variant: create(:digital).variant) } 16 | 17 | let(:stock_location) { mock_model(Spree::StockLocation) } 18 | 19 | it 'splits each package by product' do 20 | package1 = Package.new(stock_location) 21 | package1.add item1, :on_hand 22 | package1.add item2, :on_hand 23 | package1.add item3, :on_hand 24 | package1.add item4, :on_hand 25 | package1.add item5, :on_hand 26 | 27 | packages = subject.split([package1]) 28 | 29 | expect(packages[0].quantity).to eq 3 30 | expect(packages[1].quantity).to eq 2 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/models/spree/variant_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Variant do 6 | describe "#destroy" do 7 | let(:variant) { create(:variant) } 8 | let!(:digital) { create(:digital, variant: variant) } 9 | 10 | it "destroys associated digitals by default" do 11 | # default is false 12 | stub_spree_preferences(Spree::DigitalConfiguration, keep_digitals: false) 13 | 14 | expect(Spree::Digital.count).to eq(1) 15 | expect(variant.digitals.present?).to be true 16 | 17 | variant.deleted_at = Time.zone.now 18 | expect(variant.deleted?).to be true 19 | variant.save! 20 | 21 | expect { digital.reload.present? }.to raise_error(ActiveRecord::RecordNotFound) 22 | expect(Spree::Digital.count).to eq(0) 23 | end 24 | 25 | it "conditionallies keep associated digitals" do 26 | stub_spree_preferences(Spree::DigitalConfiguration, keep_digitals: true) 27 | 28 | expect(Spree::Digital.count).to eq(1) 29 | expect(variant.digitals.present?).to be true 30 | 31 | variant.deleted_at = Time.zone.now 32 | variant.save! 33 | expect(variant.deleted?).to be true 34 | expect { digital.reload.present? }.not_to raise_error 35 | expect(Spree::Digital.count).to eq(1) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /solidus_digital.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/solidus_digital/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'solidus_digital' 7 | spec.version = SolidusDigital::VERSION 8 | spec.summary = 'Digital download functionality for Solidus' 9 | spec.description = spec.summary 10 | spec.license = 'BSD-3-Clause' 11 | 12 | spec.author = ['funkensturm', 'Michael Bianco'] 13 | spec.email = 'info@cliffsidedev.com' 14 | spec.homepage = 'https://github.com/solidusio-contrib/solidus_digital' 15 | 16 | if spec.respond_to?(:metadata) 17 | spec.metadata["homepage_uri"] = spec.homepage if spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage if spec.homepage 19 | end 20 | 21 | spec.required_ruby_version = ['>= 2.4', '< 4.0'] 22 | 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | end 26 | spec.test_files = Dir['spec/**/*'] 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 5'] 32 | spec.add_dependency 'solidus_support', '~> 0.5' 33 | 34 | spec.add_development_dependency 'rspec-activemodel-mocks' 35 | spec.add_development_dependency 'shoulda-matchers' 36 | spec.add_development_dependency 'solidus_dev_support' 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 funkensturm 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name Solidus nor the names of its contributors may be used to 13 | endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /app/views/spree/order_mailer/confirm_email.text.erb: -------------------------------------------------------------------------------- 1 | <%= I18n.t('spree.order_mailer.confirm_email.dear_customer') %> 2 | 3 | <%= I18n.t('spree.order_mailer.confirm_email.instructions') %> 4 | 5 | ============================================================ 6 | <%= I18n.t('spree.order_mailer.confirm_email.order_summary') %> 7 | ============================================================ 8 | 9 | <% @order.line_items.each do |item| %> 10 | <%= item.variant.sku %> <%= raw(item.variant.product.name) %> <%= raw(item.variant.options_text) -%> (<%=item.quantity%>) @ <%= item.single_money %> = <%= item.display_amount %> 11 | <% end %> 12 | ============================================================ 13 | <%= I18n.t('spree.order_mailer.confirm_email.subtotal', :subtotal => @order.display_item_total) %> 14 | 15 | <% @order.adjustments.eligible.each do |adjustment| %> 16 | <%= raw(adjustment.label) %> <%= adjustment.display_amount %> 17 | <% end %> 18 | 19 | <%= I18n.t('spree.order_mailer.confirm_email.total', :total => @order.display_total) %> 20 | 21 | <% if @order.some_digital? %> 22 | ============================================================ 23 | Download links for digital products 24 | ============================================================ 25 | 26 | ATTENTION! Each link will only work a SINGLE TIME! 27 | Also, they will only work WITHIN 24 HOURS! 28 | 29 | <% for item in @order.line_items %> 30 | <% if item.digital? %> 31 | <%= raw item.variant.name %>: 32 | <% for link in item.digital_links %> 33 | <%= digital_url :host => @order.store.url, :secret => link.secret %> 34 | <% end %> 35 | <% end %> 36 | <% end %> 37 | ============================================================ 38 | <% end %> 39 | 40 | <%= I18n.t('spree.order_mailer.confirm_email.thanks') %> 41 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | branch = ENV.fetch('SOLIDUS_BRANCH', 'main') 7 | gem 'solidus', github: 'solidusio/solidus', branch: branch 8 | 9 | # The solidus_frontend gem has been pulled out since v3.2 10 | if branch >= 'v3.2' 11 | gem 'solidus_frontend' 12 | elsif branch == 'main' 13 | gem 'solidus_frontend', github: 'solidusio/solidus_frontend' 14 | else 15 | gem 'solidus_frontend', github: 'solidusio/solidus', branch: branch 16 | end 17 | 18 | # Needed to help Bundler figure out how to resolve dependencies, 19 | # otherwise it takes forever to resolve them. 20 | # See https://github.com/bundler/bundler/issues/6677 21 | gem 'rails', '>0.a' 22 | 23 | # Provides basic authentication functionality for testing parts of your engine 24 | gem 'solidus_auth_devise' 25 | 26 | case ENV.fetch('DB', nil) 27 | when 'mysql' 28 | gem 'mysql2' 29 | when 'postgresql' 30 | gem 'pg' 31 | else 32 | gem 'sqlite3', '~> 1.4' 33 | end 34 | 35 | gem 'rails-controller-testing' 36 | 37 | # While we still support Ruby < 3 we need to workaround a limitation in 38 | # the 'async' gem that relies on the latest ruby, since RubyGems doesn't 39 | # resolve gems based on the required ruby version. 40 | gem 'async', '< 3' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3') 41 | 42 | gemspec 43 | 44 | # Use a local Gemfile to include development dependencies that might not be 45 | # relevant for the project or for other contributors, e.g. pry-byebug. 46 | # 47 | # We use `send` instead of calling `eval_gemfile` to work around an issue with 48 | # how Dependabot parses projects: https://github.com/dependabot/dependabot-core/issues/1658. 49 | send(:eval_gemfile, 'Gemfile-local') if File.exist? 'Gemfile-local' 50 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | # Required for feature specs. 5 | browser-tools: circleci/browser-tools@1.1 6 | 7 | # Always take the latest version of the orb, this allows us to 8 | # run specs against Solidus supported versions only without the need 9 | # to change this configuration every time a Solidus version is released 10 | # or goes EOL. 11 | solidusio_extensions: solidusio/extensions@volatile 12 | 13 | jobs: 14 | run-specs-with-sqlite: 15 | executor: 16 | name: solidusio_extensions/sqlite 17 | ruby_version: "3.2" 18 | steps: 19 | - browser-tools/install-chrome 20 | - solidusio_extensions/run-tests 21 | run-specs-with-postgres: 22 | executor: 23 | name: solidusio_extensions/postgres 24 | ruby_version: "3.2" 25 | steps: 26 | - browser-tools/install-chrome 27 | - solidusio_extensions/run-tests 28 | run-specs-with-mysql: 29 | executor: 30 | name: solidusio_extensions/mysql 31 | ruby_version: "3.2" 32 | steps: 33 | - browser-tools/install-chrome 34 | - solidusio_extensions/run-tests 35 | lint-code: 36 | executor: 37 | name: solidusio_extensions/sqlite-memory 38 | ruby_version: "3.2" 39 | steps: 40 | - solidusio_extensions/lint-code 41 | 42 | workflows: 43 | "Run specs on supported Solidus versions": 44 | jobs: 45 | - run-specs-with-sqlite 46 | - run-specs-with-postgres 47 | - run-specs-with-mysql 48 | - lint-code 49 | 50 | "Weekly run specs against main": 51 | triggers: 52 | - schedule: 53 | cron: "0 0 * * 4" # every Thursday 54 | filters: 55 | branches: 56 | only: 57 | - main 58 | jobs: 59 | - run-specs-with-sqlite 60 | - run-specs-with-postgres 61 | - run-specs-with-mysql 62 | -------------------------------------------------------------------------------- /app/models/spree/digital_link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | class DigitalLink < ApplicationRecord 5 | belongs_to :digital 6 | belongs_to :line_item 7 | 8 | validates :digital, presence: true 9 | validates :secret, length: { is: 30 } 10 | before_validation :set_defaults, on: :create 11 | 12 | delegate :attachment_file_name, :cloud?, to: :digital 13 | 14 | # Can this link still be used? It is valid if it's less than 24 hours old and was not accessed more than 3 times 15 | def authorizable? 16 | !(expired? || access_limit_exceeded?) 17 | end 18 | 19 | def expired? 20 | created_at <= Spree::DigitalConfiguration[:authorized_days].day.ago 21 | end 22 | 23 | def ready? 24 | attachment.exists? 25 | end 26 | 27 | def file_exists? 28 | cloud? ? attachment.exists? : File.file?(attachment.path) 29 | end 30 | 31 | def access_limit_exceeded? 32 | return false if Spree::DigitalConfiguration[:authorized_clicks].nil? 33 | 34 | access_counter >= Spree::DigitalConfiguration[:authorized_clicks] 35 | end 36 | 37 | # This method should be called when a download is initiated. 38 | # It returns +true+ or +false+ depending on whether the authorization is granted. 39 | def authorize! 40 | authorizable? && increment!(:access_counter) ? true : false 41 | end 42 | 43 | def reset! 44 | update_column :access_counter, 0 45 | update_column :created_at, Time.zone.now 46 | end 47 | 48 | def attachment 49 | if digital.drm? 50 | digital.drm_records.find_by(line_item: line_item).attachment 51 | else 52 | digital.attachment 53 | end 54 | end 55 | 56 | private 57 | 58 | # Populating the secret automatically and zero'ing the access_counter (otherwise it might turn out to be NULL) 59 | def set_defaults 60 | self.secret = SecureRandom.hex(15) 61 | self.access_counter ||= 0 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/models/calculator/digital_delivery_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Calculator::Shipping::DigitalDelivery do 6 | subject { described_class.new } 7 | 8 | it 'has a description for the class' do 9 | expect(described_class).to respond_to(:description) 10 | end 11 | 12 | describe '#compute_package' do 13 | it 'ignores the passed in object' do 14 | expect { 15 | subject.compute_package(double) 16 | }.not_to raise_error 17 | end 18 | 19 | it 'alwayses return the preferred_amount' do 20 | amount_double = double 21 | expect(subject).to receive(:preferred_amount).and_return(amount_double) 22 | expect(subject.compute_package(double)).to eq(amount_double) 23 | end 24 | end 25 | 26 | describe '#available?' do 27 | let(:digital_variant) { build(:variant, digitals: [build(:digital)]) } 28 | let(:regular_variant) { build(:variant) } 29 | 30 | let(:digital_order) { 31 | package = Spree::Stock::Package.new(build(:stock_location), []) 32 | package.add(build(:inventory_unit, variant: digital_variant)) 33 | package 34 | } 35 | 36 | let(:mixed_order) { 37 | package = Spree::Stock::Package.new(build(:stock_location), []) 38 | package.add(build(:inventory_unit, variant: digital_variant)) 39 | package.add(build(:inventory_unit, variant: regular_variant)) 40 | package 41 | } 42 | 43 | let(:non_digital_order) { 44 | package = Spree::Stock::Package.new(build(:stock_location), []) 45 | package.add(build(:inventory_unit, variant: regular_variant)) 46 | package 47 | } 48 | 49 | it 'returns true for a digital order' do 50 | expect(subject.available?(digital_order)).to be true 51 | end 52 | 53 | it 'returns false for a mixed order' do 54 | expect(subject.available?(mixed_order)).to be false 55 | end 56 | 57 | it 'returns false for an exclusively non-digital order' do 58 | expect(subject.available?(non_digital_order)).to be false 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/views/spree/admin/digitals/_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= form_for(:digital, :url => admin_product_digitals_path(@product), :method => :create, :multipart => true ) do |f| %> 3 |
    4 | <%= Spree::Variant.model_name.human %> "<%= variant.is_master ? "Master" : variant.options_text %>" 5 | <%= I18n.t('spree.digitals.files') %>: 6 | <% if variant.digital? %> 7 | <% if can?(:display, Spree::Digital) %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% variant.digitals.each do |digital| %> 17 | 18 | 19 | 24 | 25 | <% end %> 26 | 27 |
    <%= I18n.t('spree.digitals.file_name') %>
    <%= render digital %> 20 | <% if can?(:destroy, Spree::Digital) %> 21 | <%= link_to_delete nil, url: admin_product_digital_url(@product, digital), no_text: true, confirm: I18n.t('spree.digitals.delete_file_confirmation', filename: digital.attachment_file_name) %> 22 | <% end %> 23 |
    28 | <% end %> 29 | <% else %> 30 | <%= I18n.t('spree.none') %> 31 | <% end %> 32 |

    33 | <% if can?(:create, Spree::Digital) %> 34 | <%= f.field_container :file do %> 35 | <%= f.label :file, I18n.t('spree.digitals.new_file') %> *
    36 | <%= f.file_field :attachment %> 37 | <%= f.label :drm %> 38 | <%= f.check_box :drm %> 39 | <% end %> 40 | 41 | <%= hidden_field_tag 'digital[variant_id]', variant.id %> 42 |
    43 | <%= button_tag I18n.t('spree.digitals.upload'), :class => "btn-success" %> 44 | <% end %> 45 |

    46 |
    47 | <% end %> 48 |
    49 | -------------------------------------------------------------------------------- /spec/controllers/digitals_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::DigitalsController, type: :controller do 6 | describe '#show' do 7 | let(:digital) { create(:digital) } 8 | let(:digital_link) { create(:digital_link, digital: digital) } 9 | 10 | it 'returns a 404 for a non-existent secret' do 11 | get :show, params: { secret: 'NotReal00000000000000000000000' } 12 | expect(response.status).to eq(404) 13 | end 14 | 15 | context 'unauthorized' do 16 | it 'returns a 200 and calls send_file for link that is not a file' do 17 | expect(digital_link).not_to receive(:cloud?) 18 | expect(controller).not_to receive(:send_file) 19 | 20 | get :show, params: { secret: digital_link.secret } 21 | expect(response.status).to eq(200) 22 | expect(response).to render_template(:unauthorized) 23 | end 24 | end 25 | 26 | context 'authorized' do 27 | before { allow(controller).to receive(:authorize_digital_link).and_return(true) } 28 | 29 | it 'returns a 200 and calls send_file that is a file' do 30 | expect(controller) 31 | .to receive(:send_file) 32 | .with( 33 | digital.attachment.path, 34 | filename: digital.attachment.original_filename, 35 | type: digital.attachment.content_type 36 | ){ controller.render body: nil, content_type: digital.attachment.content_type } 37 | 38 | get :show, params: { secret: digital_link.secret } 39 | expect(response.status).to eq(200) 40 | expect(response.header['Content-Type']).to match digital.attachment.content_type 41 | end 42 | 43 | it 'redirects to s3 when using s3' do 44 | skip 'TODO: needs a way to test without having a bucket' 45 | Paperclip::Attachment.default_options[:storage] = :s3 46 | 47 | expect(controller).to receive(:redirect_to) 48 | expect(controller).to receive(:attachment_is_file?).and_return(true) 49 | expect(controller).not_to receive(:send_file) 50 | 51 | get :show, params: { secret: digital_link.secret } 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /bin/sandbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | test -z "${DEBUG+empty_string}" || set -x 5 | 6 | test "$DB" = "sqlite" && export DB="sqlite3" 7 | 8 | if [ -z "$SOLIDUS_BRANCH" ] 9 | then 10 | echo "~~> Use 'export SOLIDUS_BRANCH=[main|v3.2|...]' to control the Solidus branch" 11 | SOLIDUS_BRANCH="main" 12 | fi 13 | echo "~~> Using branch $SOLIDUS_BRANCH of solidus" 14 | 15 | if [ -z "$SOLIDUS_FRONTEND" ] 16 | then 17 | echo "~~> Use 'export SOLIDUS_FRONTEND=[solidus_frontend|solidus_starter_frontend]' to control the Solidus frontend" 18 | SOLIDUS_FRONTEND="solidus_frontend" 19 | fi 20 | echo "~~> Using branch $SOLIDUS_FRONTEND as the solidus frontend" 21 | 22 | extension_name="solidus_digital" 23 | 24 | # Stay away from the bundler env of the containing extension. 25 | function unbundled { 26 | ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@ 27 | } 28 | 29 | rm -rf ./sandbox 30 | unbundled bundle exec rails new sandbox \ 31 | --database="${DB:-sqlite3}" \ 32 | --skip-bundle \ 33 | --skip-git \ 34 | --skip-keeps \ 35 | --skip-rc \ 36 | --skip-spring \ 37 | --skip-test \ 38 | --skip-javascript 39 | 40 | if [ ! -d "sandbox" ]; then 41 | echo 'sandbox rails application failed' 42 | exit 1 43 | fi 44 | 45 | cd ./sandbox 46 | cat <> Gemfile 47 | gem 'solidus', github: 'solidusio/solidus', branch: '$SOLIDUS_BRANCH' 48 | gem 'rails-i18n' 49 | gem 'solidus_i18n' 50 | 51 | gem '$extension_name', path: '..' 52 | 53 | group :test, :development do 54 | platforms :mri do 55 | gem 'pry-byebug' 56 | end 57 | end 58 | RUBY 59 | 60 | unbundled bundle install --gemfile Gemfile 61 | 62 | unbundled bundle exec rake db:drop db:create 63 | 64 | unbundled bundle exec rails generate solidus:install \ 65 | --auto-accept \ 66 | --user_class=Spree::User \ 67 | --enforce_available_locales=true \ 68 | --with-authentication=true \ 69 | --payment-method=none \ 70 | --frontend=${SOLIDUS_FRONTEND} \ 71 | $@ 72 | 73 | unbundled bundle exec rails generate solidus:auth:install --auto-run-migrations 74 | unbundled bundle exec rails generate ${extension_name}:install --auto-run-migrations 75 | 76 | echo 77 | echo "🚀 Sandbox app successfully created for $extension_name!" 78 | echo "🧪 This app is intended for test purposes." 79 | -------------------------------------------------------------------------------- /spec/models/spree/line_item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'spree/testing_support/order_walkthrough' 5 | 6 | RSpec.describe Spree::LineItem do 7 | let(:order) { create(:order) } 8 | let(:digital) { create(:digital) } 9 | let!(:digital_variant) { create(:variant, digitals: [digital]) } 10 | let!(:master_digital_variant) { create(:on_demand_master_variant, digitals: [create(:digital)]) } 11 | 12 | describe "#digital?" do 13 | it "reports as digital if either the master variant or selected variant has digitals" do 14 | expect(build(:variant)).not_to be_digital 15 | expect(build(:on_demand_master_variant)).not_to be_digital 16 | 17 | expect(digital_variant).to be_digital 18 | expect(master_digital_variant).to be_digital 19 | end 20 | end 21 | 22 | describe "#create_digital_links" do 23 | let(:line_item) { create(:line_item, order: order, variant: digital_variant) } 24 | 25 | context "digital has drm restrictions" do 26 | let(:digital) { create(:digital, drm: true) } 27 | 28 | it "creates digital link to drm record" do 29 | line_item.create_digital_links 30 | 31 | expect(digital.drm_records.count).to eq(1) 32 | end 33 | end 34 | 35 | context "digital links" do 36 | let(:digital) { create(:digital) } 37 | 38 | before do 39 | line_item.quantity = 8 40 | line_item.save 41 | end 42 | 43 | after { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "quantity") } 44 | 45 | context "when :digital_links_count settings set to 'quantity' (default)" do 46 | it "generates quantity x count digital links " do 47 | line_item.create_digital_links 48 | 49 | expect(digital.digital_links.count).to eq(8) 50 | end 51 | end 52 | 53 | context "when :digital_links_count settings set to '1'" do 54 | before { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "1") } 55 | 56 | it "generates quantity x count digital links " do 57 | line_item.create_digital_links 58 | 59 | expect(digital.digital_links.count).to eq(1) 60 | end 61 | end 62 | 63 | context "when :digital_links_count settings set to '-1'" do 64 | before { stub_spree_preferences(Spree::DigitalConfiguration, digital_links_count: "-1") } 65 | 66 | it "generates quantity x count digital links " do 67 | line_item.create_digital_links 68 | 69 | expect(digital.digital_links.count).to eq(1) 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe "#destroy" do 76 | it "destroys associated links when destroyed" do 77 | line_item = create(:line_item, order: order, variant: digital_variant) 78 | line_item.create_digital_links 79 | links = line_item.digital_links 80 | 81 | expect(links.to_a.size).to eq(1) 82 | expect(links.first.line_item).to eq(line_item) 83 | expect { 84 | line_item.destroy 85 | }.to change(Spree::DigitalLink, :count).by(-1) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /spec/models/spree/digital_link_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::DigitalLink do 6 | context 'validation' do 7 | it { is_expected.to belong_to(:digital) } 8 | it { is_expected.to belong_to(:line_item) } 9 | end 10 | 11 | describe "#create" do 12 | it "creates an appropriately long secret" do 13 | expect(create(:digital_link, secret: nil).secret.length).to eq(30) 14 | end 15 | 16 | it "zeroes out the access counter on creation" do 17 | expect(create(:digital_link, access_counter: nil).access_counter).to eq(0) 18 | end 19 | end 20 | 21 | describe "#update" do 22 | it "does not change the secret when updated" do 23 | digital_link = create(:digital_link) 24 | secret = digital_link.secret 25 | digital_link.increment(:access_counter).save 26 | expect(digital_link.secret).to eq(secret) 27 | end 28 | 29 | it "enforces to have an associated digital" do 30 | link = create(:digital_link) 31 | expect { link.update!(digital: nil) }.to raise_error(ActiveRecord::RecordInvalid) 32 | end 33 | 34 | it "does not allow an empty or too short secret" do 35 | link = create(:digital_link) 36 | expect { link.update!(secret: nil) }.to raise_error(ActiveRecord::RecordInvalid) 37 | expect { link.update!(secret: 'x' * 25) }.to raise_error(ActiveRecord::RecordInvalid) 38 | end 39 | end 40 | 41 | context "authorization" do 42 | let(:link) { create(:digital_link) } 43 | 44 | before { Spree::DigitalConfiguration.reset } 45 | 46 | it "increments the counter using #authorize!" do 47 | expect(link.access_counter).to eq(0) 48 | expect { link.authorize! }.to change(link, :access_counter).by(1) 49 | end 50 | 51 | it "is #authorized? when configuration for access_counter set to nil" do 52 | stub_spree_preferences(Spree::DigitalConfiguration, authorized_clicks: nil) 53 | expect(link.authorizable?).to be true 54 | end 55 | 56 | it "is not #authorized? when the access_counter is too high" do 57 | allow(link).to receive_messages(access_counter: Spree::DigitalConfiguration[:authorized_clicks] - 1) 58 | expect(link.authorizable?).to be true 59 | 60 | allow(link).to receive_messages(access_counter: Spree::DigitalConfiguration[:authorized_clicks]) 61 | expect(link.authorizable?).to be false 62 | end 63 | 64 | it "is not #authorize! when the created_at date is too far in the past" do 65 | expect(link.authorize!).to be true 66 | 67 | allow(link).to receive_messages(created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 - 1).hours.ago) 68 | expect(link.authorize!).to be true 69 | 70 | allow(link).to receive_messages(created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago) 71 | expect(link.authorize!).to be false 72 | end 73 | 74 | it "is not #authorized? when both access_counter and created_at are invalid" do 75 | expect(link.authorizable?).to be true 76 | allow(link).to receive_messages( 77 | access_counter: Spree::DigitalConfiguration[:authorized_clicks], 78 | created_at: (Spree::DigitalConfiguration[:authorized_days] * 24 + 1).hours.ago 79 | ) 80 | expect(link.authorizable?).to be false 81 | end 82 | end 83 | 84 | describe '#reset!' do 85 | it 'resets the access counter' do 86 | link = create(:digital_link) 87 | link.authorize! 88 | expect(link.access_counter).to eq(1) 89 | 90 | link.reset! 91 | expect(link.access_counter).to eq(0) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/controllers/admin/digitals_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Spree::Admin::DigitalsController do 6 | stub_authorization! 7 | 8 | let!(:product) { create(:product) } 9 | 10 | describe '#index' do 11 | render_views 12 | 13 | context "with variants" do 14 | let(:digitals) { 3.times.map { create(:digital) } } 15 | let(:variants_with_digitals) do 16 | digitals.map { |d| create(:variant, product: product, digitals: [d]) } 17 | end 18 | let(:variants_without_digitals) { 3.times.map { create(:variant, product: product) } } 19 | 20 | it "displays an empty page when no digitals exist" do 21 | variants_without_digitals 22 | get :index, params: { product_id: product.slug } 23 | end 24 | 25 | it "displays list of digitals when they exist" do 26 | variants_with_digitals 27 | get :index, params: { product_id: product.slug } 28 | 29 | variants_with_digitals.each do |variant| 30 | variant.digitals.each do |digital| 31 | expect(response.body).to include(digital.attachment_file_name) 32 | end 33 | end 34 | end 35 | end 36 | 37 | context "without non-master variants" do 38 | it "displays an empty page when the master variant is not digital" do 39 | get :index, params: { product_id: product.slug } 40 | expect(response.code).to eq("200") 41 | expect(response.body).to include("This product has no variants.") 42 | expect(response.body).not_to include('A digital version of this product currently exists:') 43 | end 44 | 45 | it "displays the variant details when the master is digital" do 46 | @digital = create :digital, variant: product.master 47 | get :index, params: { product_id: product.slug } 48 | 49 | expect(response.code).to eq("200") 50 | expect(response.body).to include('A digital version of this product currently exists:') 51 | end 52 | end 53 | end 54 | 55 | describe '#create' do 56 | context 'for a product that exists' do 57 | let!(:variant) { create(:variant, product: product) } 58 | 59 | it 'creates a digital associated with the variant and product' do 60 | expect { 61 | post :create, params: { product_id: product.slug, 62 | digital: { variant_id: variant.id, 63 | attachment: upload_image('thinking-cat.jpg') } } 64 | expect(response).to redirect_to(spree.admin_product_digitals_path(product)) 65 | }.to change(Spree::Digital, :count).by(1) 66 | end 67 | end 68 | 69 | context 'for an invalid object' do 70 | it 'redirects to the index page' do 71 | expect { 72 | post :create, params: { product_id: product.slug, digital: { variant_id: product.master.id } } # fail validation by not passing attachment 73 | expect(flash[:error]).to eq "Attachment can't be blank" 74 | expect(response).to redirect_to(spree.admin_product_digitals_path(product)) 75 | }.to change(Spree::Digital, :count).by(0) 76 | end 77 | end 78 | end 79 | 80 | describe '#destroy' do 81 | let(:digital) { create(:digital) } 82 | let!(:variant) { create(:variant, product: product, digitals: [digital]) } 83 | 84 | context 'for a digital and product that exist' do 85 | it 'deletes the associated digital' do 86 | expect { 87 | delete :destroy, params: { product_id: product.slug, id: digital.id } 88 | expect(response).to redirect_to(spree.admin_product_digitals_path(product)) 89 | }.to change(Spree::Digital, :count).by(-1) 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-06-19 16:42:10 UTC using RuboCop version 1.52.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 17 10 | # This cop supports unsafe autocorrection (--autocorrect-all). 11 | # Configuration parameters: Include, EnforcedStyle, ExplicitOnly. 12 | # Include: **/*_spec.rb, **/spec/**/*, spec/factories.rb, spec/factories/**/*.rb, features/support/factories/**/*.rb 13 | # SupportedStyles: create_list, n_times 14 | FactoryBot/CreateList: 15 | Exclude: 16 | - 'spec/controllers/admin/digitals_controller_spec.rb' 17 | - 'spec/models/spree/line_item_spec.rb' 18 | - 'spec/models/spree/order_spec.rb' 19 | - 'spec/models/spree/product_spec.rb' 20 | 21 | # Offense count: 1 22 | # Configuration parameters: Severity, Include. 23 | # Include: **/*.gemspec 24 | Gemspec/RequiredRubyVersion: 25 | Exclude: 26 | - 'solidus_digital.gemspec' 27 | 28 | # Offense count: 1 29 | # This cop supports safe autocorrection (--autocorrect). 30 | Lint/RedundantCopDisableDirective: 31 | Exclude: 32 | - 'lib/generators/solidus_digital/install/install_generator.rb' 33 | 34 | # Offense count: 1 35 | # This cop supports unsafe autocorrection (--autocorrect-all). 36 | # Configuration parameters: EnforcedStyleForLeadingUnderscores. 37 | # SupportedStylesForLeadingUnderscores: disallowed, required, optional 38 | Naming/MemoizedInstanceVariableName: 39 | Exclude: 40 | - 'app/controllers/spree/digitals_controller.rb' 41 | 42 | # Offense count: 2 43 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. 44 | # SupportedStyles: snake_case, normalcase, non_integer 45 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 46 | Naming/VariableNumber: 47 | Exclude: 48 | - 'spec/models/spree/order_spec.rb' 49 | 50 | # Offense count: 11 51 | # This cop supports unsafe autocorrection (--autocorrect-all). 52 | Performance/TimesMap: 53 | Exclude: 54 | - 'spec/controllers/admin/digitals_controller_spec.rb' 55 | - 'spec/models/spree/order_spec.rb' 56 | - 'spec/models/spree/product_spec.rb' 57 | 58 | # Offense count: 13 59 | # Configuration parameters: Prefixes, AllowedPatterns. 60 | # Prefixes: when, with, without 61 | RSpec/ContextWording: 62 | Exclude: 63 | - 'spec/controllers/admin/digitals_controller_spec.rb' 64 | - 'spec/controllers/digitals_controller_spec.rb' 65 | - 'spec/models/spree/digital_link_spec.rb' 66 | - 'spec/models/spree/digital_spec.rb' 67 | - 'spec/models/spree/line_item_spec.rb' 68 | - 'spec/models/spree/order_spec.rb' 69 | - 'spec/models/spree/product_spec.rb' 70 | 71 | # Offense count: 3 72 | RSpec/ExpectInHook: 73 | Exclude: 74 | - 'spec/models/spree/order_spec.rb' 75 | 76 | # Offense count: 4 77 | # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. 78 | # Include: **/*_spec*rb*, **/spec/**/* 79 | RSpec/FilePath: 80 | Exclude: 81 | - 'spec/controllers/admin/digitals_controller_spec.rb' 82 | - 'spec/controllers/admin/orders_controller_spec.rb' 83 | - 'spec/controllers/digitals_controller_spec.rb' 84 | - 'spec/models/calculator/digital_delivery_spec.rb' 85 | 86 | # Offense count: 2 87 | RSpec/IteratedExpectation: 88 | Exclude: 89 | - 'spec/models/spree/order_spec.rb' 90 | 91 | # Offense count: 1 92 | # This cop supports safe autocorrection (--autocorrect). 93 | RSpec/LeadingSubject: 94 | Exclude: 95 | - 'spec/models/spree/order_spec.rb' 96 | 97 | # Offense count: 2 98 | RSpec/LetSetup: 99 | Exclude: 100 | - 'spec/controllers/admin/digitals_controller_spec.rb' 101 | - 'spec/models/spree/product_spec.rb' 102 | 103 | # Offense count: 10 104 | # Configuration parameters: . 105 | # SupportedStyles: have_received, receive 106 | RSpec/MessageSpies: 107 | EnforcedStyle: receive 108 | 109 | # Offense count: 33 110 | RSpec/MultipleExpectations: 111 | Max: 5 112 | 113 | # Offense count: 2 114 | # Configuration parameters: AllowSubject. 115 | RSpec/MultipleMemoizedHelpers: 116 | Max: 6 117 | 118 | # Offense count: 7 119 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. 120 | # SupportedStyles: always, named_only 121 | RSpec/NamedSubject: 122 | Exclude: 123 | - 'spec/models/calculator/digital_delivery_spec.rb' 124 | - 'spec/models/spree/stock/splitter/digital_splitter_spec.rb' 125 | 126 | # Offense count: 3 127 | # Configuration parameters: AllowedGroups. 128 | RSpec/NestedGroups: 129 | Max: 4 130 | 131 | # Offense count: 4 132 | RSpec/StubbedMock: 133 | Exclude: 134 | - 'spec/controllers/digitals_controller_spec.rb' 135 | - 'spec/models/calculator/digital_delivery_spec.rb' 136 | - 'spec/models/spree/order_spec.rb' 137 | 138 | # Offense count: 1 139 | RSpec/SubjectStub: 140 | Exclude: 141 | - 'spec/models/calculator/digital_delivery_spec.rb' 142 | 143 | # Offense count: 3 144 | # Configuration parameters: ForbiddenMethods, AllowedMethods. 145 | # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all 146 | Rails/SkipsModelValidations: 147 | Exclude: 148 | - 'app/models/spree/digital_link.rb' 149 | 150 | # Offense count: 2 151 | # This cop supports unsafe autocorrection (--autocorrect-all). 152 | # Configuration parameters: EnforcedStyle. 153 | # SupportedStyles: nested, compact 154 | Style/ClassAndModuleChildren: 155 | Exclude: 156 | - 'app/models/spree/calculator/shipping/digital_delivery.rb' 157 | - 'app/services/spree/digital_links_creator.rb' 158 | 159 | # Offense count: 1 160 | # This cop supports safe autocorrection (--autocorrect). 161 | # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. 162 | Style/GuardClause: 163 | Exclude: 164 | - 'spec/models/spree/drm_record_spec.rb' 165 | 166 | # Offense count: 1 167 | # This cop supports unsafe autocorrection (--autocorrect-all). 168 | # Configuration parameters: Mode. 169 | Style/StringConcatenation: 170 | Exclude: 171 | - 'spec/support/testing_support.rb' 172 | 173 | # Offense count: 2 174 | # This cop supports safe autocorrection (--autocorrect). 175 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. 176 | # URISchemes: http, https 177 | Layout/LineLength: 178 | Max: 173 179 | -------------------------------------------------------------------------------- /spec/models/spree/order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'spree/testing_support/order_walkthrough' 5 | 6 | RSpec.describe Spree::Order do 7 | context "contents.add" do 8 | let(:order) { create(:order) } 9 | 10 | it "adds digital Variants of quantity 1 to an order" do 11 | variants = 3.times.map { create(:variant, digitals: [create(:digital)]) } 12 | variants.each { |v| order.contents.add(v, 1) } 13 | 14 | expect(order.line_items.first.variant).to eq(variants[0]) 15 | expect(order.line_items.second.variant).to eq(variants[1]) 16 | expect(order.line_items.third.variant).to eq(variants[2]) 17 | end 18 | 19 | it "handles quantity higher than 1 when adding one specific digital Variant" do 20 | digital_variant = create(:variant, digitals: [create(:digital)]) 21 | order.contents.add digital_variant, 3 22 | expect(order.line_items.first.quantity).to eq(3) 23 | 24 | order.contents.add digital_variant, 2 25 | expect(order.line_items.first.quantity).to eq(5) 26 | end 27 | end 28 | 29 | context "line_item analysis" do 30 | let(:order) { create(:order) } 31 | 32 | it "understands that all products are digital" do 33 | 3.times do 34 | order.contents.add create(:variant, digitals: [create(:digital)]), 1 35 | end 36 | expect(order.digital?).to be true 37 | 38 | order.contents.add create(:variant, digitals: [create(:digital)]), 4 39 | expect(order.digital?).to be true 40 | end 41 | 42 | it "understands that not all products are digital" do 43 | 3.times do 44 | order.contents.add create(:variant, digitals: [create(:digital)]), 1 45 | end 46 | order.contents.add create(:variant), 1 # this is the analog product 47 | expect(order.digital?).to be false 48 | 49 | order.contents.add create(:variant, digitals: [create(:digital)]), 4 50 | expect(order.digital?).to be false 51 | end 52 | end 53 | 54 | describe '#digital?/#some_digital?' do 55 | let(:order) { create(:order) } 56 | let(:digital_order) { 57 | variants = 3.times.map { create(:variant, digitals: [create(:digital)]) } 58 | variants.each { |v| order.contents.add(v, 1) } 59 | order 60 | } 61 | 62 | let(:mixed_order) { 63 | variants = 2.times.map { create(:variant, digitals: [create(:digital)]) } 64 | variants << create(:variant) 65 | variants.each { |v| order.contents.add(v, 1) } 66 | order 67 | } 68 | 69 | let(:non_digital_order) { 70 | variants = 3.times.map { create(:variant) } 71 | variants.each { |v| order.contents.add(v, 1) } 72 | order 73 | } 74 | 75 | it 'returns true/true for a digital order' do 76 | expect(digital_order).to be_digital 77 | expect(digital_order).to be_some_digital 78 | end 79 | 80 | it 'returns false/true for a mixed order' do 81 | expect(mixed_order).not_to be_digital 82 | expect(mixed_order).to be_some_digital 83 | end 84 | 85 | it 'returns false/false for an exclusively non-digital order' do 86 | expect(non_digital_order).not_to be_digital 87 | expect(non_digital_order).not_to be_some_digital 88 | end 89 | end 90 | 91 | describe '#digital_line_items' do 92 | let(:order) { create(:order) } 93 | let(:digital_order_digitals) { 3.times.map { create(:digital) } } 94 | let(:digital_order) { 95 | variants = digital_order_digitals.map { |d| create(:variant, digitals: [d]) } 96 | variants.each { |v| order.contents.add(v, 1) } 97 | order 98 | } 99 | 100 | let(:mixed_order_digitals) { 2.times.map { create(:digital) } } 101 | let(:mixed_order) { 102 | variants = mixed_order_digitals.map { |d| create(:variant, digitals: [d]) } 103 | variants << create(:variant) 104 | variants.each { |v| order.contents.add(v, 1) } 105 | order 106 | } 107 | 108 | let(:non_digital_order) { 109 | variants = 3.times.map { create(:variant) } 110 | variants.each { |v| order.contents.add(v, 1) } 111 | order 112 | } 113 | 114 | it 'returns true/true for a digital order' do 115 | digital_order_digital_line_items = digital_order.digital_line_items 116 | expect(digital_order_digital_line_items.size).to eq(digital_order_digitals.size) 117 | 118 | variants = digital_order_digital_line_items.map(&:variant) 119 | variants.each { |variant| expect(variant).to be_digital } 120 | digital_order_digitals.each { |d| expect(variants).to include(d.variant) } 121 | end 122 | 123 | it 'returns false/true for a mixed order' do 124 | mixed_order_digital_line_items = mixed_order.digital_line_items 125 | expect(mixed_order_digital_line_items.size).to eq(mixed_order_digitals.size) 126 | 127 | variants = mixed_order_digital_line_items.map(&:variant) 128 | variants.each { |variant| expect(variant).to be_digital } 129 | mixed_order_digitals.each { |d| expect(variants).to include(d.variant) } 130 | end 131 | 132 | it 'returns an empty set for an exclusively non-digital order' do 133 | expect(non_digital_order.digital_line_items).to be_empty 134 | end 135 | end 136 | 137 | describe '#digital_links' do 138 | let(:mixed_order_digitals) { 2.times.map { create(:digital) } } 139 | let(:mixed_order) { 140 | order = create(:order) 141 | variants = mixed_order_digitals.map { |d| create(:variant, digitals: [d]) } 142 | variants << create(:variant) 143 | variants.each { |v| order.contents.add(v, 1) } 144 | order 145 | } 146 | 147 | it 'correctly loads the links' do 148 | mixed_order_digital_links = mixed_order.digital_links 149 | links_from_digitals = mixed_order_digitals.map(&:reload).map(&:digital_links).flatten 150 | expect(mixed_order_digital_links.size).to eq(links_from_digitals.size) 151 | 152 | mixed_order_digital_links.each do |l| 153 | expect(links_from_digitals).to include(l) 154 | end 155 | end 156 | end 157 | 158 | describe '#generate_digital_links' do 159 | let(:digital_variant) { create(:variant, digitals: [create(:digital)]) } 160 | 161 | subject(:links) { order.digital_links } 162 | 163 | context "when order in complete state" do 164 | let(:order) { 165 | create( 166 | :order_ready_to_complete, 167 | line_items_attributes: [{ variant: digital_variant, quantity: 8 }] 168 | ) 169 | } 170 | 171 | it "creates a link for each quantity of a digital Variant" do 172 | order.complete! 173 | expect(links.count).to eq(8) 174 | end 175 | end 176 | 177 | context "when order is in other state" do 178 | let(:order) { 179 | create( 180 | :order_with_line_items, 181 | line_items_attributes: [{ variant: digital_variant, quantity: 8 }] 182 | ) 183 | } 184 | 185 | it "doesn't create a link for digital Variant" do 186 | expect(links.count).to eq(0) 187 | end 188 | end 189 | end 190 | 191 | describe '#reset_digital_links!' do 192 | let!(:order) { build(:order) } 193 | let!(:link_1) { double } 194 | let!(:link_2) { double } 195 | 196 | before do 197 | expect(link_1).to receive(:reset!) 198 | expect(link_2).to receive(:reset!) 199 | expect(order).to receive(:digital_links).and_return([link_1, link_2]) 200 | end 201 | 202 | it 'calls reset on the links' do 203 | order.reset_digital_links! 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidus_digital 2 | 3 | [![CircleCI](https://circleci.com/gh/solidusio-contrib/solidus_digital.svg?style=svg)](https://circleci.com/gh/solidusio-contrib/solidus_digital) 4 | 5 | This is a [Solidus](http://solidus.io/) extension to enable downloadable products (ebooks, MP3s, videos, etc). 6 | 7 | This documentation is not complete and possibly out of date in some cases. 8 | There are features that have been implemented that are not documented here, please look at the source for complete documentation. 9 | 10 | ### Digital products 11 | The idea is simple. 12 | You attach a file to a Product (or a Variant of this Product) and when people buy it, they will receive a link via email where they can download it once. 13 | There are a few assumptions that solidus_digital (currently) makes and it's important to be aware of them. 14 | 15 | * The table structure of spree_core is not touched. 16 | Spree digital lives parallel to spree_core and does change the existing database, except adding two new tables. 17 | * The download links will be sent via email in the order confirmation (or "resend" from the admin section). 18 | The links do *not* appear in the order "overview" that the customer sees. 19 | Adding download buttons to `OrdersController#show` is easy, [check out this gist](https://gist.github.com/3187793#file_add_solidus_digital_buttons_to_invoice.rb). 20 | * Once the order is checked-out, the download links will immediately be sent (i.e. in the order confirmation). 21 | You'll have to modify the system to support 'delayed' payments (like a billable account). 22 | * You should create a ShippingMethod based on the Digital Delivery calculator type. 23 | The default cost for digital delivery is 0, but you can define a flat rate (creating a per-item digital delivery fee would be possible as well). 24 | Checkout the [source code](https://github.com/halo/solidus_digital/blob/master/app/models/spree/calculator/digital_delivery.rb) for the Digital Delivery calculator for more information. 25 | * One may buy several items of the same digital product in one cart. 26 | The customer will simply receive several links by doing so. 27 | This allows customer's to legally purchase multiple copies of the same product and maybe give one away to a friend. 28 | * You can set how many times (clicks) the users downloads will work. 29 | You can also set how long the users links will work (expiration). 30 | For more information, [check out the preferences object](https://github.com/halo/solidus_digital/blob/master/lib/spree/solidus_digital_configuration.rb) 31 | * The file `views/order_mailer/confirm_email.text.erb` needs to be customized by you. 32 | If you are looking for HTML emails, [this branch of spree-html-email](http://github.com/iloveitaly/spree-html-email) supports solidus_digital. 33 | * A purchased product can be downloaded even if you disable the product immediately. 34 | You would have to remove the attached file in your admin section to prevent people from downloading purchased products. 35 | * File are uploaded to `RAILS_ROOT/private`. 36 | Make sure it's symlinked in case you're using Capistrano. 37 | If you want to change the upload path, [check out this gist](https://gist.github.com/3187793#file_solidus_digital_path_change_decorator.rb). 38 | * You must add a `views/spree/digitals/unauthorized.html.erb` file to customize an error message to the user if they exceed the download / days limit 39 | * We use send_file to send the files on download. 40 | See below for instructions on how to push file downloading off to nginx. 41 | 42 | ## Issues 43 | 44 | Current version of `solidus_digital` is not compatable with `solidus` version 2.3.0. 45 | 46 | ## Quickstart 47 | 48 | Add this line to the `Gemfile` in your Spree project: 49 | 50 | ```ruby 51 | gem 'solidus_digital', github: 'solidusio-contrib/solidus_digital' 52 | ``` 53 | 54 | The following terminal commands will copy the migration files to the corresponding directory in your Rails application and apply the migrations to your database. 55 | 56 | ```shell 57 | bundle exec rails g solidus_digital:install 58 | bundle exec rake db:migrate 59 | ``` 60 | 61 | Then set any preferences in the web interface. 62 | 63 | ### Shipping Configuration 64 | 65 | You should create a ShippingMethod based on the Digital Delivery calculator type. 66 | It will be detected by `solidus_digital`. 67 | Otherwise your customer will be forced to choose something like "UPS" even if they purchase only downloadable products. 68 | 69 | ### Links access configuration 70 | 71 | Configuration class `Spree::DigitalConfiguration`. 72 | Default configuration: 73 | 74 | ```ruby 75 | class SpreeDigitalConfiguration < Preferences::Configuration 76 | # number of times a customer can download a digital file 77 | # nil - infinite number of clicks 78 | preference :authorized_clicks, :integer, default: 3 79 | 80 | # number of days after initial purchase the customer can download a file 81 | preference :authorized_days, :integer, default: 2 82 | 83 | # should digitals be kept around after the associated product is destroyed 84 | preference :keep_digitals, :boolean, default: false 85 | 86 | # number of seconds before an s3 link expires 87 | preference :s3_expiration_seconds, :integer, default: 10 88 | 89 | # number of digital links generated per line item 90 | # accepts: 'quantity' or Integer numbers 91 | # quantity - 'line_item.quantity' digital links will be generated 92 | # Integer 'number' - 'number' digital links will be generated 93 | preference :digital_links_count, :string, default: 'quantity' 94 | end 95 | 96 | ``` 97 | 98 | Example: 99 | ```ruby 100 | Rails.application.config.after_initialize do 101 | Spree::DigitalConfiguration[:authorized_clicks] = nil # infinite access for user 102 | end 103 | ``` 104 | 105 | ### DRM 106 | 107 | If you want to create attachment with [DRM](https://en.wikipedia.org/wiki/Digital_rights_management) for your digital product, e.g.: _watermark_ or _digital signature_, 108 | you'll need to implement a class which will transform original attachement from `Spree::Digital` class, to modified attachment. This attachment will be stored as `Spree::DrmRecord` which is assigned to `Spree::Digital` class. And check "DRM" checkbox while creating Digital for product. 109 | 110 | For example: 111 | ```ruby 112 | class SampleDrmMaker 113 | def initialize(drm_record) 114 | @drm_record = drm_record 115 | end 116 | 117 | def create! 118 | # DRM file attachment specific code 119 | @drm_record.attachment = drm_attachemnt 120 | end 121 | end 122 | ``` 123 | 124 | Then insert it into `DrmClass`: 125 | ```ruby 126 | 127 | Spree::DrmRecord.class_eval do 128 | private 129 | def prepare_drm_mark 130 | SampleDrmMaker.new(self).create! 131 | end 132 | end 133 | ``` 134 | 135 | `prepare_drm_mark` method will call **after_create** for `Spree::DrmRecord`. We'd suggest to run your drm maker class in parallel with [Delayed::Job](https://github.com/collectiveidea/delayed_job) or [Sidekiq](https://github.com/mperham/sidekiq). 136 | 137 | Every time user confirms order on checkout process, new `Spree::DrmRecord` will be created for every `LineItem` which has digital product with enabled `DRM` flag. 138 | 139 | 140 | ### Improving File Downloading: `send_file` + nginx 141 | 142 | Without customization, all file downloading will route through the rails stack. 143 | This means that if you have two workers, and two customers are downloading files, your server is maxed out and will be unresponsive until the downloads have finished. 144 | 145 | Luckily there is an easy way around this: 146 | pass off file downloading to nginx (or apache, etc). 147 | Take a look at [this article](http://blog.kiskolabs.com/post/637725747/nginx-rails-send-file) for a good explanation. 148 | 149 | ```ruby 150 | # in your app's source 151 | # config/environments/production.rb 152 | 153 | # Specifies the header that your server uses for sending files 154 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 155 | config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 156 | ``` 157 | 158 | ```nginx 159 | # on your server 160 | # e.g. /etc/nginx/sites-available/spree-secure 161 | upstream unicorn_spree_secure { 162 | server unix:/data/spree/shared/sockets/unicorn.sock fail_timeout=0; 163 | } 164 | server { 165 | listen 443; 166 | ... 167 | 168 | location / { 169 | proxy_set_header X_FORWARDED_PROTO https; 170 | ... 171 | proxy_set_header X-Sendfile-Type X-Accel-Redirect; 172 | proxy_set_header X-Accel-Mapping /data/spree/shared/uploaded-files/digitals/=/digitals/; 173 | ... 174 | } 175 | 176 | location /digitals/ { 177 | internal; 178 | root /data/spree/shared/uploaded-files/; 179 | } 180 | ... 181 | } 182 | ``` 183 | 184 | References: 185 | 186 | * [Gist of example config](https://gist.github.com/416004) 187 | * [Change paperclip's upload / download path](https://gist.github.com/3187793#file_solidus_digital_path_change_decorator.rb) 188 | * ["X-Accel-Mapping header missing" in nginx error log](http://stackoverflow.com/questions/6237016/message-x-accel-mapping-header-missing-in-nginx-error-log) 189 | * [Another good, but older, explanation](http://kovyrin.net/2006/11/01/nginx-x-accel-redirect-php-rails/) 190 | 191 | ### Development 192 | 193 | #### Table Diagram 194 | 195 | 196 | 197 | #### Testing 198 | 199 | ```shell 200 | rake test_app 201 | rake rspec 202 | ``` 203 | 204 | ### Contributors 205 | 206 | See https://github.com/solidusio-contrib/solidus_digital/graphs/contributors 207 | 208 | ### License 209 | 210 | MIT © 2011-2015 halo, see [LICENSE](https://github.com/solidusio-contrib/solidus_digital/blob/master/LICENSE.md) 211 | --------------------------------------------------------------------------------