Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (coupons): Coupons refactoring #615

Merged
merged 10 commits into from
Nov 23, 2022
25 changes: 25 additions & 0 deletions app/controllers/api/v1/applied_coupons_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ def create
end
end

def index
applied_coupons = current_organization.applied_coupons
if params[:external_customer_id]
applied_coupons =
applied_coupons.joins(:customer).where(customers: { external_id: params[:external_customer_id] })
end
applied_coupons = applied_coupons.where(status: params[:status]) if valid_status?(params[:status])
applied_coupons = applied_coupons.order(created_at: :desc)
.page(params[:page])
.per(params[:per_page] || PER_PAGE)

render(
json: ::CollectionSerializer.new(
applied_coupons,
::V1::AppliedCouponSerializer,
collection_name: 'applied_coupons',
meta: pagination_metadata(applied_coupons),
),
)
end

private

def create_params
Expand All @@ -35,6 +56,10 @@ def create_params
:percentage_rate,
)
end

def valid_status?(status)
AppliedCoupon.statuses.key?(status)
end
end
end
end
9 changes: 9 additions & 0 deletions app/graphql/types/applied_coupons/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ class Object < Types::BaseObject
field :percentage_rate, Float, null: true
field :frequency, Types::Coupons::FrequencyEnum, null: false
field :frequency_duration, Integer, null: true
field :frequency_duration_remaining, Integer, null: true
field :amount_cents_remaining, Integer, null: true

field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :terminated_at, GraphQL::Types::ISO8601DateTime, null: false

def amount_cents_remaining
return nil if object.recurring?
return nil if object.coupon.percentage?

object.amount_cents - object.credits.sum(:amount_cents)
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/credit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Credit < ApplicationRecord

has_one :coupon, through: :applied_coupon

monetize :amount_cents, disable_validation: true, allow_nil: true

validates :amount_currency, inclusion: { in: currency_list }

scope :coupon_kind, -> { where.not(applied_coupon_id: nil) }
Expand Down Expand Up @@ -38,4 +40,17 @@ def item_name
# TODO: change it depending on invoice template
credit_note.invoice.number
end

def invoice_coupon_display_name
return nil if applied_coupon.blank?

coupon = applied_coupon.coupon
suffix = if coupon.percentage?
"#{format('%.2f', applied_coupon.percentage_rate)}%"
else
applied_coupon.amount.format
end

"#{coupon.name} (#{suffix})"
end
end
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Organization < ApplicationRecord
has_many :credit_notes, through: :customers
has_many :events
has_many :coupons
has_many :applied_coupons, through: :coupons
has_many :add_ons
has_many :invites
has_many :payment_providers
Expand Down
12 changes: 12 additions & 0 deletions app/serializers/v1/applied_coupon_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@ def serialize
coupon_code: model.coupon.code,
lago_customer_id: model.customer.id,
external_customer_id: model.customer.external_id,
status: model.status,
amount_cents: model.amount_cents,
amount_cents_remaining: amount_cents_remaining,
amount_currency: model.amount_currency,
percentage_rate: model.percentage_rate,
frequency: model.frequency,
frequency_duration: model.frequency_duration,
frequency_duration_remaining: model.frequency_duration_remaining,
expiration_date: model.coupon.expiration_date&.iso8601,
created_at: model.created_at.iso8601,
terminated_at: model.terminated_at&.iso8601,
}
end

private

def amount_cents_remaining
return nil if model.recurring?
return nil if model.coupon.percentage?

model.amount_cents - model.credits.sum(:amount_cents)
end
end
end
11 changes: 1 addition & 10 deletions app/services/applied_coupons/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ def create_from_api(organization:, args:)
def check_preconditions
return result.not_found_failure!(resource: 'customer') unless customer
return result.not_found_failure!(resource: 'coupon') unless coupon
return unless coupon_already_applied?

result.single_validation_failure!(
field: 'coupon',
error_code: 'coupon_already_applied',
)
end

def process_creation(applied_coupon_attributes)
Expand All @@ -69,6 +63,7 @@ def process_creation(applied_coupon_attributes)
percentage_rate: applied_coupon_attributes[:percentage_rate],
frequency: applied_coupon_attributes[:frequency],
frequency_duration: applied_coupon_attributes[:frequency_duration],
frequency_duration_remaining: applied_coupon_attributes[:frequency_duration],
)

if coupon.fixed_amount?
Expand All @@ -92,10 +87,6 @@ def process_creation(applied_coupon_attributes)
result.record_validation_failure!(record: e.record)
end

def coupon_already_applied?
customer.applied_coupons.active.exists?
end

def track_applied_coupon_created(applied_coupon)
SegmentTrackJob.perform_later(
membership_id: CurrentContext.membership,
Expand Down
4 changes: 2 additions & 2 deletions app/services/credits/applied_coupon_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def create
amount_currency: invoice.amount_currency,
)

applied_coupon.frequency_duration -= 1 if applied_coupon.recurring?
applied_coupon.frequency_duration_remaining -= 1 if applied_coupon.recurring?
if should_terminate_applied_coupon?(credit_amount)
applied_coupon.mark_as_terminated!
elsif applied_coupon.recurring?
Expand Down Expand Up @@ -73,7 +73,7 @@ def should_terminate_applied_coupon?(credit_amount)
if applied_coupon.once?
applied_coupon.coupon.percentage? || credit_amount >= remaining_amount
else
applied_coupon.frequency_duration <= 0
applied_coupon.frequency_duration_remaining <= 0
end
end
end
Expand Down
33 changes: 19 additions & 14 deletions app/services/invoices/create_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def create
compute_amounts(invoice)

