Skip to content

Commit

Permalink
feat(preview-invoice): add route and controller action (#3108)
Browse files Browse the repository at this point in the history
## Context

Preview feature enables fetching first Lago invoice. This invoice is not
persisted and is calculated on the fly.

## Description

This PR is the last one for the feature. It adds route, controller
action and includes few fixes based on QA returns

---------

Co-authored-by: Oleksandr Chebotarov <[email protected]>
  • Loading branch information
lovrocolic and floganz authored Jan 27, 2025
1 parent 49d90fe commit d1c844c
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 4 deletions.
63 changes: 63 additions & 0 deletions app/controllers/api/v1/invoices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,31 @@ def sync_salesforce_id
end
end

def preview
result = Invoices::PreviewContextService.call(
organization: current_organization,
params: preview_params.to_h.deep_symbolize_keys
)
return render_error_response(result) unless result.success?

result = Invoices::PreviewService.call(
customer: result.customer,
subscription: result.subscription,
applied_coupons: result.applied_coupons
)
if result.success?
render(
json: ::V1::InvoiceSerializer.new(
result.invoice,
root_name: 'invoice',
includes: %i[customer integration_customers credits applied_taxes preview_subscriptions preview_fees]
)
)
else
render_error_response(result)
end
end

private

def create_params
Expand Down Expand Up @@ -234,6 +259,44 @@ def update_params
)
end

def preview_params
params.permit(
:plan_code,
:billing_time,
:subscription_at,
coupons: [
:code,
:name,
:coupon_type,
:amount_cents,
:amount_currency,
:percentage_rate,
:frequency,
:frequency_duration,
:frequency_duration_remaining
],
customer: [
:external_id,
:name,
:tax_identification_number,
:currency,
:timezone,
shipping_address: [
:address_line1,
:address_line2,
:city,
:zipcode,
:state,
:country
],
integration_customers: [
:integration_type,
:integration_code
]
]
)
end

def sync_salesforce_id_params
params.permit(
:external_id,
Expand Down
14 changes: 14 additions & 0 deletions app/serializers/v1/invoice_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def serialize
payload.merge!(error_details) if include?(:error_details)
payload.merge!(applied_usage_thresholds) if model.progressive_billing?
payload.merge!(applied_invoice_custom_sections) if include?(:applied_invoice_custom_sections)
payload.merge!(preview_subscriptions) if include?(:preview_subscriptions)
payload.merge!(preview_fees) if include?(:preview_fees)

payload
end
Expand All @@ -63,6 +65,12 @@ def subscriptions
).serialize
end

def preview_subscriptions
::CollectionSerializer.new(
model.subscriptions, ::V1::SubscriptionSerializer, collection_name: 'subscriptions'
).serialize
end

def fees
::CollectionSerializer.new(
model.fees.includes(
Expand All @@ -80,6 +88,12 @@ def fees
).serialize
end

def preview_fees
::CollectionSerializer.new(
model.fees, ::V1::FeeSerializer, collection_name: 'fees'
).serialize
end

def credits
::CollectionSerializer.new(model.credits, ::V1::CreditSerializer, collection_name: 'credits').serialize
end
Expand Down
2 changes: 1 addition & 1 deletion app/serializers/v1/invoices/applied_tax_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def serialize
amount_cents: model.amount_cents,
amount_currency: model.amount_currency,
fees_amount_cents: model.fees_amount_cents,
created_at: model.created_at.iso8601
created_at: model&.created_at&.iso8601
}
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/services/coupons/preview_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def call

applied_coupons.each do |applied_coupon|
break unless invoice.sub_total_excluding_taxes_amount_cents&.positive?
next unless invoice.currency == applied_coupon.amount_currency
next if applied_coupon.coupon.fixed_amount? && invoice.currency != applied_coupon.amount_currency

fees = fees(applied_coupon)

