├── .ruby-version ├── .rubocop.yml ├── parking_payment_system.png ├── parking_information_system.png ├── models ├── user.rb ├── product.rb ├── order.rb ├── promotion_log.rb └── promotion.rb ├── README.md ├── Gemfile ├── services ├── promotion │ ├── base_promotion_service.rb │ ├── order_over_amount_free_product_promotion_service.rb │ ├── order_over_amount_percent_off_promotion_service.rb │ ├── order_over_amount_discount_amount_promotion_service.rb │ ├── multiple_products_discount_amount_promotion_service.rb │ ├── order_over_amount_discount_amount_monthly_max_promotion_service.rb │ └── order_over_amount_percent_off_max_discount_per_person_promotion_service.rb ├── calculation_service.rb └── applying_promotion_service.rb ├── spec ├── models │ ├── user_spec.rb │ ├── product_spec.rb │ └── promotion_spec.rb └── services │ └── calculation_service_spec.rb └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.1 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | 4 | Style/Documentation: 5 | Enabled: false 6 | -------------------------------------------------------------------------------- /parking_payment_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmy2822/autopass_work/HEAD/parking_payment_system.png -------------------------------------------------------------------------------- /parking_information_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmy2822/autopass_work/HEAD/parking_information_system.png -------------------------------------------------------------------------------- /models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User 4 | attr_accessor :id, :name 5 | 6 | def initialize(id:, name:) 7 | @id = id 8 | @name = name 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /models/product.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Product 4 | attr_accessor :id, :name, :price 5 | 6 | def initialize(id:, name:, price:) 7 | @id = id 8 | @name = name 9 | @price = price 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Environment 2 | * Ruby: `3.0.1` 3 | 4 | # Review Guide 5 | * `models`: 存放各別模型 6 | * `services`: 存放計算 service 與折價相關邏輯的 promotion service 7 | * `spec`: 存放相關測試,主要測試在檔案 `spec/services/calculation_service_spec.rb` 8 | * 根目錄存放兩張 `.png` 圖檔為系統架構設計答題內容 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Ruby Linter 8 | gem 'rubocop', require: false 9 | 10 | group :test, :development do 11 | gem 'faker', '~> 2.2' 12 | gem 'rspec', '~> 3.11' 13 | end 14 | -------------------------------------------------------------------------------- /models/order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Order 4 | attr_accessor :discount_amount 5 | attr_reader :id, :user_id, :products 6 | 7 | def initialize(id:, user_id:, products:) 8 | @id = id 9 | @user_id = user_id 10 | @products = products 11 | end 12 | 13 | def origin_amount 14 | @products.sum(&:price) 15 | end 16 | 17 | def result_amount 18 | @origin_amount - @discount_amount 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /models/promotion_log.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'date' 4 | 5 | class PromotionLog 6 | attr_accessor :id, :user_id, :promotion_id, :discount_amount, :created_at 7 | 8 | def initialize(id:, user_id:, promotion_id:, discount_amount:, created_at: DateTime.now) 9 | @id = id 10 | @user_id = user_id 11 | @promotion_id = promotion_id 12 | @discount_amount = discount_amount 13 | @created_at = created_at 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /services/promotion/base_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BasePromotionService 4 | attr_reader :user_id, :products, :promotion, :discount_amount, :promotion_logs 5 | 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | @user_id = user_id 8 | @products = products 9 | @promotion = promotion 10 | @promotion_logs = promotion_logs 11 | end 12 | 13 | def perform 14 | raise NotImplementedError 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faker' 4 | require_relative '../../models/user' 5 | 6 | RSpec.describe User, type: :model do 7 | describe 'Has columns' do 8 | subject { User.new(id: id, name: name) } 9 | 10 | let(:id) { 1 } 11 | let(:name) { Faker::Name.name } 12 | 13 | it 'has column id' do 14 | expect(subject.id).to eq(1) 15 | end 16 | 17 | it 'has column name' do 18 | expect(subject.name).to eq(name) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/models/product_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faker' 4 | require_relative '../../models/product' 5 | 6 | RSpec.describe Product, type: :model do 7 | describe 'Has columns' do 8 | subject { Product.new(id: id, name: name, price: price) } 9 | 10 | let(:id) { 1 } 11 | let(:name) { Faker::Commerce.product_name } 12 | let(:price) { Faker::Commerce.price } 13 | 14 | it 'has column id' do 15 | expect(subject.id).to eq(1) 16 | end 17 | 18 | it 'has column name' do 19 | expect(subject.name).to eq(name) 20 | end 21 | 22 | it 'has column price' do 23 | expect(subject.name).to eq(name) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /models/promotion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Promotion 4 | attr_accessor :id, :name, :code, :promotion_target_type, 5 | :options 6 | attr_reader :quantity 7 | 8 | def initialize(id:, name:, code:, promotion_target_type:, 9 | quantity: nil, options: {}) 10 | @id = id 11 | @name = name 12 | @code = code 13 | @promotion_target_type = promotion_target_type 14 | @quantity = quantity 15 | @options = options 16 | end 17 | 18 | def limited_quantity? 19 | quantity != nil 20 | end 21 | 22 | def sufficient_quantity? 23 | limited_quantity? && quantity.positive? 24 | end 25 | 26 | def quantity=(amount) 27 | raise 'Quantity can not be less than zero' if amount.negative? 28 | 29 | @quantity = amount 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /services/promotion/order_over_amount_free_product_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class OrderOverAmountFreeProductPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | end 9 | 10 | def perform 11 | return false unless qualified_amount? 12 | 13 | calculate_discount_amount 14 | true 15 | end 16 | 17 | private 18 | 19 | def qualified_amount? 20 | origin_amount >= @promotion.options[:over_amount] 21 | end 22 | 23 | def origin_amount 24 | @origin_amount ||= @products.map(&:price).sum 25 | end 26 | 27 | def calculate_discount_amount 28 | @discount_amount = 0 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /services/promotion/order_over_amount_percent_off_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class OrderOverAmountPercentOffPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | end 9 | 10 | def perform 11 | return false unless qualified_amount? 12 | 13 | calculate_discount_amount 14 | true 15 | end 16 | 17 | private 18 | 19 | def qualified_amount? 20 | origin_amount >= @promotion.options[:over_amount] 21 | end 22 | 23 | def origin_amount 24 | @origin_amount ||= @products.map(&:price).sum 25 | end 26 | 27 | def calculate_discount_amount 28 | @discount_amount = @origin_amount - (@origin_amount * (1 - (@promotion.options[:percent_off].to_f / 100))) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /services/promotion/order_over_amount_discount_amount_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class OrderOverAmountDiscountAmountPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | end 9 | 10 | def perform 11 | return false unless qualified_origin_amount? 12 | return false unless promotion.sufficient_quantity? 13 | 14 | process_promotion_quantity 15 | calculate_discount_amount 16 | true 17 | end 18 | 19 | private 20 | 21 | def process_promotion_quantity 22 | @promotion.quantity -= 1 23 | end 24 | 25 | def qualified_origin_amount? 26 | @products.map(&:price).sum >= @promotion.options[:over_amount] 27 | end 28 | 29 | def calculate_discount_amount 30 | @discount_amount = @promotion.options[:discount_amount] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/models/promotion_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faker' 4 | require_relative '../../models/promotion' 5 | 6 | RSpec.describe Promotion, type: :model do 7 | describe 'Has columns' do 8 | subject do 9 | Promotion.new(id: id, name: name, code: code, 10 | promotion_target_type: promotion_target_type, 11 | quantity: quantity, options: options) 12 | end 13 | 14 | let(:id) { 1 } 15 | let(:name) { Faker::Commerce.product_name } 16 | let(:code) { Faker::Code.isbn } 17 | let(:promotion_target_type) { Faker::Beer.name } 18 | let(:quantity) { 1 } 19 | let(:options) { { x: 1 } } 20 | 21 | it 'has column id' do 22 | expect(subject.id).to eq(1) 23 | end 24 | 25 | it 'has column name' do 26 | expect(subject.name).to eq(name) 27 | end 28 | 29 | it 'has column code' do 30 | expect(subject.code).to eq(code) 31 | end 32 | 33 | it 'has column quantity' do 34 | expect(subject.quantity).to eq(1) 35 | end 36 | 37 | it 'has column options' do 38 | expect(subject.options[:x]).to eq(1) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /services/promotion/multiple_products_discount_amount_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class MultipleProductsDiscountAmountPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | @qualified_products = [] 9 | end 10 | 11 | def perform 12 | retrieve_qualified_products 13 | 14 | return false if @qualified_products.count.zero? 15 | 16 | calculate_discount_amount 17 | true 18 | end 19 | 20 | private 21 | 22 | def retrieve_qualified_products 23 | @products.each do |product| 24 | next unless promotion_product?(product.id) && qualified_quantity?(product.id) 25 | 26 | @qualified_products << product 27 | end 28 | end 29 | 30 | def promotion_product?(product_id) 31 | @promotion.options[:promotion_product_ids].include?(product_id) 32 | end 33 | 34 | def qualified_quantity?(product_id) 35 | @products.select { |product| product.id == product_id }.count >= @promotion.options[:quantity] 36 | end 37 | 38 | def calculate_discount_amount 39 | @discount_amount = @qualified_products.map(&:id).uniq.count * @promotion.options[:discount_amount] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /services/calculation_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './applying_promotion_service' 4 | 5 | class CalculationService 6 | attr_reader :order, :promotions, :promotion_logs, :discount_amount, :result_amount, 7 | :applied_promotions, :promotion_free_products 8 | 9 | def initialize(order:, promotions: [], promotion_logs: []) 10 | @order = order 11 | @promotions = promotions 12 | @promotion_logs = promotion_logs 13 | @discount_amount = 0 14 | @applied_promotions = [] 15 | @promotion_free_products = [] 16 | end 17 | 18 | def perform 19 | apply_promotions 20 | calculate_result_amount 21 | end 22 | 23 | def origin_amount 24 | @order.origin_amount 25 | end 26 | 27 | private 28 | 29 | def apply_promotions 30 | applying_promotion_service = ApplyingPromotionService.new(order: @order, promotions: @promotions, promotion_logs: @promotion_logs) 31 | return unless applying_promotion_service.perform 32 | 33 | @discount_amount += applying_promotion_service.discount_amount 34 | @applied_promotions.concat(applying_promotion_service.applied_promotions) 35 | @promotion_free_products.concat(applying_promotion_service.promotion_free_products) 36 | end 37 | 38 | 39 | def calculate_result_amount 40 | @result_amount = @order.origin_amount - @discount_amount 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /services/promotion/order_over_amount_discount_amount_monthly_max_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class OrderOverAmountDiscountAmountMonthlyMaxPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | end 9 | 10 | def perform 11 | return false unless qualified_over_amount? 12 | return false unless qualified_monthly_max_amount? 13 | 14 | calculate_discount_amount 15 | true 16 | end 17 | 18 | private 19 | 20 | def qualified_over_amount? 21 | origin_amount >= @promotion.options[:over_amount] 22 | end 23 | 24 | def qualified_monthly_max_amount? 25 | applied_promotion_logs.sum(&:discount_amount) + present_discount_amount <= @promotion.options[:monthly_max_amount] 26 | end 27 | 28 | def present_discount_amount 29 | @promotion.options[:discount_amount] 30 | end 31 | 32 | def origin_amount 33 | @origin_amount ||= @products.map(&:price).sum 34 | end 35 | 36 | def calculate_discount_amount 37 | @discount_amount = present_discount_amount 38 | end 39 | 40 | def applied_promotion_logs 41 | @promotion_logs.select { |log| log.promotion_id == @promotion.id && log.created_at.month == DateTime.now.month } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | concurrent-ruby (1.1.10) 6 | diff-lcs (1.5.0) 7 | faker (2.20.0) 8 | i18n (>= 1.8.11, < 2) 9 | i18n (1.10.0) 10 | concurrent-ruby (~> 1.0) 11 | parallel (1.22.1) 12 | parser (3.1.1.0) 13 | ast (~> 2.4.1) 14 | rainbow (3.1.1) 15 | regexp_parser (2.3.0) 16 | rexml (3.2.5) 17 | rspec (3.11.0) 18 | rspec-core (~> 3.11.0) 19 | rspec-expectations (~> 3.11.0) 20 | rspec-mocks (~> 3.11.0) 21 | rspec-core (3.11.0) 22 | rspec-support (~> 3.11.0) 23 | rspec-expectations (3.11.0) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.11.0) 26 | rspec-mocks (3.11.1) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.11.0) 29 | rspec-support (3.11.0) 30 | rubocop (1.27.0) 31 | parallel (~> 1.10) 32 | parser (>= 3.1.0.0) 33 | rainbow (>= 2.2.2, < 4.0) 34 | regexp_parser (>= 1.8, < 3.0) 35 | rexml 36 | rubocop-ast (>= 1.16.0, < 2.0) 37 | ruby-progressbar (~> 1.7) 38 | unicode-display_width (>= 1.4.0, < 3.0) 39 | rubocop-ast (1.17.0) 40 | parser (>= 3.1.1.0) 41 | ruby-progressbar (1.11.0) 42 | unicode-display_width (2.1.0) 43 | 44 | PLATFORMS 45 | arm64-darwin-20 46 | 47 | DEPENDENCIES 48 | faker (~> 2.2) 49 | rspec (~> 3.11) 50 | rubocop 51 | 52 | BUNDLED WITH 53 | 2.2.15 54 | -------------------------------------------------------------------------------- /services/promotion/order_over_amount_percent_off_max_discount_per_person_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './base_promotion_service' 4 | 5 | class OrderOverAmountPercentOffMaxDiscountPerPersonPromotionService < BasePromotionService 6 | def initialize(user_id:, products:, promotion:, promotion_logs:) 7 | super(user_id: user_id, products: products, promotion: promotion, promotion_logs: promotion_logs) 8 | end 9 | 10 | def perform 11 | return false unless qualified_over_amount? 12 | return false unless qualified_per_person_max_amount? 13 | 14 | calculate_discount_amount 15 | true 16 | end 17 | 18 | private 19 | 20 | def qualified_over_amount? 21 | origin_amount >= @promotion.options[:over_amount] 22 | end 23 | 24 | def qualified_per_person_max_amount? 25 | user_applied_promotion_logs.sum(&:discount_amount) + present_discount_amount <= @promotion.options[:max_discount_amount] 26 | end 27 | 28 | def present_discount_amount 29 | @origin_amount - (@origin_amount * (1 - (@promotion.options[:percent_off].to_f / 100))) 30 | end 31 | 32 | def origin_amount 33 | @origin_amount ||= @products.map(&:price).sum 34 | end 35 | 36 | def calculate_discount_amount 37 | @discount_amount = present_discount_amount 38 | end 39 | 40 | def user_applied_promotion_logs 41 | @promotion_logs.select { |log| log.user_id == @user_id && log.promotion_id == @promotion.id } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /services/applying_promotion_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | require_relative '../models/promotion' 6 | require_relative './promotion/multiple_products_discount_amount_promotion_service' 7 | require_relative './promotion/order_over_amount_percent_off_promotion_service' 8 | require_relative './promotion/order_over_amount_free_product_promotion_service' 9 | require_relative './promotion/order_over_amount_discount_amount_promotion_service' 10 | require_relative './promotion/order_over_amount_percent_off_max_discount_per_person_promotion_service' 11 | require_relative './promotion/order_over_amount_discount_amount_monthly_max_promotion_service' 12 | 13 | class ApplyingPromotionService 14 | attr_reader :discount_amount, :result_amount, :promotions, 15 | :applied_promotions, :promotion_free_products, :products 16 | 17 | def initialize(order:, promotions:, promotion_logs:) 18 | @order = order 19 | @promotions = promotions 20 | @promotion_logs = promotion_logs 21 | @discount_amount = 0 22 | @result_amount = 0 23 | @applied_promotions = [] 24 | @promotion_free_products = [] 25 | @logger = Logger.new($stdout) 26 | end 27 | 28 | def perform 29 | apply_promotions 30 | calculate_result_amount 31 | end 32 | 33 | private 34 | 35 | def get_promotion_service_class(code) 36 | service_name = code.split('_').collect!(&:capitalize).join + 'PromotionService' 37 | Object.const_get(service_name) 38 | end 39 | 40 | def calculate_result_amount 41 | @order.origin_amount - @discount_amount 42 | end 43 | 44 | def apply_promotions 45 | @promotions.each do |promotion| 46 | promotion_service = get_promotion_service_class(promotion.code) 47 | .new(user_id: @order.user_id, products: @order.products, 48 | promotion: promotion, promotion_logs: @promotion_logs) 49 | next unless promotion_service.perform 50 | 51 | @applied_promotions << promotion 52 | @promotion_free_products.concat(retrieve_promotion_free_products(promotion)) if free_product_promotion?(promotion) 53 | @discount_amount += promotion_service.discount_amount 54 | rescue NameError => e 55 | @logger.warn("Applying Promotion: #{promotion.name} error. reason: #{e.message}") 56 | next 57 | end 58 | end 59 | 60 | def retrieve_promotion_free_products(promotion) 61 | free_products = [] 62 | promotion.options[:free_product_quantity].times do 63 | free_products << Product.new(id: promotion.options[:free_product_id], name: 'Free Promotion Product', price: 0) 64 | end 65 | free_products 66 | end 67 | 68 | def free_product_promotion?(promotion) 69 | promotion.options.keys.include?(:free_product_id) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/services/calculation_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'faker' 4 | require_relative '../../models/order' 5 | require_relative '../../services/calculation_service' 6 | require_relative '../../models/product' 7 | require_relative '../../models/promotion_log' 8 | 9 | RSpec.describe CalculationService do 10 | let(:user) { User.new(id: 1, name: 'Jimmy') } 11 | let(:order) { Order.new(id: 1, user_id: user.id, products: products ) } 12 | let(:promotion_logs) { [] } 13 | subject { CalculationService.new(order: order, promotions: promotions, promotion_logs: promotion_logs) } 14 | 15 | describe '#perform' do 16 | before { subject.perform } 17 | 18 | let(:promotions) do 19 | [ 20 | Promotion.new(id: 1, name: '特定商品滿 2 件折 100 元', code: 'multiple_products_discount_amount', 21 | promotion_target_type: 'PRODUCT', 22 | options: { promotion_product_ids: [1, 2], quantity: 2, discount_amount: 100 }), 23 | Promotion.new(id: 2, name: '訂單滿 1000 元折 10%', code: 'order_over_amount_percent_off', 24 | promotion_target_type: 'ORDER', 25 | options: { over_amount: 1000, percent_off: 10 }), 26 | Promotion.new(id: 3, name: '訂單滿 2000 元贈送面紙一盒', code: 'order_over_amount_free_product', 27 | promotion_target_type: 'ORDER', 28 | options: { over_amount: 2000, free_product_id: 3, free_product_quantity: 1 }), 29 | Promotion.new(id: 4, name: '訂單滿 10000 元可以折 2000 元,全站限用 1 次', code: 'order_over_amount_discount_amount', 30 | promotion_target_type: 'ORDER', quantity: 1, 31 | options: { over_amount: 10_000, discount_amount: 2000 }) 32 | ] 33 | end 34 | 35 | describe 'when products are not qualified any promotions' do 36 | let(:products) do 37 | [ 38 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 200) , 39 | Product.new(id: 2, name: Faker::Commerce.product_name, price: 300) 40 | ] 41 | end 42 | 43 | 44 | it 'origin_amount is equal to products price sum' do 45 | expect(subject.origin_amount).to eq(500) 46 | end 47 | 48 | it 'discount amount should be 0 if there is no promotion applying' do 49 | expect(subject.discount_amount).to eq(0) 50 | end 51 | 52 | it 'result_amount should be origin_amount' do 53 | expect(subject.result_amount).to eq(500) 54 | end 55 | end 56 | 57 | describe 'when products are qualified to apply multiple products discount promotion' do 58 | let(:products) do 59 | [ 60 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 300), 61 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 300) 62 | ] 63 | end 64 | 65 | it 'origin_amount is equal to products price sum' do 66 | expect(subject.origin_amount).to eq(600) 67 | end 68 | 69 | it 'discount amount should be 100 due to the promotion' do 70 | expect(subject.discount_amount).to eq(100) 71 | end 72 | 73 | it 'result_amount should be discounted' do 74 | expect(subject.result_amount).to eq(500) 75 | end 76 | end 77 | 78 | describe 'when order is qualified to apply order over amount percent off promotion' do 79 | let(:products) do 80 | [ 81 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 500), 82 | Product.new(id: 2, name: Faker::Commerce.product_name, price: 500) 83 | ] 84 | end 85 | 86 | it 'origin_amount is equal to products price sum' do 87 | expect(subject.origin_amount).to eq(1000) 88 | end 89 | 90 | it 'discount amount should be 100 due to the promotion' do 91 | expect(subject.discount_amount).to eq(100) 92 | end 93 | 94 | it 'result_amount should be discounted' do 95 | expect(subject.result_amount).to eq(900) 96 | end 97 | end 98 | 99 | describe 'when order is qualified to apply order over amount free product promotion' do 100 | let(:products) do 101 | [ 102 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 1000), 103 | Product.new(id: 2, name: Faker::Commerce.product_name, price: 2000) 104 | ] 105 | end 106 | 107 | it 'origin_amount is equal to products price sum' do 108 | expect(subject.origin_amount).to eq(3000) 109 | end 110 | 111 | it 'discount amount should come from 10% off promotion' do 112 | expect(subject.discount_amount).to eq(300) 113 | end 114 | 115 | it 'result_amount should be origin_amount' do 116 | expect(subject.result_amount).to eq(2700) 117 | end 118 | 119 | it 'should get free product' do 120 | expect(subject.promotion_free_products.size).to eq(1) 121 | end 122 | 123 | it 'should get free product' do 124 | expect(subject.promotion_free_products.first.name).to eq('Free Promotion Product') 125 | end 126 | end 127 | 128 | describe 'when order is qualified to apply order over amount discount amount promotion' do 129 | let(:promotions) do 130 | [ 131 | Promotion.new(id: 4, name: '訂單滿 10000 元可以折 2000 元,全站限用 1 次', code: 'order_over_amount_discount_amount', 132 | promotion_target_type: 'ORDER', quantity: 1, 133 | options: { over_amount: 10_000, discount_amount: 2000 }) 134 | ] 135 | end 136 | let(:products) do 137 | [ 138 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 6000), 139 | Product.new(id: 2, name: Faker::Commerce.product_name, price: 8000) 140 | ] 141 | end 142 | 143 | it 'origin_amount is equal to products price sum' do 144 | expect(subject.origin_amount).to eq(14_000) 145 | end 146 | 147 | it 'discount amount should come from this promotion' do 148 | expect(subject.discount_amount).to eq(2000) 149 | end 150 | 151 | it 'result_amount should be' do 152 | expect(subject.result_amount).to eq(12_000) 153 | end 154 | 155 | it 'promotion quantity should be 0' do 156 | expect(promotions.first.quantity).to eq(0) 157 | end 158 | 159 | context 'when promotion quantity is 0, promotion is not applicable' do 160 | before { subject.perform } 161 | 162 | it 'discount_amount should be the same' do 163 | expect(subject.discount_amount).to eq(2000) 164 | end 165 | 166 | it 'result_amount should be the same' do 167 | expect(subject.result_amount).to eq(12_000) 168 | end 169 | 170 | it 'applied_promotions should not be increased' do 171 | expect(subject.applied_promotions.size).to eq(1) 172 | end 173 | end 174 | end 175 | 176 | describe 'when order is qualified to apply order over amount percent off max discount per person promotion' do 177 | let(:promotions) do 178 | [ 179 | Promotion.new(id: 5, name: '訂單滿 3000 元可以折 10%,每人總共最高折 500 元', code: 'order_over_amount_percent_off_max_discount_per_person', 180 | promotion_target_type: 'ORDER', 181 | options: { over_amount: 3000, percent_off: 10, max_discount_amount: 2500 }) 182 | ] 183 | end 184 | let(:products) do 185 | [ 186 | Product.new(id: 1, name: Faker::Commerce.product_name, price: 6000), 187 | Product.new(id: 2, name: Faker::Commerce.product_name, price: 8000) 188 | ] 189 | end 190 | 191 | it 'origin_amount is equal to products price sum' do 192 | expect(subject.origin_amount).to eq(14_000) 193 | end 194 | 195 | it 'discount amount should come from this promotion' do 196 | expect(subject.discount_amount).to eq(1400) 197 | end 198 | 199 | it 'result_amount should be' do 200 | expect(subject.result_amount).to eq(12_600) 201 | end 202 | 203 | context 'when user with promotion logs over max discount amount 500' do 204 | let(:promotion_logs) do 205 | [ 206 | PromotionLog.new(id: 1, user_id: 1, promotion_id: 5, discount_amount: 1300), 207 | PromotionLog.new(id: 2, user_id: 1, promotion_id: 5, discount_amount: 1200) 208 | ] 209 | end 210 | 211 | it 'promotion is not qualified of applying, discount amount should be zero' do 212 | expect(subject.discount_amount).to eq(0) 213 | end 214 | 215 | it 'there is no applied promotion ' do 216 | expect(subject.applied_promotions.size).to eq(0) 217 | end 218 | end 219 | end 220 | 221 | describe 'when order is qualified to apply order over amount discount amount monthly max promotion' do 222 | let(:promotions) do 223 | [ 224 | Promotion.new(id: 6, name: '訂單滿 3000 元可以折 500 元每月全站最高折 10000 元', code: 'order_over_amount_discount_amount_monthly_max', 225 | promotion_target_type: 'ORDER', 226 | options: { over_amount: 3000, discount_amount: 500, monthly_max_amount: 2000 }) 227 | ] 228 | end 229 | let(:products) { [Product.new(id: 1, name: Faker::Commerce.product_name, price: 5000)] } 230 | 231 | it 'origin_amount is equal to products price sum' do 232 | expect(subject.origin_amount).to eq(5000) 233 | end 234 | 235 | it 'discount amount should come from this promotion' do 236 | expect(subject.discount_amount).to eq(500) 237 | end 238 | 239 | it 'result_amount should be' do 240 | expect(subject.result_amount).to eq(4500) 241 | end 242 | 243 | context 'when promotion logs over max discount amount 2000' do 244 | let(:promotion_logs) do 245 | [ 246 | PromotionLog.new(id: 1, user_id: 1, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 4, 5)), 247 | PromotionLog.new(id: 2, user_id: 2, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 4, 10)), 248 | PromotionLog.new(id: 3, user_id: 3, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 4, 22)), 249 | PromotionLog.new(id: 4, user_id: 4, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 4, 7)) 250 | ] 251 | end 252 | 253 | it 'promotion is not qualified of applying, discount amount should be zero' do 254 | expect(subject.discount_amount).to eq(0) 255 | end 256 | 257 | it 'there is no applied promotion ' do 258 | expect(subject.applied_promotions.size).to eq(0) 259 | end 260 | end 261 | 262 | context 'when promotion logs over max discount amount 2000 but not in this month' do 263 | let(:promotion_logs) do 264 | [ 265 | PromotionLog.new(id: 1, user_id: 1, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 1, 5)), 266 | PromotionLog.new(id: 2, user_id: 2, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 1, 10)), 267 | PromotionLog.new(id: 3, user_id: 3, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 1, 22)), 268 | PromotionLog.new(id: 4, user_id: 4, promotion_id: 6, discount_amount: 500, created_at: DateTime.new(2022, 1, 7)) 269 | ] 270 | end 271 | 272 | it 'promotion is qualified of applying, discount amount should 500' do 273 | expect(subject.discount_amount).to eq(500) 274 | end 275 | 276 | it 'there is applied promotion ' do 277 | expect(subject.applied_promotions.size).to eq(1) 278 | end 279 | end 280 | end 281 | end 282 | end 283 | --------------------------------------------------------------------------------