mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-11 03:40:20 +00:00
Merge pull request #12949 from rioug/12859-use-VINE-voucher
[City OFN Voucher] A shopper can use a VINE voucher
This commit is contained in:
@@ -34,12 +34,6 @@ Lint/EmptyClass:
|
||||
Exclude:
|
||||
- 'spec/lib/reports/report_loader_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowComments.
|
||||
Lint/EmptyFile:
|
||||
Exclude:
|
||||
- 'spec/lib/open_food_network/enterprise_injection_data_spec.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Lint/FloatComparison:
|
||||
Exclude:
|
||||
@@ -92,7 +86,6 @@ Metrics/AbcSize:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/admin/variants_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
@@ -127,7 +120,7 @@ Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- 'app/models/spree/payment/processing.rb'
|
||||
|
||||
# Offense count: 46
|
||||
# Offense count: 47
|
||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
@@ -137,6 +130,7 @@ Metrics/ClassLength:
|
||||
- 'app/controllers/admin/resource_controller.rb'
|
||||
- 'app/controllers/admin/subscriptions_controller.rb'
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/controllers/checkout_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/orders_controller.rb'
|
||||
- 'app/controllers/spree/admin/payment_methods_controller.rb'
|
||||
@@ -183,7 +177,7 @@ Metrics/ClassLength:
|
||||
Metrics/CyclomaticComplexity:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/checkout_helper.rb'
|
||||
- 'app/helpers/order_cycles_helper.rb'
|
||||
@@ -208,13 +202,12 @@ Metrics/CyclomaticComplexity:
|
||||
- 'lib/spree/localized_number.rb'
|
||||
- 'spec/models/product_importer_spec.rb'
|
||||
|
||||
# Offense count: 24
|
||||
# Offense count: 23
|
||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
- 'app/models/spree/ability.rb'
|
||||
@@ -293,19 +286,17 @@ Metrics/ParameterLists:
|
||||
- 'spec/support/controller_requests_helper.rb'
|
||||
- 'spec/system/admin/reports_spec.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 3
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
||||
Metrics/PerceivedComplexity:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/models/enterprise_relationship.rb'
|
||||
- 'app/models/spree/ability.rb'
|
||||
- 'app/models/spree/order/checkout.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# Offense count: 7
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/admin/taxonomies_controller.rb'
|
||||
- 'app/mailers/producer_mailer.rb'
|
||||
- 'app/models/spree/order.rb'
|
||||
- 'app/services/checkout/post_checkout_actions.rb'
|
||||
@@ -353,7 +344,7 @@ Naming/VariableNumber:
|
||||
- 'spec/models/spree/tax_rate_spec.rb'
|
||||
- 'spec/requests/api/orders_spec.rb'
|
||||
|
||||
# Offense count: 142
|
||||
# Offense count: 143
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: ResponseMethods.
|
||||
# ResponseMethods: response, last_response
|
||||
@@ -557,7 +548,7 @@ RSpecRails/InferredSpecType:
|
||||
- 'spec/requests/voucher_adjustments_spec.rb'
|
||||
- 'spec/routing/stripe_spec.rb'
|
||||
|
||||
# Offense count: 22
|
||||
# Offense count: 21
|
||||
# Configuration parameters: IgnoreScopes, Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/InverseOf:
|
||||
@@ -572,7 +563,6 @@ Rails/InverseOf:
|
||||
- 'app/models/spree/price.rb'
|
||||
- 'app/models/spree/product.rb'
|
||||
- 'app/models/spree/stock_item.rb'
|
||||
- 'app/models/spree/taxonomy.rb'
|
||||
- 'app/models/spree/variant.rb'
|
||||
- 'app/models/subscription_line_item.rb'
|
||||
|
||||
@@ -720,7 +710,7 @@ Style/GlobalStdStream:
|
||||
- 'lib/tasks/subscriptions/debug.rake'
|
||||
- 'lib/tasks/subscriptions/test.rake'
|
||||
|
||||
# Offense count: 12
|
||||
# Offense count: 10
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowSplatArgument.
|
||||
Style/HashConversion:
|
||||
@@ -728,9 +718,7 @@ Style/HashConversion:
|
||||
- 'app/controllers/admin/column_preferences_controller.rb'
|
||||
- 'app/controllers/admin/variant_overrides_controller.rb'
|
||||
- 'app/controllers/spree/admin/products_controller.rb'
|
||||
- 'app/models/order_cycle.rb'
|
||||
- 'app/models/product_import/product_importer.rb'
|
||||
- 'app/models/spree/shipping_method.rb'
|
||||
- 'app/serializers/api/admin/exchange_serializer.rb'
|
||||
- 'app/services/variants_stock_levels.rb'
|
||||
- 'spec/controllers/admin/inventory_items_controller_spec.rb'
|
||||
|
||||
@@ -44,9 +44,9 @@ module Admin
|
||||
|
||||
create_connected_app
|
||||
|
||||
jwt_service = VineJwtService.new(secret: connected_app_params[:vine_secret])
|
||||
vine_api = VineApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||
jwt_generator: jwt_service)
|
||||
jwt_service = Vine::JwtService.new(secret: connected_app_params[:vine_secret])
|
||||
vine_api = Vine::ApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||
jwt_generator: jwt_service)
|
||||
|
||||
if !@app.connect(api_key: connected_app_params[:vine_api_key],
|
||||
secret: connected_app_params[:vine_secret], vine_api:)
|
||||
|
||||
@@ -78,6 +78,18 @@ class CheckoutController < BaseController
|
||||
|
||||
return true if redirect_to_payment_gateway
|
||||
|
||||
# Redeem VINE voucher
|
||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
||||
unless vine_voucher_redeemer.redeem
|
||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
||||
else
|
||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
||||
end
|
||||
return false
|
||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
||||
end
|
||||
@order.process_payments!
|
||||
@order.confirm!
|
||||
order_completion_reset @order
|
||||
|
||||
@@ -24,9 +24,12 @@ module Spree
|
||||
end
|
||||
|
||||
def create
|
||||
# Try to redeem VINE voucher first as we don't want to create a payment and complete
|
||||
# the order if it fails
|
||||
return redirect_to spree.admin_order_payments_path(@order) unless redeem_vine_voucher
|
||||
|
||||
@payment = @order.payments.build(object_params)
|
||||
load_payment_source
|
||||
|
||||
begin
|
||||
unless @payment.save
|
||||
redirect_to spree.admin_order_payments_path(@order)
|
||||
@@ -51,6 +54,10 @@ module Spree
|
||||
event = params[:e]
|
||||
return unless event && @payment.payment_source
|
||||
|
||||
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
|
||||
# voucher first and exit if it fails
|
||||
return if event == "capture_and_complete_order" && !redeem_vine_voucher
|
||||
|
||||
# Because we have a transition method also called void, we do this to avoid conflicts.
|
||||
event = "void_transaction" if event == "void"
|
||||
if allowed_events.include?(event) && @payment.public_send("#{event}!")
|
||||
@@ -182,6 +189,22 @@ module Spree
|
||||
%w{capture void_transaction credit refund resend_authorization_email
|
||||
capture_and_complete_order}
|
||||
end
|
||||
|
||||
def redeem_vine_voucher
|
||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
||||
if vine_voucher_redeemer.redeem == false
|
||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
||||
else
|
||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
||||
end
|
||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,8 +70,7 @@ module Spree
|
||||
@order.recreate_all_fees! # Enterprise fees on line items and on the order itself
|
||||
|
||||
# Re apply the voucher
|
||||
VoucherAdjustmentsService.new(@order).update
|
||||
@order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
||||
|
||||
if @order.complete?
|
||||
@order.update_payment_fees!
|
||||
|
||||
@@ -4,7 +4,16 @@ class VoucherAdjustmentsController < BaseController
|
||||
before_action :set_order
|
||||
|
||||
def create
|
||||
if add_voucher
|
||||
if voucher_params[:voucher_code].blank?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return render_error
|
||||
end
|
||||
|
||||
voucher = load_voucher
|
||||
|
||||
return render_error unless valid_voucher?(voucher)
|
||||
|
||||
if add_voucher_to_order(voucher)
|
||||
update_payment_section
|
||||
elsif @order.errors.present?
|
||||
render_error
|
||||
@@ -30,19 +39,28 @@ class VoucherAdjustmentsController < BaseController
|
||||
@order = current_order
|
||||
end
|
||||
|
||||
def add_voucher
|
||||
if voucher_params[:voucher_code].blank?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return false
|
||||
end
|
||||
|
||||
voucher = Voucher.find_by(code: voucher_params[:voucher_code], enterprise: @order.distributor)
|
||||
def valid_voucher?(voucher)
|
||||
return false if @order.errors.present?
|
||||
|
||||
if voucher.nil?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return false
|
||||
end
|
||||
|
||||
if !voucher.valid?
|
||||
@order.errors.add(
|
||||
:voucher_code,
|
||||
I18n.t(
|
||||
'checkout.errors.create_voucher_error', error: voucher.errors.full_messages.to_sentence
|
||||
)
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def add_voucher_to_order(voucher)
|
||||
adjustment = voucher.create_adjustment(voucher.code, @order)
|
||||
|
||||
unless adjustment.persisted?
|
||||
@@ -51,14 +69,38 @@ class VoucherAdjustmentsController < BaseController
|
||||
return false
|
||||
end
|
||||
|
||||
# calculate_voucher_adjustment
|
||||
clear_payments
|
||||
|
||||
VoucherAdjustmentsService.new(@order).update
|
||||
@order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def load_voucher
|
||||
voucher = Voucher.find_by(code: voucher_params[:voucher_code],
|
||||
enterprise: @order.distributor)
|
||||
return voucher unless voucher.nil? || voucher.is_a?(Vouchers::Vine)
|
||||
|
||||
vine_voucher
|
||||
end
|
||||
|
||||
def vine_voucher
|
||||
vine_voucher_validator = Vine::VoucherValidatorService.new(
|
||||
voucher_code: voucher_params[:voucher_code], enterprise: @order.distributor
|
||||
)
|
||||
voucher = vine_voucher_validator.validate
|
||||
|
||||
return nil if vine_voucher_validator.errors[:not_found_voucher].present?
|
||||
|
||||
if vine_voucher_validator.errors.present?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.add_voucher_error'))
|
||||
return nil
|
||||
end
|
||||
|
||||
voucher
|
||||
end
|
||||
|
||||
def update_payment_section
|
||||
render cable_ready: cable_car.replace(
|
||||
selector: "#checkout-payment-methods",
|
||||
|
||||
31
app/models/concerns/vouchers/flat_ratable.rb
Normal file
31
app/models/concerns/vouchers/flat_ratable.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/concern"
|
||||
|
||||
module Vouchers
|
||||
module FlatRatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0 }
|
||||
end
|
||||
|
||||
def display_value
|
||||
Spree::Money.new(amount)
|
||||
end
|
||||
|
||||
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
||||
# covers more than the order.total we only need to create an adjustment covering the order.total
|
||||
def compute_amount(order)
|
||||
-amount.clamp(0, order.pre_discount_total)
|
||||
end
|
||||
|
||||
def rate(order)
|
||||
amount = compute_amount(order)
|
||||
|
||||
amount / order.pre_discount_total
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ class Voucher < ApplicationRecord
|
||||
class_name: 'Spree::Adjustment',
|
||||
dependent: nil
|
||||
|
||||
validates :code, presence: true, uniqueness: { scope: :enterprise_id }
|
||||
validates :code, presence: true
|
||||
|
||||
TYPES = ["Vouchers::FlatRate", "Vouchers::PercentageRate"].freeze
|
||||
|
||||
|
||||
@@ -2,24 +2,8 @@
|
||||
|
||||
module Vouchers
|
||||
class FlatRate < Voucher
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0 }
|
||||
include FlatRatable
|
||||
|
||||
def display_value
|
||||
Spree::Money.new(amount)
|
||||
end
|
||||
|
||||
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
||||
# covers more than the order.total we only need to create an adjustment covering the order.total
|
||||
def compute_amount(order)
|
||||
-amount.clamp(0, order.pre_discount_total)
|
||||
end
|
||||
|
||||
def rate(order)
|
||||
amount = compute_amount(order)
|
||||
|
||||
amount / order.pre_discount_total
|
||||
end
|
||||
validates_with ScopedUniquenessValidator
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ module Vouchers
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
|
||||
validates_with ScopedUniquenessValidator
|
||||
|
||||
def display_value
|
||||
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)
|
||||
|
||||
13
app/models/vouchers/vine.rb
Normal file
13
app/models/vouchers/vine.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module Vouchers
|
||||
class Vine < Voucher
|
||||
include FlatRatable
|
||||
|
||||
# a VINE voucher :
|
||||
# - can potentially be associated with mutiple enterprise
|
||||
# - code ( "short code" in VINE ) can be recycled, but they shouldn't be linked to the same
|
||||
# voucher_id
|
||||
validates :code, uniqueness: { scope: [:enterprise_id, :external_voucher_id] }
|
||||
end
|
||||
end
|
||||
81
app/services/vine/api_service.rb
Normal file
81
app/services/vine/api_service.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
|
||||
module Vine
|
||||
class ApiService
|
||||
attr_reader :api_key, :jwt_generator
|
||||
|
||||
def initialize(api_key:, jwt_generator:)
|
||||
@vine_api_url = ENV.fetch("VINE_API_URL")
|
||||
@api_key = api_key
|
||||
@jwt_generator = jwt_generator
|
||||
end
|
||||
|
||||
def my_team
|
||||
my_team_url = "#{@vine_api_url}/my-team"
|
||||
|
||||
call_with_logging do
|
||||
connection.get(my_team_url)
|
||||
end
|
||||
end
|
||||
|
||||
def voucher_validation(voucher_short_code)
|
||||
voucher_validation_url = "#{@vine_api_url}/voucher-validation"
|
||||
|
||||
call_with_logging do
|
||||
connection.post(
|
||||
voucher_validation_url,
|
||||
{ type: "voucher_code", value: voucher_short_code },
|
||||
'Content-Type': "application/json"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
voucher_redemptions_url = "#{@vine_api_url}/voucher-redemptions"
|
||||
|
||||
call_with_logging do
|
||||
connection.post(
|
||||
voucher_redemptions_url,
|
||||
{ voucher_id:, voucher_set_id:, amount: amount.to_i },
|
||||
'Content-Type': "application/json"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection
|
||||
jwt = jwt_generator.generate_token
|
||||
Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
'X-Authorization': "JWT #{jwt}",
|
||||
Accept: "application/json"
|
||||
}
|
||||
) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.request :authorization, 'Bearer', api_key
|
||||
f.use Faraday::Response::RaiseError
|
||||
end
|
||||
end
|
||||
|
||||
def call_with_logging
|
||||
yield
|
||||
rescue Faraday::ClientError, Faraday::ServerError => e
|
||||
# caller_location(2,1) gets us the second entry in the stacktrace,
|
||||
# ie the method where `call_with_logging` is called from
|
||||
log_error("#{self.class}##{caller_locations(2, 1)[0].label}", e.response)
|
||||
|
||||
# Re raise the same exception
|
||||
raise
|
||||
end
|
||||
|
||||
def log_error(prefix, response)
|
||||
Rails.logger.error "#{prefix} -- response_status: #{response[:status]}"
|
||||
Rails.logger.error "#{prefix} -- response: #{response[:body]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/services/vine/jwt_service.rb
Normal file
23
app/services/vine/jwt_service.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class JwtService
|
||||
ALGORITHM = "HS256"
|
||||
ISSUER = "openfoodnetwork"
|
||||
|
||||
def initialize(secret: )
|
||||
@secret = secret
|
||||
end
|
||||
|
||||
def generate_token
|
||||
generation_time = Time.zone.now
|
||||
payload = {
|
||||
iss: ISSUER,
|
||||
iat: generation_time.to_i,
|
||||
exp: (generation_time + 1.minute).to_i,
|
||||
}
|
||||
|
||||
JWT.encode(payload, @secret, ALGORITHM)
|
||||
end
|
||||
end
|
||||
end
|
||||
62
app/services/vine/voucher_redeemer_service.rb
Normal file
62
app/services/vine/voucher_redeemer_service.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class VoucherRedeemerService
|
||||
attr_reader :order, :errors
|
||||
|
||||
def initialize(order: )
|
||||
@order = order
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def redeem
|
||||
# Do nothing if we don't have a vine voucher added to the order
|
||||
@voucher_adjustment = order.voucher_adjustments.first
|
||||
@voucher = @voucher_adjustment&.originator
|
||||
|
||||
return true if @voucher_adjustment.nil? || !@voucher.is_a?(Vouchers::Vine)
|
||||
|
||||
return false if vine_settings.nil?
|
||||
|
||||
call_vine_api
|
||||
|
||||
@voucher_adjustment.close
|
||||
|
||||
true
|
||||
rescue Faraday::ClientError => e
|
||||
handle_errors(e.response)
|
||||
false
|
||||
rescue Faraday::Error => e
|
||||
Rails.logger.error e.inspect
|
||||
Bugsnag.notify(e)
|
||||
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vine_settings
|
||||
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: order.distributor)&.data
|
||||
end
|
||||
|
||||
def call_vine_api
|
||||
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
|
||||
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
|
||||
|
||||
# Voucher adjustment amount is stored in dollars and negative, VINE expect cents
|
||||
amount = -1 * @voucher_adjustment.amount * 100
|
||||
vine_api.voucher_redemptions(
|
||||
@voucher.external_voucher_id, @voucher.external_voucher_set_id, amount
|
||||
)
|
||||
end
|
||||
|
||||
def handle_errors(response)
|
||||
if response[:status] == 400
|
||||
errors[:redeeming_failed] = I18n.t("vine_voucher_redeemer_service.errors.redeeming_failed")
|
||||
else
|
||||
errors[:vine_api] = I18n.t("vine_voucher_redeemer_service.errors.vine_api")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
71
app/services/vine/voucher_validator_service.rb
Normal file
71
app/services/vine/voucher_validator_service.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class VoucherValidatorService
|
||||
attr_reader :voucher_code, :errors
|
||||
|
||||
def initialize(voucher_code:, enterprise:)
|
||||
@voucher_code = voucher_code
|
||||
@enterprise = enterprise
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def validate
|
||||
return nil if vine_settings.nil?
|
||||
|
||||
response = call_vine_api
|
||||
|
||||
save_voucher(response)
|
||||
rescue Faraday::ClientError => e
|
||||
handle_errors(e.response)
|
||||
nil
|
||||
rescue Faraday::Error => e
|
||||
Rails.logger.error e.inspect
|
||||
Bugsnag.notify(e)
|
||||
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vine_settings
|
||||
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: @enterprise)&.data
|
||||
end
|
||||
|
||||
def call_vine_api
|
||||
# Check voucher is valid
|
||||
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
|
||||
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
|
||||
|
||||
vine_api.voucher_validation(voucher_code)
|
||||
end
|
||||
|
||||
def handle_errors(response)
|
||||
if response[:status] == 400
|
||||
errors[:invalid_voucher] = I18n.t("vine_voucher_validator_service.errors.invalid_voucher")
|
||||
elsif response[:status] == 404
|
||||
errors[:not_found_voucher] =
|
||||
I18n.t("vine_voucher_validator_service.errors.not_found_voucher")
|
||||
else
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
end
|
||||
end
|
||||
|
||||
def save_voucher(response)
|
||||
voucher_data = response.body["data"]
|
||||
|
||||
# Check if voucher already exist
|
||||
voucher = Vouchers::Vine.find_or_initialize_by(
|
||||
code: voucher_code,
|
||||
enterprise: @enterprise,
|
||||
external_voucher_id: voucher_data["id"],
|
||||
external_voucher_set_id: voucher_data["voucher_set_id"]
|
||||
)
|
||||
voucher.amount = voucher_data["voucher_value_remaining"].to_f / 100
|
||||
voucher.save
|
||||
|
||||
voucher
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
|
||||
class VineApiService
|
||||
attr_reader :api_key, :jwt_generator
|
||||
|
||||
def initialize(api_key:, jwt_generator:)
|
||||
@vine_api_url = ENV.fetch("VINE_API_URL")
|
||||
@api_key = api_key
|
||||
@jwt_generator = jwt_generator
|
||||
end
|
||||
|
||||
def my_team
|
||||
my_team_url = "#{@vine_api_url}/my-team"
|
||||
|
||||
jwt = jwt_generator.generate_token
|
||||
connection = Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
'X-Authorization': "JWT #{jwt}",
|
||||
Accept: "application/json"
|
||||
}
|
||||
) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.request :authorization, 'Bearer', api_key
|
||||
end
|
||||
|
||||
response = connection.get(my_team_url)
|
||||
|
||||
if !response.success?
|
||||
Rails.logger.error "VineApiService#my_team -- response_status: #{response.status}"
|
||||
Rails.logger.error "VineApiService#my_team -- response: #{response.body}"
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VineJwtService
|
||||
ALGORITHM = "HS256"
|
||||
ISSUER = "openfoodnetwork"
|
||||
|
||||
def initialize(secret: )
|
||||
@secret = secret
|
||||
end
|
||||
|
||||
def generate_token
|
||||
generation_time = Time.zone.now
|
||||
payload = {
|
||||
iss: ISSUER,
|
||||
iat: generation_time.to_i,
|
||||
exp: (generation_time + 1.minute).to_i,
|
||||
}
|
||||
|
||||
JWT.encode(payload, @secret, ALGORITHM)
|
||||
end
|
||||
end
|
||||
25
app/validators/vouchers/scoped_uniqueness_validator.rb
Normal file
25
app/validators/vouchers/scoped_uniqueness_validator.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# paranoia doesn't support unique validation including deleted records:
|
||||
# https://github.com/rubysherpas/paranoia/pull/333
|
||||
# We use a custom validator to fix the issue, so we don't need to fork/patch the gem
|
||||
module Vouchers
|
||||
class ScopedUniquenessValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
@record = record
|
||||
|
||||
return unless unique_voucher_code_per_enterprise?
|
||||
|
||||
record.errors.add :code, :taken, value: @record.code
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unique_voucher_code_per_enterprise?
|
||||
query = Voucher.with_deleted.where(code: @record.code, enterprise_id: @record.enterprise_id)
|
||||
query = query.where.not(id: @record.id) unless @record.id.nil?
|
||||
|
||||
query.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
= t('.add_new')
|
||||
%br
|
||||
|
||||
- if @enterprise.vouchers.with_deleted.present?
|
||||
- if @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.present?
|
||||
%table
|
||||
%thead
|
||||
%tr
|
||||
@@ -17,7 +17,7 @@
|
||||
/%th= t('.customers')
|
||||
/%th= t('.net_value')
|
||||
%tbody
|
||||
- @enterprise.vouchers.with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
|
||||
- @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
|
||||
%tr
|
||||
%td= voucher.code
|
||||
%td= voucher.display_value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.medium-6#checkout-payment-methods
|
||||
- if @order.distributor.vouchers.present?
|
||||
- if @order.distributor.vouchers.present? || @order.distributor.connected_apps.vine.present?
|
||||
%div.checkout-substep
|
||||
= render partial: "checkout/voucher_section", locals: { order: @order, voucher_adjustment: @order.voucher_adjustments.first }
|
||||
|
||||
|
||||
@@ -574,7 +574,15 @@ en:
|
||||
depth: "Depth"
|
||||
payment_could_not_process: "The payment could not be processed"
|
||||
payment_could_not_complete: "The payment could not be completed"
|
||||
|
||||
vine_voucher_validator_service:
|
||||
errors:
|
||||
vine_api: "There was an error communicating with the API, please try again later."
|
||||
invalid_voucher: "The voucher is not valid"
|
||||
not_found_voucher: "Sorry, we couldn't find that voucher, please check the code."
|
||||
vine_voucher_redeemer_service:
|
||||
errors:
|
||||
vine_api: "There was an error communicating with the API"
|
||||
redeeming_failed: "Redeeming the voucher failed"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -2124,6 +2132,8 @@ en:
|
||||
no_shipping_methods_available: Checkout is not possible due to absence of shipping options. Please contact the shop owner.
|
||||
voucher_not_found: Not found
|
||||
add_voucher_error: There was an error while adding the voucher
|
||||
create_voucher_error: "There was an error while creating the voucher: %{error}"
|
||||
voucher_redeeming_error: There was an error while trying to redeem your voucher
|
||||
shops:
|
||||
hubs:
|
||||
show_closed_shops: "Show closed shops"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class AddExternalVoucherIdExternalVoucherSetIdVoucherTypeToVouchers < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :vouchers, :external_voucher_id, :uuid
|
||||
add_column :vouchers, :external_voucher_set_id, :uuid
|
||||
add_column :vouchers, :voucher_type, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class UpdateIndexAndRemoveVoucherTypeFromVoucher < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
remove_column :vouchers, :voucher_type
|
||||
remove_index :vouchers, [:code, :enterprise_id], unique: true
|
||||
|
||||
add_index :vouchers, [:code, :enterprise_id]
|
||||
add_index :vouchers, [:code, :enterprise_id, :external_voucher_id], name: "index_vouchers_on_code_and_enterprise_id_and_ext_voucher_id"
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_10_30_033956) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_11_12_230401) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "plpgsql"
|
||||
@@ -1110,7 +1110,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_30_033956) do
|
||||
t.datetime "deleted_at", precision: nil
|
||||
t.decimal "amount", precision: 10, scale: 2, default: "0.0", null: false
|
||||
t.string "type", limit: 255, default: "Vouchers::FlatRate", null: false
|
||||
t.index ["code", "enterprise_id"], name: "index_vouchers_on_code_and_enterprise_id", unique: true
|
||||
t.uuid "external_voucher_id"
|
||||
t.uuid "external_voucher_set_id"
|
||||
t.index ["code", "enterprise_id", "external_voucher_id"], name: "index_vouchers_on_code_and_enterprise_id_and_ext_voucher_id"
|
||||
t.index ["code", "enterprise_id"], name: "index_vouchers_on_code_and_enterprise_id"
|
||||
t.index ["deleted_at"], name: "index_vouchers_on_deleted_at"
|
||||
t.index ["enterprise_id"], name: "index_vouchers_on_enterprise_id"
|
||||
end
|
||||
|
||||
@@ -161,6 +161,11 @@ module OrderManagement
|
||||
persist_totals
|
||||
end
|
||||
|
||||
def update_voucher
|
||||
VoucherAdjustmentsService.new(order).update
|
||||
update_totals_and_states
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cancel_payments_requiring_auth
|
||||
|
||||
@@ -460,6 +460,26 @@ module OrderManagement
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update_voucher" do
|
||||
let(:voucher_service) { instance_double(VoucherAdjustmentsService) }
|
||||
|
||||
it "calls VoucherAdjustmentsService" do
|
||||
expect(VoucherAdjustmentsService).to receive(:new).and_return(voucher_service)
|
||||
expect(voucher_service).to receive(:update)
|
||||
|
||||
updater.update_voucher
|
||||
end
|
||||
|
||||
it "calls update_totals_and_states" do
|
||||
allow(VoucherAdjustmentsService).to receive(:new).and_return(voucher_service)
|
||||
allow(voucher_service).to receive(:update)
|
||||
|
||||
expect(updater).to receive(:update_totals_and_states)
|
||||
|
||||
updater.update_voucher
|
||||
end
|
||||
end
|
||||
|
||||
def update_order_quantity(order)
|
||||
order.line_items.first.update_attribute(:quantity, 2)
|
||||
end
|
||||
|
||||
@@ -433,6 +433,7 @@ RSpec.describe CheckoutController, type: :controller do
|
||||
|
||||
context "summary step" do
|
||||
let(:step) { "summary" }
|
||||
let(:checkout_params) { { confirm_order: "Complete order" } }
|
||||
|
||||
before do
|
||||
order.bill_address = address
|
||||
@@ -496,6 +497,58 @@ RSpec.describe CheckoutController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", feature: :connected_apps do
|
||||
let(:vine_voucher) {
|
||||
create(:vine_voucher, code: 'some_code', enterprise: distributor, amount: 6)
|
||||
}
|
||||
let(:vine_voucher_redeemer) { instance_double(Vine::VoucherRedeemerService) }
|
||||
|
||||
before do
|
||||
# Adding voucher to the order
|
||||
vine_voucher.create_adjustment(vine_voucher.code, order)
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
|
||||
allow(Vine::VoucherRedeemerService).to receive(:new).and_return(vine_voucher_redeemer)
|
||||
end
|
||||
|
||||
it "completes the order and redirects to order confirmation" do
|
||||
expect(vine_voucher_redeemer).to receive(:redeem).and_return(true)
|
||||
|
||||
put(:update, params:)
|
||||
|
||||
expect(response).to redirect_to order_path(order, order_token: order.token)
|
||||
expect(order.reload.state).to eq "complete"
|
||||
end
|
||||
|
||||
context "when redeeming the voucher fails" do
|
||||
it "returns 422 and some error" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ redeeming_failed: "Redeeming the voucher failed" }
|
||||
)
|
||||
|
||||
put(:update, params:)
|
||||
|
||||
expect(response.status).to eq 422
|
||||
expect(flash[:error]).to match "Redeeming the voucher failed"
|
||||
end
|
||||
end
|
||||
|
||||
context "when an other error happens" do
|
||||
it "returns 422 and some error" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ vine_api: "There was an error communicating with the API" }
|
||||
)
|
||||
|
||||
put(:update, params:)
|
||||
|
||||
expect(response.status).to eq 422
|
||||
expect(flash[:error]).to match "There was an error while trying to redeem your voucher"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when an external payment gateway is used" do
|
||||
before do
|
||||
expect(Checkout::PaymentMethodFetcher).
|
||||
|
||||
@@ -14,4 +14,10 @@ FactoryBot.define do
|
||||
factory :voucher_percentage_rate, parent: :voucher, class: Vouchers::PercentageRate do
|
||||
amount { rand(1..100) }
|
||||
end
|
||||
|
||||
factory :vine_voucher, parent: :voucher, class: Vouchers::Vine do
|
||||
amount { 20 }
|
||||
external_voucher_id { SecureRandom.uuid }
|
||||
external_voucher_set_id { SecureRandom.uuid }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"type":"voucher_code","value":"CI3922"}'
|
||||
headers:
|
||||
X-Authorization:
|
||||
- "<HIDDEN-VINE-TOKEN>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Faraday v2.9.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
Cache-Control:
|
||||
- no-cache, private
|
||||
Date:
|
||||
- Sun, 20 Oct 2024 03:32:37 GMT
|
||||
X-Ratelimit-Limit:
|
||||
- '60'
|
||||
X-Ratelimit-Remaining:
|
||||
- '58'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"meta":{"responseCode":200,"limit":50,"offset":0,"message":""},"data":{"id":"9d2437c8-4559-4dda-802e-8d9c642a0c1d","voucher_short_code":"CI3922","voucher_set_id":"9d24349c-1fe8-4090-988b-d7355ed32559","is_test":1,"voucher_value_original":500,"voucher_value_remaining":500,"num_voucher_redemptions":0,"last_redemption_at":null,"created_at":"2024-10-01T13:20:02.000000Z","updated_at":"2024-10-01T13:20:02.000000Z","deleted_at":null}}'
|
||||
recorded_at: Sun, 20 Oct 2024 03:32:37 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"type":"voucher_code","value":"KM1891"}'
|
||||
headers:
|
||||
X-Authorization:
|
||||
- "<HIDDEN-VINE-TOKEN>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Faraday v2.9.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Cache-Control:
|
||||
- no-cache, private
|
||||
Date:
|
||||
- Sun, 20 Oct 2024 03:42:25 GMT
|
||||
X-Ratelimit-Limit:
|
||||
- '60'
|
||||
X-Ratelimit-Remaining:
|
||||
- '59'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"meta":{"responseCode":400,"limit":50,"offset":0,"message":"Invalid
|
||||
merchant team."},"data":[]}'
|
||||
recorded_at: Sun, 20 Oct 2024 03:42:25 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-redemptions
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"voucher_id":"9d316d27-4e3c-4c8c-b3c8-8e23cc171f20","voucher_set_id":"9d314daa-0878-4b73-922d-698047640cf4","amount":1}'
|
||||
headers:
|
||||
X-Authorization:
|
||||
- "<HIDDEN-VINE-TOKEN>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Faraday v2.9.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
Cache-Control:
|
||||
- no-cache, private
|
||||
Date:
|
||||
- Wed, 23 Oct 2024 03:16:39 GMT
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"meta":{"responseCode":200,"limit":50,"offset":0,"message":"Redemption
|
||||
successful. This was a test redemption. Do NOT provide the person with goods
|
||||
or services."},"data":{"voucher_id":"9d316d27-4e3c-4c8c-b3c8-8e23cc171f20","voucher_set_id":"9d314daa-0878-4b73-922d-698047640cf4","redeemed_by_user_id":8,"redeemed_by_team_id":4,"redeemed_amount":1,"is_test":1,"updated_at":"2024-10-23T03:16:39.000000Z","created_at":"2024-10-23T03:16:39.000000Z","id":7}}'
|
||||
recorded_at: Wed, 23 Oct 2024 03:16:39 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-redemptions
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"voucher_id":"9d316d27-0dad-411a-8953-316a1aaf7742","voucher_set_id":"9d314daa-0878-4b73-922d-698047640cf4","amount":1}'
|
||||
headers:
|
||||
X-Authorization:
|
||||
- "<HIDDEN-VINE-TOKEN>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Faraday v2.9.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
Cache-Control:
|
||||
- no-cache, private
|
||||
Date:
|
||||
- Mon, 21 Oct 2024 03:07:09 GMT
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"meta":{"responseCode":200,"limit":50,"offset":0,"message":"Redemption
|
||||
successful. This was a test redemption. Do NOT provide the person with goods
|
||||
or services."},"data":{"voucher_id":"9d316d27-0dad-411a-8953-316a1aaf7742","voucher_set_id":"9d314daa-0878-4b73-922d-698047640cf4","redeemed_by_user_id":8,"redeemed_by_team_id":4,"redeemed_amount":1,"is_test":1,"updated_at":"2024-10-21T03:07:09.000000Z","created_at":"2024-10-21T03:07:09.000000Z","id":5}}'
|
||||
recorded_at: Mon, 21 Oct 2024 03:07:09 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"type":"voucher_code","value":"CI3922"}'
|
||||
headers:
|
||||
X-Authorization:
|
||||
- "<HIDDEN-VINE-TOKEN>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Faraday v2.9.0
|
||||
Content-Type:
|
||||
- application/json
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx
|
||||
Content-Type:
|
||||
- application/json
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Connection:
|
||||
- keep-alive
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
Cache-Control:
|
||||
- no-cache, private
|
||||
Date:
|
||||
- Mon, 07 Oct 2024 23:48:46 GMT
|
||||
X-Ratelimit-Limit:
|
||||
- '60'
|
||||
X-Ratelimit-Remaining:
|
||||
- '59'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"meta":{"responseCode":200,"limit":50,"offset":0,"message":""},"data":{"id":"9d2437c8-4559-4dda-802e-8d9c642a0c1d","voucher_short_code":"CI3922","voucher_set_id":"9d24349c-1fe8-4090-988b-d7355ed32559","is_test":1,"voucher_value_original":500,"voucher_value_remaining":500,"num_voucher_redemptions":0,"last_redemption_at":null,"created_at":"2024-10-01T13:20:02.000000Z","updated_at":"2024-10-01T13:20:02.000000Z","deleted_at":null}}'
|
||||
recorded_at: Mon, 07 Oct 2024 23:48:46 GMT
|
||||
recorded_with: VCR 6.2.0
|
||||
@@ -272,8 +272,7 @@ RSpec.describe "Reporting::Reports::SalesTax::SalesTaxTotalsByOrder" do
|
||||
def add_voucher(order, voucher)
|
||||
# Add voucher to the order
|
||||
voucher.create_adjustment(voucher.code, order)
|
||||
VoucherAdjustmentsService.new(order).update
|
||||
order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
|
||||
Orders::WorkflowService.new(order).complete!
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ RSpec.describe ConnectedApps::Vine do
|
||||
|
||||
let(:vine_api_key) { "12345" }
|
||||
let(:secret) { "my_secret" }
|
||||
let(:vine_api) { instance_double(VineApiService) }
|
||||
let(:vine_api) { instance_double(Vine::ApiService) }
|
||||
|
||||
describe "#connect" do
|
||||
it "send a request to VINE api" do
|
||||
|
||||
@@ -27,7 +27,6 @@ RSpec.describe Voucher do
|
||||
subject { build(:voucher_flat_rate, code: 'new_code', enterprise:) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:code) }
|
||||
it { is_expected.to validate_uniqueness_of(:code).scoped_to(:enterprise_id) }
|
||||
end
|
||||
|
||||
describe '#display_value' do
|
||||
|
||||
@@ -8,6 +8,7 @@ RSpec.describe Vouchers::FlatRate do
|
||||
|
||||
it { is_expected.to validate_presence_of(:amount) }
|
||||
it { is_expected.to validate_numericality_of(:amount).is_greater_than(0) }
|
||||
it_behaves_like 'has a unique code per enterprise', "voucher_flat_rate"
|
||||
end
|
||||
|
||||
describe '#compute_amount' do
|
||||
|
||||
@@ -12,6 +12,7 @@ RSpec.describe Vouchers::PercentageRate do
|
||||
.is_greater_than(0)
|
||||
.is_less_than_or_equal_to(100)
|
||||
end
|
||||
it_behaves_like 'has a unique code per enterprise', "voucher_percentage_rate"
|
||||
end
|
||||
|
||||
describe '#compute_amount' do
|
||||
|
||||
83
spec/models/vouchers/vine_spec.rb
Normal file
83
spec/models/vouchers/vine_spec.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Vouchers::Vine do
|
||||
describe 'validations' do
|
||||
subject { build(:vine_voucher) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:amount) }
|
||||
it { is_expected.to validate_numericality_of(:amount).is_greater_than(0) }
|
||||
|
||||
describe "#code" do
|
||||
subject { build(:vine_voucher, code: 'vine_code', enterprise:, external_voucher_id: ) }
|
||||
|
||||
let(:external_voucher_id) { SecureRandom.uuid }
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
it {
|
||||
is_expected.to validate_uniqueness_of(:code).scoped_to(
|
||||
[:enterprise_id, :external_voucher_id]
|
||||
)
|
||||
}
|
||||
|
||||
it "can be reused within the same enterprise" do
|
||||
subject.save!
|
||||
# Voucher with the same code but different external_voucher_id, it is mapped to a
|
||||
# different voucher in VINE
|
||||
voucher = build(:vine_voucher, code: 'vine_code', enterprise: )
|
||||
expect(voucher.valid?).to be(true)
|
||||
end
|
||||
|
||||
it "can be used by mutiple enterprises" do
|
||||
subject.save!
|
||||
# Voucher with the same code and external_voucher_id, ie exiting VINE voucher used by
|
||||
# another enterprise
|
||||
voucher = build(:vine_voucher, code: 'vine_code', enterprise: build(:enterprise),
|
||||
external_voucher_id: )
|
||||
expect(voucher.valid?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#compute_amount' do
|
||||
let(:order) { create(:order_with_totals) }
|
||||
|
||||
before do
|
||||
order.update_columns(item_total: 15)
|
||||
end
|
||||
|
||||
context 'when order total is more than the voucher' do
|
||||
subject { create(:vine_voucher, amount: 5) }
|
||||
|
||||
it 'uses the voucher total' do
|
||||
expect(subject.compute_amount(order).to_f).to eq(-5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when order total is less than the voucher' do
|
||||
subject { create(:vine_voucher, amount: 20) }
|
||||
|
||||
it 'matches the order total' do
|
||||
expect(subject.compute_amount(order).to_f).to eq(-15)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#rate" do
|
||||
subject do
|
||||
create(:vine_voucher, code: 'new_code', amount: 5)
|
||||
end
|
||||
let(:order) { create(:order_with_totals) }
|
||||
|
||||
before do
|
||||
order.update_columns(item_total: 10)
|
||||
end
|
||||
|
||||
it "returns the voucher rate" do
|
||||
# rate = -voucher_amount / order.pre_discount_total
|
||||
# -5 / 10 = -0.5
|
||||
expect(subject.rate(order).to_f).to eq(-0.5)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -13,11 +13,11 @@ RSpec.describe "Admin ConnectedApp" do
|
||||
|
||||
describe "POST /admin/enterprises/:enterprise_id/connected_apps" do
|
||||
context "with type ConnectedApps::Vine" do
|
||||
let(:vine_api) { instance_double(VineApiService) }
|
||||
let(:vine_api) { instance_double(Vine::ApiService) }
|
||||
|
||||
before do
|
||||
allow(VineJwtService).to receive(:new).and_return(instance_double(VineJwtService))
|
||||
allow(VineApiService).to receive(:new).and_return(vine_api)
|
||||
allow(Vine::JwtService).to receive(:new).and_return(instance_double(Vine::JwtService))
|
||||
allow(Vine::ApiService).to receive(:new).and_return(vine_api)
|
||||
end
|
||||
|
||||
it "creates a new connected app" do
|
||||
@@ -115,7 +115,7 @@ RSpec.describe "Admin ConnectedApp" do
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with("VINE_API_URL").and_raise(KeyError)
|
||||
allow(VineApiService).to receive(:new).and_call_original
|
||||
allow(Vine::ApiService).to receive(:new).and_call_original
|
||||
end
|
||||
|
||||
it "redirects to enterprise edit page, with an error" do
|
||||
|
||||
@@ -10,6 +10,120 @@ RSpec.describe Spree::Admin::PaymentsController, type: :request do
|
||||
sign_in create(:admin_user)
|
||||
end
|
||||
|
||||
describe "POST /admin/orders/:order_number/payments.json" do
|
||||
let(:params) do
|
||||
{
|
||||
payment: {
|
||||
payment_method_id: payment_method.id, amount: order.total
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:payment_method) do
|
||||
create(:payment_method, distributors: [order.distributor])
|
||||
end
|
||||
|
||||
it "creates a payment" do
|
||||
expect {
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
}.to change { order.payments.count }.by(1)
|
||||
end
|
||||
|
||||
it "redirect to payments page" do
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
expect(flash[:success]).to eq "Payment has been successfully created!"
|
||||
end
|
||||
|
||||
context "when failing to create payment" do
|
||||
it "redirects to payments page" do
|
||||
payment_mock = instance_double(Spree::Payment)
|
||||
allow(order.payments).to receive(:build).and_return(payment_mock)
|
||||
allow(payment_mock).to receive(:save).and_return(false)
|
||||
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
end
|
||||
end
|
||||
|
||||
context "when a gateway error happens" do
|
||||
let(:payment_method) do
|
||||
create(:stripe_sca_payment_method, distributors: [order.distributor])
|
||||
end
|
||||
|
||||
it "redirect to payments page" do
|
||||
allow(Spree::Order).to receive(:find_by!).and_return(order)
|
||||
|
||||
stripe_sca_payment_authorize =
|
||||
instance_double(OrderManagement::Order::StripeScaPaymentAuthorize)
|
||||
allow(OrderManagement::Order::StripeScaPaymentAuthorize).to receive(:new)
|
||||
.and_return(stripe_sca_payment_authorize)
|
||||
# Simulate an error
|
||||
allow(stripe_sca_payment_authorize).to receive(:call!) do
|
||||
order.errors.add(:base, "authorization_failure")
|
||||
end
|
||||
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
expect(flash[:error]).to eq("Authorization Failure")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", feature: :connected_apps do
|
||||
let(:vine_voucher) {
|
||||
create(:vine_voucher, code: 'some_code', enterprise: order.distributor, amount: 6)
|
||||
}
|
||||
let(:vine_voucher_redeemer) { instance_double(Vine::VoucherRedeemerService) }
|
||||
|
||||
before do
|
||||
add_voucher_to_order(vine_voucher, order)
|
||||
|
||||
allow(Vine::VoucherRedeemerService).to receive(:new).and_return(vine_voucher_redeemer)
|
||||
end
|
||||
|
||||
it "completes the order and redirects to payment page" do
|
||||
expect(vine_voucher_redeemer).to receive(:redeem).and_return(true)
|
||||
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
expect(flash[:success]).to eq "Payment has been successfully created!"
|
||||
|
||||
expect(order.reload.state).to eq "complete"
|
||||
end
|
||||
|
||||
context "when redeeming the voucher fails" do
|
||||
it "redirect to payments page" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ redeeming_failed: "Redeeming the voucher failed" }
|
||||
)
|
||||
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
expect(flash[:error]).to match "Redeeming the voucher failed"
|
||||
end
|
||||
end
|
||||
|
||||
context "when an other error happens" do
|
||||
it "redirect to payments page" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ vine_api: "There was an error communicating with the API" }
|
||||
)
|
||||
|
||||
post("/admin/orders/#{order.number}/payments.json", params:)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_path(order))
|
||||
expect(flash[:error]).to match "There was an error while trying to redeem your voucher"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /admin/orders/:order_number/payments/:id/fire" do
|
||||
let(:payment) do
|
||||
create(
|
||||
@@ -57,7 +171,7 @@ RSpec.describe Spree::Admin::PaymentsController, type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context "with 'void' parameter" do
|
||||
context "with 'void' event" do
|
||||
before do
|
||||
allow(Spree::Payment).to receive(:find).and_return(payment)
|
||||
end
|
||||
@@ -101,6 +215,120 @@ RSpec.describe Spree::Admin::PaymentsController, type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context "with 'capture_and_complete_order' event" do
|
||||
before do
|
||||
allow(Spree::Payment).to receive(:find).and_return(payment)
|
||||
end
|
||||
|
||||
it "calls capture_and_complete_order! on payment" do
|
||||
expect(payment).to receive(:capture_and_complete_order!)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
end
|
||||
|
||||
it "redirect to payments page" do
|
||||
allow(payment).to receive(:capture_and_complete_order!).and_return(true)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_url(order))
|
||||
expect(flash[:success]).to eq "Payment Updated"
|
||||
end
|
||||
|
||||
context "when capture_and_complete_order! fails" do
|
||||
it "set an error flash message" do
|
||||
allow(payment).to receive(:capture_and_complete_order!).and_return(false)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_url(order))
|
||||
expect(flash[:error]).to eq "Could not update the payment"
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", feature: :connected_apps do
|
||||
let(:vine_voucher) {
|
||||
create(:vine_voucher, code: 'some_code', enterprise: order.distributor, amount: 6)
|
||||
}
|
||||
let(:vine_voucher_redeemer) { instance_double(Vine::VoucherRedeemerService) }
|
||||
|
||||
before do
|
||||
add_voucher_to_order(vine_voucher, order)
|
||||
|
||||
allow(Vine::VoucherRedeemerService).to receive(:new).and_return(vine_voucher_redeemer)
|
||||
end
|
||||
|
||||
it "completes the order and redirects to payment page" do
|
||||
expect(vine_voucher_redeemer).to receive(:redeem).and_return(true)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_url(order))
|
||||
expect(flash[:success]).to eq "Payment Updated"
|
||||
|
||||
expect(order.reload.state).to eq "complete"
|
||||
end
|
||||
|
||||
context "when redeeming the voucher fails" do
|
||||
it "redirect to payments page" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ redeeming_failed: "Redeeming the voucher failed" }
|
||||
)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_url(order))
|
||||
expect(flash[:error]).to match "Redeeming the voucher failed"
|
||||
end
|
||||
end
|
||||
|
||||
context "when an other error happens" do
|
||||
it "redirect to payments page" do
|
||||
allow(vine_voucher_redeemer).to receive(:redeem).and_return(false)
|
||||
allow(vine_voucher_redeemer).to receive(:errors).and_return(
|
||||
{ vine_api: "There was an error communicating with the API" }
|
||||
)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/" \
|
||||
"fire?e=capture_and_complete_order",
|
||||
params: {},
|
||||
headers:
|
||||
)
|
||||
|
||||
expect(response).to redirect_to(spree.admin_order_payments_url(order))
|
||||
expect(flash[:error]).to match "There was an error while trying to redeem your voucher"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when something unexpected happen" do
|
||||
before do
|
||||
allow(Spree::Payment).to receive(:find).and_return(payment)
|
||||
@@ -133,4 +361,9 @@ RSpec.describe Spree::Admin::PaymentsController, type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_voucher_to_order(voucher, order)
|
||||
voucher.create_adjustment(voucher.code, order)
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,10 +34,10 @@ RSpec.describe VoucherAdjustmentsController, type: :request do
|
||||
let(:params) { { order: { voucher_code: voucher.code } } }
|
||||
|
||||
it "adds a voucher to the user's current order" do
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect {
|
||||
post("/voucher_adjustments", params:)
|
||||
}.to change { order.reload.voucher_adjustments.count }.by(1)
|
||||
expect(response).to be_successful
|
||||
expect(order.reload.voucher_adjustments.length).to eq(1)
|
||||
end
|
||||
|
||||
context "when voucher doesn't exist" do
|
||||
@@ -85,6 +85,142 @@ RSpec.describe VoucherAdjustmentsController, type: :request do
|
||||
end.to change { order.reload.all_adjustments.payment_fee.count }.from(1).to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", feature: :connected_apps do
|
||||
let(:vine_voucher_validator) { instance_double(Vine::VoucherValidatorService) }
|
||||
|
||||
before do
|
||||
allow(Vine::VoucherValidatorService).to receive(:new).and_return(vine_voucher_validator)
|
||||
end
|
||||
|
||||
context "with a new voucher" do
|
||||
let(:params) { { order: { voucher_code: vine_voucher_code } } }
|
||||
let(:vine_voucher_code) { "PQ3187" }
|
||||
|
||||
context "with a valid voucher" do
|
||||
it "verifies the voucher with VINE API" do
|
||||
expect(vine_voucher_validator).to receive(:validate)
|
||||
allow(vine_voucher_validator).to receive(:errors).and_return({})
|
||||
|
||||
post "/voucher_adjustments", params:
|
||||
end
|
||||
|
||||
it "adds a voucher to the user's current order" do
|
||||
vine_voucher = create(:vine_voucher, code: vine_voucher_code)
|
||||
mock_vine_voucher_validator(voucher: vine_voucher)
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(order.reload.voucher_adjustments.length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when coordinator is not connected to VINE" do
|
||||
it "returns 422 and an error message" do
|
||||
mock_vine_voucher_validator(voucher: nil)
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match "Voucher code Not found"
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an API error" do
|
||||
it "returns 422 and an error message" do
|
||||
mock_vine_voucher_validator(
|
||||
voucher: nil,
|
||||
errors: { vine_api: "There was an error communicating with the API" }
|
||||
)
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match "There was an error while adding the voucher"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the voucher doesn't exist" do
|
||||
it "returns 422 and an error message" do
|
||||
mock_vine_voucher_validator(voucher: nil,
|
||||
errors: { not_found_voucher: "The voucher doesn't exist" })
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match "Voucher code Not found"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the voucher is invalid voucher" do
|
||||
it "returns 422 and an error message" do
|
||||
mock_vine_voucher_validator(voucher: nil,
|
||||
errors: { invalid_voucher: "The voucher is not valid" })
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match "There was an error while adding the voucher"
|
||||
end
|
||||
end
|
||||
|
||||
context "when creating a new voucher fails" do
|
||||
it "returns 422 and an error message" do
|
||||
vine_voucher = build(:vine_voucher, code: vine_voucher_code,
|
||||
enterprise: distributor, amount: "")
|
||||
mock_vine_voucher_validator(voucher: vine_voucher)
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match(
|
||||
"There was an error while creating the voucher: Amount can't be blank and " \
|
||||
"Amount is not a number"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with an existing voucher" do
|
||||
let(:params) { { order: { voucher_code: vine_voucher_code } } }
|
||||
let(:vine_voucher_code) { "PQ3187" }
|
||||
|
||||
it "verify the voucher with VINE API" do
|
||||
expect(vine_voucher_validator).to receive(:validate)
|
||||
allow(vine_voucher_validator).to receive(:errors).and_return({})
|
||||
|
||||
post "/voucher_adjustments", params:
|
||||
end
|
||||
|
||||
it "adds a voucher to the user's current order" do
|
||||
vine_voucher = create(:vine_voucher, code: vine_voucher_code,
|
||||
enterprise: distributor)
|
||||
mock_vine_voucher_validator(voucher: vine_voucher)
|
||||
|
||||
expect {
|
||||
post("/voucher_adjustments", params:)
|
||||
}.to change { order.reload.voucher_adjustments.count }.by(1)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
|
||||
context "when updating the voucher fails" do
|
||||
it "returns 422 and an error message" do
|
||||
vine_voucher = build(:vine_voucher, code: vine_voucher_code,
|
||||
enterprise: distributor, amount: "")
|
||||
mock_vine_voucher_validator(voucher: vine_voucher)
|
||||
|
||||
post("/voucher_adjustments", params:)
|
||||
|
||||
expect(response).to be_unprocessable
|
||||
expect(flash[:error]).to match(
|
||||
"There was an error while creating the voucher: Amount can't be blank and " \
|
||||
"Amount is not a number"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE voucher_adjustments/:id" do
|
||||
@@ -137,4 +273,9 @@ RSpec.describe VoucherAdjustmentsController, type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mock_vine_voucher_validator(voucher:, errors: {})
|
||||
allow(vine_voucher_validator).to receive(:validate).and_return(voucher)
|
||||
allow(vine_voucher_validator).to receive(:errors).and_return(errors)
|
||||
end
|
||||
end
|
||||
|
||||
198
spec/services/vine/api_service_spec.rb
Normal file
198
spec/services/vine/api_service_spec.rb
Normal file
@@ -0,0 +1,198 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Vine::ApiService do
|
||||
subject(:vine_api) { described_class.new(api_key: vine_api_key, jwt_generator: jwt_service) }
|
||||
|
||||
let(:vine_api_url) { "https://vine-staging.openfoodnetwork.org.au/api/v1" }
|
||||
let(:vine_api_key) { "12345" }
|
||||
let(:jwt_service) { Vine::JwtService.new(secret:) }
|
||||
let(:secret) { "my_secret" }
|
||||
let(:token) { "some.jwt.token" }
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with("VINE_API_URL").and_return(vine_api_url)
|
||||
end
|
||||
|
||||
describe "#my_team" do
|
||||
let(:my_team_url) { "#{vine_api_url}/my-team" }
|
||||
|
||||
it "send a request to the team VINE api endpoint" do
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect(a_request(
|
||||
:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team"
|
||||
)).to have_been_made
|
||||
end
|
||||
|
||||
it "sends the VINE api key via a header" do
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect_request_with_api_key(:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team")
|
||||
end
|
||||
|
||||
it "sends JWT token via a header" do
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
mock_jwt_service
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect_request_with_jwt_token(:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team")
|
||||
end
|
||||
|
||||
context "when a request succeed", :vcr do
|
||||
it "returns the response" do
|
||||
response = vine_api.my_team
|
||||
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.body).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a request fails" do
|
||||
it "logs the error" do
|
||||
stub_request(:get, my_team_url).to_return(body: "error", status: 401)
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(match("Vine::ApiService#my_team")).twice
|
||||
expect { vine_api.my_team }.to raise_error(Faraday::UnauthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#voucher_validation" do
|
||||
let(:voucher_validation_url) { "#{vine_api_url}/voucher-validation" }
|
||||
let(:voucher_short_code) { "CI3922" }
|
||||
|
||||
it "send a POST request to the team VINE api endpoint" do
|
||||
stub_request(:post, voucher_validation_url).to_return(status: 200)
|
||||
vine_api.voucher_validation(voucher_short_code)
|
||||
|
||||
expect(a_request(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation"
|
||||
).with(body: { type: "voucher_code", value: voucher_short_code } )).to have_been_made
|
||||
end
|
||||
|
||||
it "sends the VINE api key via a header" do
|
||||
stub_request(:post, voucher_validation_url).to_return(status: 200)
|
||||
|
||||
vine_api.voucher_validation(voucher_short_code)
|
||||
|
||||
expect_request_with_api_key(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation"
|
||||
)
|
||||
end
|
||||
|
||||
it "sends JWT token via a header" do
|
||||
stub_request(:post, voucher_validation_url).to_return(status: 200)
|
||||
mock_jwt_service
|
||||
|
||||
vine_api.voucher_validation(voucher_short_code)
|
||||
|
||||
expect_request_with_jwt_token(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-validation"
|
||||
)
|
||||
end
|
||||
|
||||
context "when a request succeed", :vcr do
|
||||
it "returns the response" do
|
||||
response = vine_api.voucher_validation(voucher_short_code)
|
||||
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.body).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a request fails" do
|
||||
it "logs the error" do
|
||||
stub_request(:post, voucher_validation_url).to_return(body: "error", status: 401)
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(
|
||||
match("Vine::ApiService#voucher_validation")
|
||||
).twice
|
||||
expect {
|
||||
vine_api.voucher_validation(voucher_short_code)
|
||||
}.to raise_error(Faraday::UnauthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#voucher_redemptions" do
|
||||
let(:voucher_redemptions_url) { "#{vine_api_url}/voucher-redemptions" }
|
||||
let(:voucher_id) { "quia" }
|
||||
let(:voucher_set_id) { "natus" }
|
||||
let(:amount) { 500 } # $5.00
|
||||
|
||||
it "send a POST request to the voucher redemptions VINE api endpoint" do
|
||||
stub_request(:post, voucher_redemptions_url).to_return(status: 200)
|
||||
vine_api.voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
|
||||
expect(a_request(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-redemptions"
|
||||
).with(body: { voucher_id:, voucher_set_id:, amount: })).to have_been_made
|
||||
end
|
||||
|
||||
it "sends the VINE api key via a header" do
|
||||
stub_request(:post, voucher_redemptions_url).to_return(status: 200)
|
||||
|
||||
vine_api.voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
|
||||
expect_request_with_api_key(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-redemptions"
|
||||
)
|
||||
end
|
||||
|
||||
it "sends JWT token via a header" do
|
||||
stub_request(:post, voucher_redemptions_url).to_return(status: 200)
|
||||
mock_jwt_service
|
||||
|
||||
vine_api.voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
|
||||
expect_request_with_jwt_token(
|
||||
:post, "https://vine-staging.openfoodnetwork.org.au/api/v1/voucher-redemptions"
|
||||
)
|
||||
end
|
||||
|
||||
context "when a request succeed", :vcr do
|
||||
it "returns the response" do
|
||||
response = vine_api.voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.body).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a request fails" do
|
||||
it "logs the error" do
|
||||
stub_request(:post, voucher_redemptions_url).to_return(body: "error", status: 401)
|
||||
|
||||
expect(Rails.logger).to receive(:error).with(
|
||||
match("Vine::ApiService#voucher_redemptions")
|
||||
).twice.and_call_original
|
||||
|
||||
expect {
|
||||
vine_api.voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
}.to raise_error(Faraday::UnauthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_request_with_api_key(method, url)
|
||||
expect(a_request(method, url).with( headers: { Authorization: "Bearer #{vine_api_key}" }))
|
||||
.to have_been_made
|
||||
end
|
||||
|
||||
def expect_request_with_jwt_token(method, url)
|
||||
expect(a_request(method, url).with( headers: { 'X-Authorization': "JWT #{token}" }))
|
||||
.to have_been_made
|
||||
end
|
||||
|
||||
def mock_jwt_service
|
||||
allow(jwt_service).to receive(:generate_token).and_return(token)
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe VineJwtService do
|
||||
RSpec.describe Vine::JwtService do
|
||||
describe "#generate_token" do
|
||||
subject { described_class.new(secret: vine_secret) }
|
||||
let(:vine_secret) { "some_secret" }
|
||||
269
spec/services/vine/voucher_redeemer_service_spec.rb
Normal file
269
spec/services/vine/voucher_redeemer_service_spec.rb
Normal file
@@ -0,0 +1,269 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Vine::VoucherRedeemerService, feature: :connected_apps do
|
||||
subject(:voucher_redeemer_service) { described_class.new(order: ) }
|
||||
|
||||
let(:user) { order.user }
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:order_cycle, distributors: [distributor]) }
|
||||
let(:order) { create(:order_with_line_items, line_items_count: 1, distributor:, order_cycle:) }
|
||||
|
||||
let(:vine_voucher) {
|
||||
create(:vine_voucher, code: 'some_code', enterprise: distributor,
|
||||
amount: 50, external_voucher_id: voucher_id,
|
||||
external_voucher_set_id: voucher_set_id )
|
||||
}
|
||||
let(:voucher_id) { "9d316d27-0dad-411a-8953-316a1aaf7742" }
|
||||
let(:voucher_set_id) { "9d314daa-0878-4b73-922d-698047640cf4" }
|
||||
let(:vine_api_service) { instance_double(Vine::ApiService) }
|
||||
|
||||
before do
|
||||
allow(Vine::ApiService).to receive(:new).and_return(vine_api_service)
|
||||
end
|
||||
|
||||
describe "#redeem" do
|
||||
context "with a valid voucher" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: {
|
||||
responseCode: 200,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
message: "Redemption successful. This was a test redemption. Do NOT provide " \
|
||||
"the person with goods or services."
|
||||
},
|
||||
data: {
|
||||
voucher_id: "9d316d27-0dad-411a-8953-316a1aaf7742",
|
||||
voucher_set_id: "9d314daa-0878-4b73-922d-698047640cf4",
|
||||
redeemed_by_user_id: 8,
|
||||
redeemed_by_team_id: 4,
|
||||
redeemed_amount: 1,
|
||||
is_test: 1,
|
||||
updated_at: "2024-10-21T03:07:09.000000Z",
|
||||
created_at: "2024-10-21T03:07:09.000000Z",
|
||||
id: 5
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before { add_voucher(vine_voucher) }
|
||||
|
||||
it "redeems the voucher with VINE" do
|
||||
# Order pre discount total is $10, so we expect to redeen 1000 cents
|
||||
expect(vine_api_service).to receive(:voucher_redemptions)
|
||||
.with(voucher_id, voucher_set_id, 1000)
|
||||
.and_return(mock_api_response(data:))
|
||||
|
||||
voucher_redeemer_service.redeem
|
||||
end
|
||||
|
||||
it "closes the linked assement" do
|
||||
allow(vine_api_service).to receive(:voucher_redemptions)
|
||||
.and_return(mock_api_response(data:))
|
||||
|
||||
expect {
|
||||
voucher_redeemer_service.redeem
|
||||
}.to change { order.voucher_adjustments.first.state }.to("closed")
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
allow(vine_api_service).to receive(:voucher_redemptions)
|
||||
.and_return(mock_api_response(data:))
|
||||
|
||||
expect(voucher_redeemer_service.redeem).to be(true)
|
||||
end
|
||||
|
||||
context "when redeeming fails" do
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 400, limit: 50, offset: 0, message: "Invalid merchant team." },
|
||||
data: []
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
before do
|
||||
mock_api_exception(type: Faraday::BadRequestError, status: 400, body: data)
|
||||
end
|
||||
|
||||
it "doesn't close the linked assement" do
|
||||
expect {
|
||||
voucher_redeemer_service.redeem
|
||||
}.not_to change { order.voucher_adjustments.first.state }
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(voucher_redeemer_service.redeem).to be(false)
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
voucher_redeemer_service.redeem
|
||||
|
||||
expect(voucher_redeemer_service.errors).to include(
|
||||
{ redeeming_failed: "Redeeming the voucher failed" }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when distributor is not connected to VINE" do
|
||||
before { add_voucher(vine_voucher) }
|
||||
|
||||
it "returns false" do
|
||||
expect(voucher_redeemer_service.redeem).to be(false)
|
||||
end
|
||||
|
||||
it "doesn't redeem the VINE API" do
|
||||
expect(vine_api_service).not_to receive(:voucher_redemptions)
|
||||
|
||||
voucher_redeemer_service.redeem
|
||||
end
|
||||
|
||||
it "doesn't close the linked assement" do
|
||||
expect {
|
||||
voucher_redeemer_service.redeem
|
||||
}.not_to change { order.voucher_adjustments.first.state }
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are no voucher added to the order" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
|
||||
it "returns true" do
|
||||
expect(voucher_redeemer_service.redeem).to be(true)
|
||||
end
|
||||
|
||||
it "doesn't redeem the VINE API" do
|
||||
expect(vine_api_service).not_to receive(:voucher_redemptions)
|
||||
|
||||
voucher_redeemer_service.redeem
|
||||
end
|
||||
end
|
||||
|
||||
context "with a non vine voucher" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:voucher) { create(:voucher_flat_rate, enterprise: distributor) }
|
||||
|
||||
before { add_voucher(voucher) }
|
||||
|
||||
it "returns true" do
|
||||
expect(voucher_redeemer_service.redeem).to be(true)
|
||||
end
|
||||
|
||||
it "doesn't redeem the VINE API" do
|
||||
expect(vine_api_service).not_to receive(:voucher_redemptions)
|
||||
|
||||
voucher_redeemer_service.redeem
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an API error" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
|
||||
before do
|
||||
add_voucher(vine_voucher)
|
||||
mock_api_exception(type: Faraday::ConnectionFailed)
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(voucher_redeemer_service.redeem).to be(false)
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
voucher_redeemer_service.redeem
|
||||
|
||||
expect(voucher_redeemer_service.errors).to include(
|
||||
{ vine_api: "There was an error communicating with the API, please try again later." }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't close the linked assement" do
|
||||
expect {
|
||||
voucher_redeemer_service.redeem
|
||||
}.not_to change { order.voucher_adjustments.first.state }
|
||||
end
|
||||
|
||||
it "logs the error and notify bugsnag" do
|
||||
expect(Rails.logger).to receive(:error)
|
||||
expect(Bugsnag).to receive(:notify)
|
||||
|
||||
voucher_redeemer_service.redeem
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an API authentication error" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { numRecords: 0, totalRows: 0, responseCode: 401,
|
||||
message: "Incorrect authorization signature." },
|
||||
data: []
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
add_voucher(vine_voucher)
|
||||
|
||||
mock_api_exception(type: Faraday::UnauthorizedError, status: 401, body: data)
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(voucher_redeemer_service.redeem).to be(false)
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
voucher_redeemer_service.redeem
|
||||
|
||||
expect(voucher_redeemer_service.errors).to include(
|
||||
{ vine_api: "There was an error communicating with the API" }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't close the linked assement" do
|
||||
expect {
|
||||
voucher_redeemer_service.redeem
|
||||
}.not_to change { order.voucher_adjustments.first.state }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_voucher(voucher)
|
||||
voucher.create_adjustment(voucher.code, order)
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
end
|
||||
|
||||
def mock_api_response(data: nil)
|
||||
mock_response = instance_double(Faraday::Response)
|
||||
if data.present?
|
||||
allow(mock_response).to receive(:body).and_return(data)
|
||||
end
|
||||
mock_response
|
||||
end
|
||||
|
||||
def mock_api_exception(type: Faraday::Error, status: 503, body: nil)
|
||||
allow(vine_api_service).to receive(:voucher_redemptions).and_raise(type.new(nil,
|
||||
{ status:,
|
||||
body: }) )
|
||||
end
|
||||
end
|
||||
446
spec/services/vine/voucher_validator_service_spec.rb
Normal file
446
spec/services/vine/voucher_validator_service_spec.rb
Normal file
@@ -0,0 +1,446 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Vine::VoucherValidatorService, feature: :connected_apps do
|
||||
subject(:validate_voucher_service) { described_class.new(voucher_code:, enterprise: distributor) }
|
||||
|
||||
let(:voucher_code) { "good_code" }
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:vine_api_service) { instance_double(Vine::ApiService) }
|
||||
|
||||
before do
|
||||
allow(Vine::ApiService).to receive(:new).and_return(vine_api_service)
|
||||
end
|
||||
|
||||
describe "#validate" do
|
||||
context "with a valid voucher" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: vine_voucher_id,
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: vine_voucher_set_id,
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: 500,
|
||||
num_voucher_redemptions: 0,
|
||||
last_redemption_at: "null",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
let(:vine_voucher_id) { "9d2437c8-4559-4dda-802e-8d9c642a0c1d" }
|
||||
let(:vine_voucher_set_id) { "9d24349c-1fe8-4090-988b-d7355ed32559" }
|
||||
|
||||
it "verifies the voucher with VINE API" do
|
||||
expect(vine_api_service).to receive(:voucher_validation).and_return(
|
||||
mock_api_response(data:)
|
||||
)
|
||||
|
||||
validate_voucher_service.validate
|
||||
end
|
||||
|
||||
it "creates a new VINE voucher" do
|
||||
allow(vine_api_service).to receive(:voucher_validation).and_return(mock_api_response(data:))
|
||||
|
||||
vine_voucher = validate_voucher_service.validate
|
||||
|
||||
expect(vine_voucher).not_to be_nil
|
||||
expect(vine_voucher).to be_a(Vouchers::Vine)
|
||||
expect(vine_voucher.code).to eq(voucher_code)
|
||||
expect(vine_voucher.amount).to eq(5.00)
|
||||
expect(vine_voucher.external_voucher_id).to eq(vine_voucher_id)
|
||||
expect(vine_voucher.external_voucher_set_id).to eq(vine_voucher_set_id)
|
||||
end
|
||||
|
||||
context "when the VINE voucher has already been used by another enterprise" do
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: vine_voucher_id,
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: vine_voucher_set_id,
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: 250,
|
||||
num_voucher_redemptions: 0,
|
||||
last_redemption_at: "null",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
it "creates a new voucher" do
|
||||
existing_voucher = create(:vine_voucher, enterprise: create(:enterprise),
|
||||
code: voucher_code,
|
||||
external_voucher_id: vine_voucher_id,
|
||||
external_voucher_set_id: vine_voucher_set_id)
|
||||
allow(vine_api_service).to receive(:voucher_validation)
|
||||
.and_return(mock_api_response(data:))
|
||||
|
||||
vine_voucher = validate_voucher_service.validate
|
||||
|
||||
expect(vine_voucher.id).not_to eq(existing_voucher.id)
|
||||
expect(vine_voucher.enterprise).to eq(distributor)
|
||||
expect(vine_voucher.code).to eq(voucher_code)
|
||||
expect(vine_voucher.amount).to eq(2.50)
|
||||
expect(vine_voucher).to be_a(Vouchers::Vine)
|
||||
expect(vine_voucher.external_voucher_id).to eq(vine_voucher_id)
|
||||
expect(vine_voucher.external_voucher_set_id).to eq(vine_voucher_set_id)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a recycled code" do
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: new_vine_voucher_id,
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: new_vine_voucher_set_id,
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: 140,
|
||||
num_voucher_redemptions: 0,
|
||||
last_redemption_at: "null",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
let(:new_vine_voucher_id) { "9d2437c8-4559-4dda-802e-8d9c642a0c5e" }
|
||||
let(:new_vine_voucher_set_id) { "9d24349c-1fe8-4090-988b-d7355ed32590" }
|
||||
|
||||
it "creates a new voucher" do
|
||||
existing_voucher = create(:vine_voucher, enterprise: distributor, code: voucher_code,
|
||||
external_voucher_id: vine_voucher_id,
|
||||
external_voucher_set_id: vine_voucher_set_id)
|
||||
allow(vine_api_service).to receive(:voucher_validation)
|
||||
.and_return(mock_api_response(data:))
|
||||
|
||||
vine_voucher = validate_voucher_service.validate
|
||||
|
||||
expect(vine_voucher.id).not_to eq(existing_voucher.id)
|
||||
expect(vine_voucher.enterprise).to eq(distributor)
|
||||
expect(vine_voucher.code).to eq(voucher_code)
|
||||
expect(vine_voucher.amount).to eq(1.40)
|
||||
expect(vine_voucher).to be_a(Vouchers::Vine)
|
||||
expect(vine_voucher.external_voucher_id).to eq(new_vine_voucher_id)
|
||||
expect(vine_voucher.external_voucher_set_id).to eq(new_vine_voucher_set_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when distributor is not connected to VINE" do
|
||||
it "returns nil" do
|
||||
expect_validate_to_be_nil
|
||||
end
|
||||
|
||||
it "doesn't call the VINE API" do
|
||||
expect(vine_api_service).not_to receive(:voucher_validation)
|
||||
|
||||
validate_voucher_service.validate
|
||||
end
|
||||
|
||||
it "doesn't creates a new VINE voucher" do
|
||||
expect_voucher_count_not_to_change
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an API error" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234567", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
|
||||
before do
|
||||
mock_api_exception(type: Faraday::ConnectionFailed)
|
||||
end
|
||||
|
||||
it "returns nil" do
|
||||
expect_validate_to_be_nil
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
validate_voucher_service.validate
|
||||
|
||||
expect(validate_voucher_service.errors).to include(
|
||||
{ vine_api: "There was an error communicating with the API, please try again later." }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't creates a new VINE voucher" do
|
||||
expect_voucher_count_not_to_change
|
||||
end
|
||||
|
||||
it "logs the error and notify bugsnag" do
|
||||
expect(Rails.logger).to receive(:error)
|
||||
expect(Bugsnag).to receive(:notify)
|
||||
|
||||
validate_voucher_service.validate
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an API authentication error" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234567", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { numRecords: 0, totalRows: 0, responseCode: 401,
|
||||
message: "Incorrect authorization signature." },
|
||||
data: []
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
mock_api_exception(type: Faraday::UnauthorizedError, status: 401, body: data)
|
||||
end
|
||||
|
||||
it "returns nil" do
|
||||
expect_validate_to_be_nil
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
validate_voucher_service.validate
|
||||
|
||||
expect(validate_voucher_service.errors).to include(
|
||||
{ vine_api: "There was an error communicating with the API, please try again later." }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't creates a new VINE voucher" do
|
||||
expect_voucher_count_not_to_change
|
||||
end
|
||||
end
|
||||
|
||||
context "when the voucher doesn't exist" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 404, limit: 50, offset: 0, message: "Not found" },
|
||||
data: []
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
mock_api_exception(type: Faraday::ResourceNotFound, status: 404, body: data)
|
||||
end
|
||||
|
||||
it "returns nil" do
|
||||
expect_validate_to_be_nil
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
validate_voucher_service.validate
|
||||
|
||||
expect(validate_voucher_service.errors).to include(
|
||||
{ not_found_voucher: "Sorry, we couldn't find that voucher, please check the code." }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't creates a new VINE voucher" do
|
||||
expect_voucher_count_not_to_change
|
||||
end
|
||||
end
|
||||
|
||||
context "when the voucher is an invalid voucher" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 400, limit: 50, offset: 0, message: "Invalid merchant team." },
|
||||
data: []
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
mock_api_exception(type: Faraday::BadRequestError, status: 400, body: data)
|
||||
end
|
||||
|
||||
it "returns nil" do
|
||||
expect_validate_to_be_nil
|
||||
end
|
||||
|
||||
it "adds an error message" do
|
||||
validate_voucher_service.validate
|
||||
|
||||
expect(validate_voucher_service.errors).to include(
|
||||
{ invalid_voucher: "The voucher is not valid" }
|
||||
)
|
||||
end
|
||||
|
||||
it "doesn't creates a new VINE voucher" do
|
||||
expect_voucher_count_not_to_change
|
||||
end
|
||||
end
|
||||
|
||||
context "when creating a new voucher fails" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: "9d2437c8-4559-4dda-802e-8d9c642a0c1d",
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: "9d24349c-1fe8-4090-988b-d7355ed32559",
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: '',
|
||||
num_voucher_redemptions: 0,
|
||||
last_redemption_at: "null",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
allow(vine_api_service).to receive(:voucher_validation).and_return(
|
||||
mock_api_response(data: )
|
||||
)
|
||||
end
|
||||
|
||||
it "returns an invalid voucher" do
|
||||
voucher = validate_voucher_service.validate
|
||||
expect(voucher).not_to be_valid
|
||||
expect(voucher.errors[:amount]).to include "must be greater than 0"
|
||||
end
|
||||
end
|
||||
|
||||
context "with an existing voucher" do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234567", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let!(:voucher) {
|
||||
create(:vine_voucher, enterprise: distributor, code: voucher_code, amount: 500,
|
||||
external_voucher_id: vine_voucher_id,
|
||||
external_voucher_set_id: "9d24349c-1fe8-4090-988b-d7355ed32559")
|
||||
}
|
||||
let(:vine_voucher_id) { "9d2437c8-4559-4dda-802e-8d9c642a0c1d" }
|
||||
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: vine_voucher_id,
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: "9d24349c-1fe8-4090-988b-d7355ed32559",
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: 250,
|
||||
num_voucher_redemptions: 1,
|
||||
last_redemption_at: "2024-10-05T13:20:02.000000Z",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
before do
|
||||
allow(vine_api_service).to receive(:voucher_validation).and_return(
|
||||
mock_api_response(data: )
|
||||
)
|
||||
end
|
||||
|
||||
it "verify the voucher with VINE API" do
|
||||
expect(vine_api_service).to receive(:voucher_validation).and_return(
|
||||
mock_api_response(data: )
|
||||
)
|
||||
|
||||
validate_voucher_service.validate
|
||||
end
|
||||
|
||||
it "updates the VINE voucher" do
|
||||
vine_voucher = validate_voucher_service.validate
|
||||
|
||||
expect(vine_voucher.id).to eq(voucher.id)
|
||||
expect(vine_voucher.reload.amount).to eq(2.50)
|
||||
end
|
||||
|
||||
context "when updating the voucher fails" do
|
||||
let(:data) {
|
||||
{
|
||||
meta: { responseCode: 200, limit: 50, offset: 0, message: "" },
|
||||
data: {
|
||||
id: "9d2437c8-4559-4dda-802e-8d9c642a0c1d",
|
||||
voucher_short_code: voucher_code,
|
||||
voucher_set_id: "9d24349c-1fe8-4090-988b-d7355ed32559",
|
||||
is_test: 1,
|
||||
voucher_value_original: 500,
|
||||
voucher_value_remaining: '',
|
||||
num_voucher_redemptions: 0,
|
||||
last_redemption_at: "null",
|
||||
created_at: "2024-10-01T13:20:02.000000Z",
|
||||
updated_at: "2024-10-01T13:20:02.000000Z",
|
||||
deleted_at: "null"
|
||||
}
|
||||
}.deep_stringify_keys
|
||||
}
|
||||
|
||||
it "returns an invalid voucher" do
|
||||
vine_voucher = validate_voucher_service.validate
|
||||
expect(vine_voucher).not_to be_valid
|
||||
end
|
||||
|
||||
it "doesn't update existing voucher" do
|
||||
expect {
|
||||
validate_voucher_service.validate
|
||||
}.not_to change { voucher.reload.amount }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_validate_to_be_nil
|
||||
expect(validate_voucher_service.validate).to be_nil
|
||||
end
|
||||
|
||||
def expect_voucher_count_not_to_change
|
||||
expect { validate_voucher_service.validate }.not_to change { Voucher.count }
|
||||
end
|
||||
|
||||
def mock_api_response(data: nil)
|
||||
mock_response = instance_double(Faraday::Response)
|
||||
if data.present?
|
||||
allow(mock_response).to receive(:body).and_return(data)
|
||||
end
|
||||
mock_response
|
||||
end
|
||||
|
||||
def mock_api_exception(type: Faraday::Error, status: 503, body: nil)
|
||||
allow(vine_api_service).to receive(:voucher_validation).and_raise(type.new(nil,
|
||||
{ status:, body: }) )
|
||||
end
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe VineApiService do
|
||||
subject(:vine_api) { described_class.new(api_key: vine_api_key, jwt_generator: jwt_service) }
|
||||
|
||||
let(:vine_api_url) { "https://vine-staging.openfoodnetwork.org.au/api/v1" }
|
||||
let(:vine_api_key) { "12345" }
|
||||
let(:jwt_service) { VineJwtService.new(secret:) }
|
||||
let(:secret) { "my_secret" }
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with("VINE_API_URL").and_return(vine_api_url)
|
||||
end
|
||||
|
||||
describe "#my_team" do
|
||||
let(:my_team_url) { "#{vine_api_url}/my-team" }
|
||||
|
||||
it "send a request to the team VINE api endpoint" do
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect(a_request(
|
||||
:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team"
|
||||
)).to have_been_made
|
||||
end
|
||||
|
||||
it "sends the VINE api key via a header" do
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect(a_request(:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team").with(
|
||||
headers: { Authorization: "Bearer #{vine_api_key}" }
|
||||
)).to have_been_made
|
||||
end
|
||||
|
||||
it "sends JWT token via a header" do
|
||||
token = "some.jwt.token"
|
||||
stub_request(:get, my_team_url).to_return(status: 200)
|
||||
|
||||
allow(jwt_service).to receive(:generate_token).and_return(token)
|
||||
|
||||
vine_api.my_team
|
||||
|
||||
expect(a_request(:get, "https://vine-staging.openfoodnetwork.org.au/api/v1/my-team").with(
|
||||
headers: { 'X-Authorization': "JWT #{token}" }
|
||||
)).to have_been_made
|
||||
end
|
||||
|
||||
context "when a request succeed", :vcr do
|
||||
it "returns the response" do
|
||||
response = vine_api.my_team
|
||||
|
||||
expect(response.success?).to be(true)
|
||||
expect(response.body).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a request fails" do
|
||||
it "logs the error" do
|
||||
stub_request(:get, my_team_url).to_return(body: "error", status: 401)
|
||||
|
||||
expect(Rails.logger).to receive(:error).twice
|
||||
|
||||
response = vine_api.my_team
|
||||
|
||||
expect(response.success?).to be(false)
|
||||
end
|
||||
|
||||
it "return the response" do
|
||||
stub_request(:get, my_team_url).to_return(body: "error", status: 401)
|
||||
response = vine_api.my_team
|
||||
|
||||
expect(response.success?).to be(false)
|
||||
expect(response.body).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
39
spec/support/voucher_uniqueness_helper.rb
Normal file
39
spec/support/voucher_uniqueness_helper.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
shared_examples_for 'has a unique code per enterprise' do |voucher_type|
|
||||
describe "code" do
|
||||
let(:code) { "super_code" }
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
it "is unique per enterprise" do
|
||||
voucher = create(voucher_type, code:, enterprise:)
|
||||
expect(voucher).to be_valid
|
||||
|
||||
expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)
|
||||
|
||||
expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
|
||||
end
|
||||
|
||||
context "with deleted voucher" do
|
||||
it "is unique per enterprise" do
|
||||
create(voucher_type, code:, enterprise:).destroy!
|
||||
|
||||
expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)
|
||||
|
||||
expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expect_voucher_with_same_enterprise_to_be_invalid(voucher_type)
|
||||
new_voucher = build(voucher_type, code:, enterprise: )
|
||||
|
||||
expect(new_voucher).not_to be_valid
|
||||
expect(new_voucher.errors.full_messages).to include("Code has already been taken")
|
||||
end
|
||||
|
||||
def expect_voucher_with_other_enterprise_to_be_valid(voucher_type)
|
||||
other_voucher = build(voucher_type, code:, enterprise: create(:enterprise) )
|
||||
expect(other_voucher).to be_valid
|
||||
end
|
||||
end
|
||||
@@ -154,8 +154,6 @@ RSpec.describe "Revenues By Hub Reports" do
|
||||
order.update_shipping_fees!
|
||||
order.update_order!
|
||||
|
||||
VoucherAdjustmentsService.new(order).update
|
||||
|
||||
order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ RSpec.describe '
|
||||
|
||||
let(:enterprise) { create(:supplier_enterprise, name: 'Feedme', sells: 'own') }
|
||||
let(:voucher_code) { 'awesomevoucher' }
|
||||
let(:vine_voucher_code) { 'vine_voucher' }
|
||||
let(:amount) { 25 }
|
||||
let(:enterprise_user) { create(:user, enterprise_limit: 1) }
|
||||
|
||||
@@ -22,6 +23,7 @@ RSpec.describe '
|
||||
it 'lists enterprise vouchers' do
|
||||
# Given an enterprise with vouchers
|
||||
create(:voucher_flat_rate, enterprise:, code: voucher_code, amount:)
|
||||
create(:vine_voucher, enterprise:, code: vine_voucher_code, amount:)
|
||||
|
||||
# When I go to the enterprise voucher tab
|
||||
visit edit_admin_enterprise_path(enterprise)
|
||||
@@ -31,6 +33,8 @@ RSpec.describe '
|
||||
# Then I see a list of vouchers
|
||||
expect(page).to have_content voucher_code
|
||||
expect(page).to have_content amount
|
||||
|
||||
expect(page).not_to have_content vine_voucher_code
|
||||
end
|
||||
|
||||
describe "adding voucher" do
|
||||
|
||||
@@ -172,6 +172,37 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
expect(page).to have_content("Voucher code Not found")
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", :vcr, feature: :connected_apps do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with("VINE_API_URL").and_return("https://vine-staging.openfoodnetwork.org.au/api/v1")
|
||||
end
|
||||
|
||||
it "adds a voucher to the order" do
|
||||
apply_voucher "CI3922"
|
||||
|
||||
expect(page).to have_content "$5.00 Voucher"
|
||||
expect(order.reload.voucher_adjustments.length).to eq(1)
|
||||
expect(Vouchers::Vine.find_by(code: "CI3922",
|
||||
enterprise: distributor)).not_to be_nil
|
||||
end
|
||||
|
||||
context "with an invalid voucher" do
|
||||
it "show an error" do
|
||||
fill_in "Enter voucher code", with: "KM1891"
|
||||
click_button("Apply")
|
||||
|
||||
expect(page).to have_content("There was an error while adding the voucher")
|
||||
expect(Vouchers::Vine.find_by(code: "KM1891", enterprise: distributor)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "removing voucher from order" do
|
||||
@@ -353,7 +384,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
|
||||
def add_voucher_to_order(voucher, order)
|
||||
voucher.create_adjustment(voucher.code, order)
|
||||
VoucherAdjustmentsService.new(order).update
|
||||
order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
end
|
||||
end
|
||||
|
||||
@@ -344,6 +344,48 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a VINE voucher", feature: :connected_apps do
|
||||
let!(:vine_connected_app) {
|
||||
ConnectedApps::Vine.create(
|
||||
enterprise: distributor, data: { api_key: "1234568", secret: "my_secret" }
|
||||
)
|
||||
}
|
||||
let(:vine_voucher) {
|
||||
create(:vine_voucher, code: 'some_vine_code', enterprise: distributor, amount: 0.01)
|
||||
}
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with("VINE_API_URL")
|
||||
.and_return("https://vine-staging.openfoodnetwork.org.au/api/v1")
|
||||
|
||||
add_voucher_to_order(vine_voucher, order)
|
||||
end
|
||||
|
||||
it "shows the applied voucher" do
|
||||
visit checkout_step_path(:summary)
|
||||
|
||||
within ".summary-right" do
|
||||
expect(page).to have_content "some_vine_code"
|
||||
expect(page).to have_content "-0.01"
|
||||
end
|
||||
end
|
||||
|
||||
context "when placing the order" do
|
||||
it "completes the order", :vcr do
|
||||
visit checkout_step_path(:summary)
|
||||
|
||||
place_order
|
||||
|
||||
within "#line-items" do
|
||||
expect(page).to have_content "Voucher: some_vine_code"
|
||||
expect(page).to have_content "$-0.01"
|
||||
end
|
||||
expect(order.reload.state).to eq "complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with previous open orders" do
|
||||
@@ -393,53 +435,50 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
}
|
||||
let(:payment) { completed_order.payments.first }
|
||||
|
||||
shared_examples "order confirmation page" do |paid_state, paid_amount|
|
||||
it "displays the relevant information" do
|
||||
expect(page).to have_content paid_state.to_s
|
||||
expect(page).to have_selector('strong', text: "Amount Paid")
|
||||
expect(page).to have_selector('strong', text: with_currency(paid_amount))
|
||||
end
|
||||
end
|
||||
|
||||
context "an order with balance due" do
|
||||
before { set_up_order(-10, "balance_due") }
|
||||
|
||||
it_behaves_like "order confirmation page", "NOT PAID", "40" do
|
||||
before do
|
||||
expect(page).to have_selector('h5', text: "Balance Due")
|
||||
expect(page).to have_selector('h5', text: with_currency(10))
|
||||
end
|
||||
it "displays balance due and paid state" do
|
||||
expect(page).to have_selector('h5', text: "Balance Due")
|
||||
expect(page).to have_selector('h5', text: with_currency(10))
|
||||
|
||||
confirmation_page_expect_paid(paid_state: "NOT PAID", paid_amount: 40)
|
||||
end
|
||||
end
|
||||
|
||||
context "an order with credit owed" do
|
||||
before { set_up_order(10, "credit_owed") }
|
||||
|
||||
it_behaves_like "order confirmation page", "PAID", "60" do
|
||||
before do
|
||||
expect(page).to have_selector('h5', text: "Credit Owed")
|
||||
expect(page).to have_selector('h5', text: with_currency(-10))
|
||||
end
|
||||
it "displays Credit owned and paid state" do
|
||||
expect(page).to have_selector('h5', text: "Credit Owed")
|
||||
expect(page).to have_selector('h5', text: with_currency(-10))
|
||||
|
||||
confirmation_page_expect_paid(paid_state: "PAID", paid_amount: 60)
|
||||
end
|
||||
end
|
||||
|
||||
context "an order with no outstanding balance" do
|
||||
before { set_up_order(0, "paid") }
|
||||
|
||||
it_behaves_like "order confirmation page", "PAID", "50" do
|
||||
before do
|
||||
expect(page).not_to have_selector('h5', text: "Credit Owed")
|
||||
expect(page).not_to have_selector('h5', text: "Balance Due")
|
||||
end
|
||||
it "displays paid state" do
|
||||
expect(page).not_to have_selector('h5', text: "Credit Owed")
|
||||
expect(page).not_to have_selector('h5', text: "Balance Due")
|
||||
|
||||
confirmation_page_expect_paid(paid_state: "PAID", paid_amount: 50)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def confirmation_page_expect_paid(paid_state:, paid_amount:)
|
||||
expect(page).to have_content paid_state.to_s
|
||||
expect(page).to have_selector('strong', text: "Amount Paid")
|
||||
expect(page).to have_selector('strong', text: with_currency(paid_amount))
|
||||
end
|
||||
|
||||
def add_voucher_to_order(voucher, order)
|
||||
voucher.create_adjustment(voucher.code, order)
|
||||
VoucherAdjustmentsService.new(order).update
|
||||
order.update_totals_and_states
|
||||
OrderManagement::Order::Updater.new(order).update_voucher
|
||||
end
|
||||
|
||||
def set_up_order(balance, state)
|
||||
|
||||
Reference in New Issue
Block a user