solidus_virtual_gift_card 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +26 -0
  6. data/README.md +52 -0
  7. data/Rakefile +21 -0
  8. data/app/assets/javascripts/spree/backend/solidus_virtual_gift_card.coffee +9 -0
  9. data/app/assets/stylesheets/spree/backend/solidus_virtual_gift_card.css +7 -0
  10. data/app/controllers/spree/admin/gift_cards_controller.rb +54 -0
  11. data/app/controllers/spree/api/gift_cards_controller.rb +23 -0
  12. data/app/mailers/spree/gift_card_mailer.rb +8 -0
  13. data/app/models/concerns/spree/gift_cards/line_item_concerns.rb +17 -0
  14. data/app/models/concerns/spree/gift_cards/order_concerns.rb +34 -0
  15. data/app/models/concerns/spree/redemption_code_generator.rb +10 -0
  16. data/app/models/spree/line_item_decorator.rb +1 -0
  17. data/app/models/spree/order_decorator.rb +1 -0
  18. data/app/models/spree/permission_sets/virtual_gift_card_display.rb +9 -0
  19. data/app/models/spree/permission_sets/virtual_gift_card_management.rb +9 -0
  20. data/app/models/spree/store_credit_category_decorator.rb +2 -0
  21. data/app/models/spree/virtual_gift_card.rb +75 -0
  22. data/app/overrides/admin_item_view_gift_card_codes.rb +13 -0
  23. data/app/overrides/admin_user_sidebar_store_credits.rb +6 -0
  24. data/app/overrides/admin_user_sub_menu.rb +6 -0
  25. data/app/views/spree/admin/gift_cards/_lookup_form.html.erb +8 -0
  26. data/app/views/spree/admin/gift_cards/_redemption_form.html.erb +8 -0
  27. data/app/views/spree/admin/gift_cards/index.html.erb +1 -0
  28. data/app/views/spree/admin/gift_cards/lookup.html.erb +16 -0
  29. data/app/views/spree/admin/gift_cards/show.html.erb +32 -0
  30. data/app/views/spree/admin/orders/_gift_card_redemption_codes.html.erb +3 -0
  31. data/app/views/spree/admin/users/_gift_card_sidebar.html.erb +3 -0
  32. data/app/views/spree/admin/users/_sub_menu.html.erb +10 -0
  33. data/app/views/spree/gift_card_mailer/gift_card_email.text.erb +7 -0
  34. data/bin/rails +7 -0
  35. data/circle.yml +6 -0
  36. data/config/initializers/gift_card_store_credit_event_originator.rb +7 -0
  37. data/config/locales/en.yml +21 -0
  38. data/config/routes.rb +23 -0
  39. data/db/migrate/20140623152903_create_virtual_gift_card.rb +16 -0
  40. data/db/migrate/20140624175113_seed_gift_card_category.rb +5 -0
  41. data/db/migrate/20140627185148_add_timestamps_to_gift_cards.rb +6 -0
  42. data/db/migrate/20140702153907_add_gift_card_flag_to_products.rb +5 -0
  43. data/db/migrate/20140707200431_add_line_item_to_gift_card.rb +7 -0
  44. data/db/seeds.rb +1 -0
  45. data/lib/generators/solidus_virtual_gift_card/install/install_generator.rb +36 -0
  46. data/lib/solidus_virtual_gift_card.rb +2 -0
  47. data/lib/spree_virtual_gift_card/engine.rb +20 -0
  48. data/lib/spree_virtual_gift_card/factories.rb +13 -0
  49. data/solidus_virtual_gift_card.gemspec +31 -0
  50. data/spec/controllers/spree/admin/gift_cards_controller_spec.rb +111 -0
  51. data/spec/controllers/spree/api/gift_cards_controller_spec.rb +88 -0
  52. data/spec/models/concerns/spree/redemption_code_generator_spec.rb +42 -0
  53. data/spec/models/spree/line_item_spec.rb +30 -0
  54. data/spec/models/spree/order_spec.rb +90 -0
  55. data/spec/models/spree/permission_sets/virtual_gift_card_display_spec.rb +22 -0
  56. data/spec/models/spree/permission_sets/virtual_gift_card_management_spec.rb +21 -0
  57. data/spec/models/spree/store_credit_category_spec.rb +23 -0
  58. data/spec/models/spree/virtual_gift_card_spec.rb +179 -0
  59. data/spec/spec_helper.rb +74 -0
  60. metadata +258 -0