Expand Down
4 changes: 3 additions & 1 deletion app/services/invoices/preview_context_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def find_or_build_customer
organization: organization,
tax_identification_number: customer_params[:tax_identification_number],
currency: customer_params[:currency],
timezone: customer_params[:timezone],
shipping_address_line1: customer_params.dig(:shipping_address, :address_line1),
shipping_address_line2: customer_params.dig(:shipping_address, :address_line2),
shipping_city: customer_params.dig(:shipping_address, :city),
Expand Down Expand Up @@ -73,7 +74,7 @@ def build_subscription
subscription_at: params[:subscription_at].presence || Time.current,
started_at: params[:subscription_at].presence || Time.current,
billing_time:,
created_at: Time.current,
created_at: params[:subscription_at].presence || Time.current,
updated_at: Time.current
)
end
Expand All @@ -94,6 +95,7 @@ def find_or_build_applied_coupons
coupon || Coupon.new(coupon_attr)
end.map do |coupon|
AppliedCoupon.new(
id: SecureRandom.uuid,
coupon:,
customer: result.customer,
amount_cents: coupon.amount_cents,
Expand Down
4 changes: 3 additions & 1 deletion app/services/invoices/preview_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def call
compute_tax_and_totals

result.invoice = invoice
result.subscription = subscription
result
end

Expand All @@ -56,8 +57,9 @@ def date_service

def billing_time
return @billing_time if defined? @billing_time
return subscription.subscription_at if subscription.plan.pay_in_advance?

ds = Subscriptions::DatesService.new_instance(subscription, Time.current, current_usage: true)
ds = Subscriptions::DatesService.new_instance(subscription, subscription.subscription_at, current_usage: true)

@billing_time = ds.end_of_period + 1.day
end
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
post :retry, on: :member
post :retry_payment, on: :member
post :payment_url, on: :member
post :preview, on: :collection
put :refresh, on: :member
put :finalize, on: :member
put :sync_salesforce_id, on: :member
Expand Down
46 changes: 46 additions & 0 deletions spec/requests/api/v1/invoices_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1008,4 +1008,50 @@
end
end
end

describe 'POST /api/v1/invoices/preview' do
subject { post_with_token(organization, '/api/v1/invoices/preview', preview_params) }

let(:plan) { create(:plan, organization:) }
let(:preview_params) do
{
customer: {
name: 'test 1',
currency: 'EUR',
tax_identification_number: '123456789'
},
plan_code: plan.code,
billing_time: 'anniversary'
}
end

it 'creates a preview invoice' do
subject

expect(response).to have_http_status(:success)
expect(json[:invoice]).to include(
invoice_type: 'subscription',
fees_amount_cents: 100,
taxes_amount_cents: 20,
total_amount_cents: 120,
currency: 'EUR'
)
end

context 'when customer does not exist' do
let(:preview_params) do
{
customer: {
external_id: 'unknown'
},
plan_code: plan.code
}
end

it 'returns a not found error' do
subject
expect(response).to have_http_status(:not_found)
end
end
end
end
32 changes: 32 additions & 0 deletions spec/services/invoices/preview_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@
end
end

context 'with in advance billing in the future' do
let(:plan) { create(:plan, organization:, interval: 'monthly', pay_in_advance: true) }
let(:subscription) do
build(
:subscription,
customer:,
plan:,
billing_time:,
subscription_at: timestamp + 1.day,
started_at: timestamp + 1.day,
created_at: timestamp + 1.day
)
end

it 'creates preview invoice for 1 day' do
travel_to(timestamp) do
result = preview_service.call

expect(result).to be_success
expect(result.invoice.subscriptions.first).to eq(subscription)
expect(result.invoice.fees.length).to eq(1)
expect(result.invoice.invoice_type).to eq('subscription')
expect(result.invoice.issuing_date.to_s).to eq('2024-03-31')
expect(result.invoice.fees_amount_cents).to eq(3)
expect(result.invoice.sub_total_excluding_taxes_amount_cents).to eq(3)
expect(result.invoice.taxes_amount_cents).to eq(2)
expect(result.invoice.sub_total_including_taxes_amount_cents).to eq(5)
expect(result.invoice.total_amount_cents).to eq(5)
end
end
end

context 'with applied coupons' do
let(:applied_coupon) do
build(
Expand Down

0 comments on commit d1c844c

Please sign in to comment.