create_credit_note_credit(invoice) if should_create_credit_note_credit?
create_coupon_credit(invoice) if should_create_coupon_credit?
create_coupon_credit(invoice) if should_create_coupon_credit?(invoice)
create_applied_prepaid_credit(invoice) if should_create_applied_prepaid_credit?(invoice)

invoice.total_amount_cents = invoice.amount_cents + invoice.vat_amount_cents
Expand Down Expand Up @@ -132,10 +132,10 @@ def should_deliver_webhook?
customer.organization.webhook_url?
end

def applied_coupon
return @applied_coupon if @applied_coupon
def applied_coupons
return @applied_coupons if @applied_coupons

@applied_coupon = customer.applied_coupons.active.first
@applied_coupons = customer.applied_coupons.active.order(created_at: :asc)
end

def credit_notes
Expand All @@ -152,10 +152,9 @@ def should_create_credit_note_credit?
credit_notes.any?
end

def should_create_coupon_credit?
return false if applied_coupon.nil?

return applied_coupon.amount_currency == currency if applied_coupon.coupon.fixed_amount?
def should_create_coupon_credit?(invoice)
return false if applied_coupons.blank?
return false unless invoice.amount_cents&.positive?

true
end
Expand All @@ -178,13 +177,19 @@ def create_credit_note_credit(invoice)
end

def create_coupon_credit(invoice)
credit_result = Credits::AppliedCouponService.new(
invoice: invoice,
applied_coupon: applied_coupon,
).create
credit_result.throw_error unless credit_result.success?
applied_coupons.each do |applied_coupon|
break unless invoice.amount_cents&.positive?

refresh_amounts(invoice: invoice, credit_amount_cents: credit_result.credit.amount_cents)
next if applied_coupon.coupon.fixed_amount? && applied_coupon.amount_currency != currency

credit_result = Credits::AppliedCouponService.new(
invoice: invoice,
applied_coupon: applied_coupon,
).create
credit_result.throw_error unless credit_result.success?

refresh_amounts(invoice: invoice, credit_amount_cents: credit_result.credit.amount_cents)
end
end

def create_applied_prepaid_credit(invoice)
Expand Down
22 changes: 12 additions & 10 deletions app/views/templates/invoice.slim
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,13 @@ html
.body-1 All usage based fees
td.body-1 style="text-align: right;" width="30%"
= charge_amount.format
- if legacy? && credits.any?
tr
td width="70%"
.body-1 Coupons
td.body-1 style="text-align: right; color: #008559;" width="30%"
= credit_amount.format
- if legacy? && credits.coupon_kind.any?
- credits.coupon_kind.order(created_at: :asc).each do |credit|
tr
td width="70%"
.body-1 #{credit.invoice_coupon_display_name}
td.body-1 style="text-align: right; color: #008559;" width="30%"
= credit.amount.format

table.total-table width="100%"
- if legacy?
Expand Down Expand Up @@ -469,10 +470,11 @@ html
td.body-1 style="text-align: right; color: #008559;"
= credit_notes_total_amount.format
- if credits.coupon_kind.any?
tr
td.body-2 Coupons
td.body-1 style="text-align: right; color: #008559;"
= coupon_total_amount.format
- credits.coupon_kind.order(created_at: :asc).each do |credit|
tr
td.body-2 #{credit.invoice_coupon_display_name}
td.body-1 style="text-align: right; color: #008559;"
= credit.amount.format
- if subscription? && wallet_transactions.exists?
tr
td.body-2 width="70%" Prepaid credits
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
put :void, on: :member
end
resources :events, only: %i[create show]
resources :applied_coupons, only: %i[create]
resources :applied_coupons, only: %i[create index]
resources :applied_add_ons, only: %i[create]
resources :invoices, only: %i[update show index] do
post :download, on: :member
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class AddFrequencyDurationRemainingToAppliedCoupons < ActiveRecord::Migration[7.0]
def change
add_column :applied_coupons, :frequency_duration_remaining, :integer
remove_index :applied_coupons, column: %i[coupon_id customer_id]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class FillFrequencyDurationRemainingField < ActiveRecord::Migration[7.0]
def change
LagoApi::Application.load_tasks
Rake::Task['applied_coupons:populate_frequency_duration_remaining'].invoke
end
end
4 changes: 2 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions lib/tasks/applied_coupons.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

namespace :applied_coupons do
desc 'Populate frequency duration remaining field'
task populate_frequency_duration_remaining: :environment do
AppliedCoupon.find_each do |applied_coupon|
next unless applied_coupon.recurring?

applied_coupon.frequency_duration_remaining = applied_coupon.frequency_duration
applied_coupon.save!
end
end
end
2 changes: 2 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,13 @@ type AppliedAddOn {

type AppliedCoupon {
amountCents: Int
amountCentsRemaining: Int
amountCurrency: CurrencyEnum
coupon: Coupon!
createdAt: ISO8601DateTime!
frequency: CouponFrequency!
frequencyDuration: Int
frequencyDurationRemaining: Int
id: ID!
percentageRate: Float
terminatedAt: ISO8601DateTime!
Expand Down
28 changes: 28 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,20 @@

]
},
{
"name": "amountCentsRemaining",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null,
"args": [

]
},
{
"name": "amountCurrency",
"description": null,
Expand Down Expand Up @@ -874,6 +888,20 @@

]
},
{
"name": "frequencyDurationRemaining",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null,
"args": [

]
},
{
"name": "id",
"description": null,
Expand Down
Loading