@@ -0,0 +1,6 @@
1
+ class AddTimestampsToGiftCards < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_virtual_gift_cards, :created_at, :datetime
4
+ add_column :spree_virtual_gift_cards, :updated_at, :datetime
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddGiftCardFlagToProducts < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_products, :gift_card, :boolean, default: false
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class AddLineItemToGiftCard < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_virtual_gift_cards, :line_item_id, :integer
4
+
5
+ add_index :spree_virtual_gift_cards, :line_item_id
6
+ end
7
+ end
data/db/seeds.rb ADDED
@@ -0,0 +1 @@
1
+ Spree::StoreCreditCategory.find_or_create_by(name: 'Gift Card')
@@ -0,0 +1,36 @@
1
+ module SolidusVirtualGiftCard
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ class_option :auto_run_migrations, type: :boolean, default: false
6
+
7
+ def add_javascripts
8
+ append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_virtual_gift_card\n"
9
+ end
10
+
11
+ def add_stylesheets
12
+ inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/solidus_virtual_gift_card\n", before: /\*\//, verbose: true
13
+ end
14
+
15
+ def include_seed_data
16
+ append_file "db/seeds.rb", <<-SEEDS
17
+ \n
18
+ SpreeVirtualGiftCard::Engine.load_seed if defined?(SpreeVirtualGiftCard::Engine)
19
+ SEEDS
20
+ end
21
+
22
+ def add_migrations
23
+ run 'bundle exec rake railties:install:migrations FROM=solidus_virtual_gift_card'
24
+ end
25
+
26
+ def run_migrations
27
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
28
+ if run_migrations
29
+ run 'bundle exec rake db:migrate'
30
+ else
31
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,2 @@
1
+ require 'spree_core'
2
+ require 'spree_virtual_gift_card/engine'
@@ -0,0 +1,20 @@
1
+ module SpreeVirtualGiftCard
2
+ class Engine < Rails::Engine
3
+ require "solidus_core"
4
+
5
+ isolate_namespace Spree
6
+ engine_name "solidus_virtual_gift_card"
7
+
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ def self.activate
13
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator*.rb")) do |c|
14
+ Rails.configuration.cache_classes ? require(c) : load(c)
15
+ end
16
+ end
17
+
18
+ config.to_prepare &method(:activate).to_proc
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ FactoryGirl.define do
2
+ factory :store_credit_gift_card_category, class: Spree::StoreCreditCategory do
3
+ name Spree::StoreCreditCategory::GIFT_CARD_CATEGORY_NAME
4
+ end
5
+
6
+ factory :virtual_gift_card, class: Spree::VirtualGiftCard do
7
+ association :purchaser, factory: :user
8
+ association :line_item, factory: :line_item
9
+
10
+ amount 25.0
11
+ currency "USD"
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.platform = Gem::Platform::RUBY
5
+ s.name = "solidus_virtual_gift_card"
6
+ s.version = "1.0.0"
7
+ s.summary = "Virtual gift card for purchase, drops into the user's account as store credit"
8
+ s.required_ruby_version = ">= 2.1"
9
+
10
+ s.author = "Bonobos"
11
+ s.email = "engineering@bonobos.com"
12
+ s.homepage = "http://www.bonobos.com"
13
+ s.license = %q{BSD-3}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.require_path = "lib"
18
+ s.requirements << "none"
19
+
20
+ s.add_dependency "solidus", [">= 1.0.0", "< 1.2.0"]
21
+
22
+ s.add_development_dependency "rspec-rails", "~> 3.2"
23
+ s.add_development_dependency "simplecov"
24
+ s.add_development_dependency "sqlite3"
25
+ s.add_development_dependency "sass-rails"
26
+ s.add_development_dependency "coffee-rails"
27
+ s.add_development_dependency "factory_girl"
28
+ s.add_development_dependency "capybara"
29
+ s.add_development_dependency "database_cleaner"
30
+ s.add_development_dependency "ffaker"
31
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::Admin::GiftCardsController do
4
+ stub_authorization!
5
+ let!(:gc_category) { create(:store_credit_gift_card_category) }
6
+ let!(:credit_type) { create(:secondary_credit_type, name: "Non-expiring") }
7
+
8
+ describe 'GET index' do
9
+ subject { spree_get :index }
10
+
11
+ it "returns a 200 status code" do
12
+ subject
13
+ expect(response.code).to eq "200"
14
+ end
15
+ end
16
+
17
+ describe 'GET show' do
18
+ let(:gift_card) { create(:virtual_gift_card) }
19
+ let(:redemption_code) { gift_card.redemption_code }
20
+
21
+ subject { spree_get :show, id: redemption_code }
22
+
23
+ context 'with a valid redemption code' do
24
+ it 'loads the gift cards' do
25
+ subject
26
+ expect(assigns(:gift_cards)).to eq [gift_card]
27
+ end
28
+
29
+ it 'returns a 200 status code' do
30
+ subject
31
+ expect(response.code).to eq '200'
32
+ end
33
+ end
34
+
35
+ context 'with an invalid redemption code' do
36
+ let(:redemption_code) { "DOES-NOT-EXIST" }
37
+
38
+ it "redirects to index" do
39
+ expect(subject).to redirect_to spree.admin_gift_cards_path
40
+ end
41
+
42
+ it "sets the flash error" do
43
+ subject
44
+ expect(flash[:error]).to eq Spree.t('admin.gift_cards.errors.not_found')
45
+ end
46
+ end
47
+ end
48
+
49
+ describe 'GET lookup' do
50
+ let(:user) { create :user }
51
+ subject { spree_get :lookup, user_id: user.id }
52
+
53
+ it "returns a 200 status code" do
54
+ subject
55
+ expect(response.code).to eq "200"
56
+ end
57
+ end
58
+
59
+ describe 'POST redeem' do
60
+ let(:user) { create :user }
61
+ let(:gift_card) { create(:virtual_gift_card) }
62
+ let(:redemption_code) { gift_card.redemption_code }
63
+
64
+ subject { spree_post :redeem, user_id: user.id, gift_card: { redemption_code: redemption_code } }
65
+
66
+ context "with a gift card that has not yet been redeemed" do
67
+
68
+ it "redirects to store credit index" do
69
+ expect(subject).to redirect_to spree.admin_user_store_credits_path(user)
70
+ end
71
+
72
+ it "redeems the gift card" do
73
+ subject
74
+ expect(gift_card.reload.redeemed?).to eq true
75
+ end
76
+
77
+ it "sets the redeemer to the correct user" do
78
+ subject
79
+ expect(gift_card.reload.redeemer).to eq user
80
+ end
81
+
82
+ it "creates store credit for the user" do
83
+ subject
84
+ expect(user.reload.store_credits.count).to eq 1
85
+ end
86
+
87
+ it "sets the store credit equal to the amount of the gift card" do
88
+ subject
89
+ expect(user.reload.store_credits.first.amount).to eq gift_card.amount
90
+ end
91
+ end
92
+
93
+ context "with a gift card that has already been redeemed" do
94
+ before(:each) { gift_card.update_attribute(:redeemed_at, Date.yesterday) }
95
+
96
+ it "renders the lookup page" do
97
+ subject
98
+ expect(response).to render_template(:lookup)
99
+ end
100
+ end
101
+
102
+ context "with a gift card code that does not exist" do
103
+ let(:redemption_code) { "INVALID-REDEMPTION-CODE" }
104
+
105
+ it "renders the lookup page" do
106
+ subject
107
+ expect(response).to render_template(:lookup)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::Api::GiftCardsController do
4
+ render_views
5
+ let!(:credit_type) { create(:secondary_credit_type, name: "Non-expiring") }
6
+ let!(:gc_category) { create(:store_credit_gift_card_category) }
7
+
8
+ describe "POST redeem" do
9
+ let(:gift_card) { create(:virtual_gift_card) }
10
+
11
+ let(:parameters) do
12
+ {
13
+ redemption_code: gift_card.redemption_code
14
+ }
15
+ end
16
+
17
+ subject { spree_post :redeem, parameters, { format: :json } }
18
+
19
+ context "the user is not logged in" do
20
+
21
+ before { subject }
22
+
23
+ it "returns a 401" do
24
+ expect(response.status).to eq 401
25
+ end
26
+ end
27
+
28
+ context "the current api user is authenticated" do
29
+ let(:api_user) { create(:user) }
30
+
31
+ before do
32
+ allow(controller).to receive(:load_user)
33
+ controller.instance_variable_set(:@current_api_user, api_user)
34
+ end
35
+
36
+ let(:parsed_response) { HashWithIndifferentAccess.new(JSON.parse(response.body)) }
37
+
38
+ context "given an invalid gift card redemption code" do
39
+ before { subject }
40
+
41
+ let(:parameters) do
42
+ {
43
+ redemption_code: 'INVALID_CODE'
44
+ }
45
+ end
46
+
47
+ it 'does not find the gift card' do
48
+ expect(assigns(:gift_card)).to eq nil
49
+ end
50
+
51
+ it 'contains an error message' do
52
+ expect(parsed_response['error_message']).to be_present
53
+ end
54
+
55
+ it "returns a 404" do
56
+ expect(subject.status).to eq 404
57
+ end
58
+ end
59
+
60
+ context "there is no redemption code in the request body" do
61
+ let(:parameters) { {} }
62
+
63
+ it "returns a 404" do
64
+ expect(subject.status).to eq 404
65
+ end
66
+ end
67
+
68
+ context "given a valid gift card redemption code" do
69
+
70
+ it 'finds the gift card' do
71
+ subject
72
+ expect(assigns(:gift_card)).to eq gift_card
73
+ end
74
+
75
+ it 'redeems the gift card' do
76
+ allow(Spree::VirtualGiftCard).to receive(:active_by_redemption_code).and_return(gift_card)
77
+ expect(gift_card).to receive(:redeem).with(api_user)
78
+ subject
79
+ end
80
+
81
+ it "returns a 201" do
82
+ subject
83
+ expect(subject.status).to eq 201
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::RedemptionCodeGenerator do
4
+ describe '#generate_redemption_code' do
5
+ subject { Spree::RedemptionCodeGenerator.generate_redemption_code }
6
+
7
+ it 'generates a 16 character alpha-numeric code' do
8
+ code = subject
9
+ expect(code.match('^[a-zA-Z0-9]{16}')[0]).to eq code
10
+ end
11
+ end
12
+
13
+ describe '#format_redemption_code_for_lookup' do
14
+ subject { Spree::RedemptionCodeGenerator.format_redemption_code_for_lookup(redemption_code) }
15
+
16
+ context 'redemption code has no dashes' do
17
+ let(:redemption_code) { "1234ABCD1234ABCD" }
18
+
19
+ it 'does nothing to the code' do
20
+ expect(subject).to eq redemption_code
21
+ end
22
+ end
23
+
24
+ context 'redemption code 4 groups of 4 characters, separated by dashes' do
25
+ let(:redemption_code) { "1234-ABCD-1234-ABCD" }
26
+ let(:formatted_redemption_code) { "1234ABCD1234ABCD" }
27
+
28
+ it 'strips the dashes' do
29
+ expect(subject).to eq formatted_redemption_code
30
+ end
31
+ end
32
+
33
+ context 'redemption code is mixed-case' do
34
+ let(:redemption_code) { "1234-aBCd-1234-AbcD" }
35
+ let(:formatted_redemption_code) { "1234ABCD1234ABCD" }
36
+
37
+ it 'makes it all upcase' do
38
+ expect(subject).to eq formatted_redemption_code
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::LineItem do
4
+ describe "#redemption_codes" do
5
+ let(:line_item) { create(:line_item, quantity: 2) }
6
+ let!(:gift_card) { create(:virtual_gift_card, line_item: line_item) }
7
+ let!(:gift_card_2) { create(:virtual_gift_card, line_item: line_item) }
8
+ let(:subject) { line_item.redemption_codes }
9
+
10
+ it 'has the correct number of redemption codes and keys' do
11
+ expect(subject.size).to eq line_item.quantity
12
+ end
13
+
14
+ it 'contains a redemption_code' do
15
+ expect(subject.first).to have_key :redemption_code
16
+ end
17
+
18
+ it 'formats the redemption_code' do
19
+ expect(subject.first).to have_value gift_card.formatted_redemption_code
20
+ end
21
+
22
+ it 'contains an amount' do
23
+ expect(subject.first).to have_key :amount
24
+ end
25
+
26
+ it 'formats the amount' do
27
+ expect(subject.first).to have_value gift_card.formatted_amount
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::Order do
4
+ describe "#create_gift_cards" do
5
+ let(:order) { create(:order_with_line_items) }
6
+ let(:line_item) { order.line_items.first }
7
+ subject { order.create_gift_cards }
8
+
9
+ context "the line item is a gift card" do
10
+ before do
11
+ allow(line_item).to receive(:gift_card?).and_return(true)
12
+ allow(line_item).to receive(:quantity).and_return(3)
13
+ end
14
+
15
+ it 'creates a gift card for each gift card in the line item' do
16
+ expect { subject }.to change { Spree::VirtualGiftCard.count }.by(line_item.quantity)
17
+ end
18
+
19
+ it 'sets the purchaser, amount, and currency' do
20
+ expect(Spree::VirtualGiftCard).to receive(:create!).exactly(3).times.with(amount: line_item.price, currency: line_item.currency, purchaser: order.user, line_item: line_item)
21
+ subject
22
+ end
23
+ end
24
+
25
+ context "the line item is not a gift card" do
26
+ before { allow(line_item).to receive(:gift_card?).and_return(false) }
27
+
28
+ it 'does not create a gift card' do
29
+ expect(Spree::VirtualGiftCard).not_to receive(:create!)
30
+ subject
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "#finalize!" do
36
+ context "the order contains gift cards and transitions to complete" do
37
+ let(:order) { create(:order_with_line_items, state: 'complete') }
38
+
39
+ subject { order.finalize! }
40
+
41
+ it "calls #create_gift_cards" do
42
+ expect(order).to receive(:create_gift_cards)
43
+ subject
44
+ end
45
+ end
46
+ end
47
+
48
+ describe "#send_gift_card_emails" do
49
+
50
+ subject { order.send_gift_card_emails }
51
+
52
+ context "the order has gift cards" do
53
+ let(:gift_card) { create(:virtual_gift_card) }
54
+ let(:line_item) { gift_card.line_item }
55
+ let(:gift_card_2) { create(:virtual_gift_card, line_item: line_item) }
56
+ let(:order) { gift_card.line_item.order }
57
+
58
+ it "should call GiftCardMailer#send" do
59
+ expect(Spree::GiftCardMailer).to receive(:gift_card_email).with(gift_card).and_return(double(deliver: true))
60
+ expect(Spree::GiftCardMailer).to receive(:gift_card_email).with(gift_card_2).and_return(double(deliver: true))
61
+ subject
62
+ end
63
+ end
64
+
65
+ context "no gift cards" do
66
+ let(:order) { create(:order_with_line_items) }
67
+
68
+ it "should not call GiftCardMailer#send" do
69
+ expect(Spree::GiftCardMailer).to_not receive(:gift_card_email)
70
+ subject
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "#display_total_applicable_store_credit" do
76
+ let(:total_applicable_store_credit) { 10.00 }
77
+
78
+ subject { create(:order) }
79
+
80
+ before { allow(subject).to receive_messages(total_applicable_store_credit: total_applicable_store_credit) }
81
+
82
+ it "returns a money instance" do
83
+ expect(subject.display_total_applicable_store_credit).to be_a(Spree::Money)
84
+ end
85
+
86
+ it "returns a negative amount" do
87
+ expect(subject.display_total_applicable_store_credit.money.cents).to eq (total_applicable_store_credit * -100.0)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::PermissionSets::VirtualGiftCardDisplay do
4
+ let(:ability) { Spree::Ability.new nil }
5
+
6
+ subject { ability }
7
+
8
+ context "when activated" do
9
+ before do
10
+ described_class.new(ability).activate!
11
+ end
12
+
13
+ it { should be_able_to(:admin, Spree::VirtualGiftCard) }
14
+ it { should be_able_to(:display, Spree::VirtualGiftCard) }
15
+ end
16
+
17
+ context "when not activated" do
18
+ it { should_not be_able_to(:admin, Spree::VirtualGiftCard) }
19
+ it { should_not be_able_to(:display, Spree::VirtualGiftCard) }
20
+ end
21
+ end
22
+
@@ -0,0 +1,21 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe Spree::PermissionSets::VirtualGiftCardManagement do
5
+ let(:ability) { Spree::Ability.new nil }
6
+
7
+ subject { ability }
8
+
9
+ context "when activated" do
10
+ before do
11
+ described_class.new(ability).activate!
12
+ end
13
+
14
+ it { should be_able_to(:manage, Spree::VirtualGiftCard) }
15
+ end
16
+
17
+ context "when not activated" do
18
+ it { should_not be_able_to(:manage, Spree::VirtualGiftCard) }
19
+ end
20
+ end
21
+
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::StoreCreditCategory, type: :model do
4
+ describe "#non_expiring?" do
5
+ subject { build(:store_credit_category, name: category_name).non_expiring? }
6
+
7
+ context "non-expiring type store credit" do
8
+ let(:category_name) { Spree::StoreCreditCategory.non_expiring_credit_types.first }
9
+
10
+ it "returns true" do
11
+ expect(subject).to be true
12
+ end
13
+ end
14
+
15
+ context "expiring type store credit" do
16
+ let(:category_name) { "Expiring" }
17
+
18
+ it "returns false" do
19
+ expect(subject).to be false
20
+ end
21
+ end
22
+ end
23
+ end