Skip to content

Commit 0a00f8c

Browse files
authored
feat(issuing-date-5) Finalize invoices based on expected_finalization_date (#4667)
## Roadmap Task 👉 [Spec](https://www.notion.so/getlago/Spec-Define-the-issuing-date-preferences-of-subscription-invoices-2a0ef63110d2807d860cc22ced334bfb) 👉 [Dive In](https://www.notion.so/getlago/BE-Dive-In-Define-the-issuing-date-preferences-of-subscription-invoices-2a3ef63110d2800e8620fb5687f0edf9?d=2a3ef63110d2808f96fd001cbc71298b#2a3ef63110d2807abf14f498b984ac57) ## Context This PR is part of the Invoice Issuing Date preferences updates. With the new issuing date adjustment setting, `issuing_date` might skip using `invoice_grace_period` (when the adjustment set to "keep_anchor"). But we still need to factor in `invoice_grace_period` when finalizing invoices. ## Description This PR adds a new field, `expected_finalization_date`, which stores the date when a draft invoice should be auto-finalized. It's basically: ``` expected_finalization_date = invoice creation date + invoice_grace_period.days ``` The `expected_finalization_date` field is mostly for internal use. We only expose it in GraphQL so we can show the right message about when a draft invoice will be finalized. It's not available in the REST API or in the SDK clients.
1 parent aa9c06f commit 0a00f8c

21 files changed

+269
-54
lines changed

app/graphql/types/invoices/object.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Object < Types::BaseObject
4141
field :total_due_amount_cents, GraphQL::Types::BigInt, null: false
4242
field :total_paid_amount_cents, GraphQL::Types::BigInt, null: false
4343

44+
field :expected_finalization_date, GraphQL::Types::ISO8601Date, null: false
4445
field :issuing_date, GraphQL::Types::ISO8601Date, null: false
4546
field :payment_due_date, GraphQL::Types::ISO8601Date, null: false
4647
field :payment_overdue, Boolean, null: false

app/models/invoice.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class Invoice < ApplicationRecord
121121
scope :invisible, -> { where(status: INVISIBLE_STATUS.keys) }
122122
scope :with_generated_number, -> { where(status: %w[finalized voided]) }
123123
scope :ready_to_be_refreshed, -> { draft.where(ready_to_be_refreshed: true) }
124-
scope :ready_to_be_finalized, -> { draft.where("issuing_date <= ?", Time.current.to_date) }
124+
scope :ready_to_be_finalized, -> { draft.where("COALESCE(expected_finalization_date, issuing_date) <= ?", Time.current.to_date) }
125125

126126
scope :created_before,
127127
lambda { |invoice|
@@ -440,6 +440,12 @@ def allow_manual_payment?
440440
MANUALLY_PAYABLE_INVOICE_STATUS.include?(status.to_sym)
441441
end
442442

443+
# A safeguard while we're populating the expected finalization date.
444+
# We can drop it once fill_expected_finalization_date has been run.
445+
def expected_finalization_date
446+
read_attribute(:expected_finalization_date) || issuing_date
447+
end
448+
443449
private
444450

445451
# Checks that every charge has at least one fee without a filter (charge_filter_id IS NULL)
@@ -594,6 +600,7 @@ def set_finalized_at
594600
# coupons_amount_cents :bigint default(0), not null
595601
# credit_notes_amount_cents :bigint default(0), not null
596602
# currency :string
603+
# expected_finalization_date :date
597604
# fees_amount_cents :bigint default(0), not null
598605
# file :string
599606
# finalized_at :datetime

app/services/customers/update_invoice_issuing_date_settings_service.rb

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def call
2121
# NOTE: Update issuing_date on draft invoices.
2222
customer.invoices.draft.find_each do |invoice|
2323
invoice.issuing_date = invoice.issuing_date + issuing_date_adjustment(invoice)
24+
invoice.expected_finalization_date = invoice.expected_finalization_date + grace_period_adjustment(invoice)
2425
invoice.payment_due_date = grace_period_payment_due_date(invoice)
2526
invoice.save!
2627
end
@@ -60,21 +61,37 @@ def set_issuing_date_settings
6061
end
6162

6263
def issuing_date_adjustment(invoice)
63-
recurring = invoice.invoice_subscriptions.first&.recurring?
64+
new_issuing_date_adjustment = new_issuing_date_service(invoice).issuing_date_adjustment
65+
old_issuing_date_adjustment = old_issuing_date_service(invoice).issuing_date_adjustment
6466

65-
old_issuing_date_adjustment = Invoices::IssuingDateService.new(
67+
new_issuing_date_adjustment - old_issuing_date_adjustment
68+
end
69+
70+
def grace_period_adjustment(invoice)
71+
new_grace_period = new_issuing_date_service(invoice).grace_period
72+
old_grace_period = old_issuing_date_service(invoice).grace_period
73+
74+
new_grace_period - old_grace_period
75+
end
76+
77+
def old_issuing_date_service(invoice)
78+
Invoices::IssuingDateService.new(
6679
customer_settings: previous_issuing_date_settings,
6780
billing_entity_settings: customer.billing_entity,
68-
recurring:
69-
).issuing_date_adjustment
81+
recurring: recurring(invoice)
82+
)
83+
end
7084

71-
new_issuing_date_adjustment = Invoices::IssuingDateService.new(
85+
def new_issuing_date_service(invoice)
86+
Invoices::IssuingDateService.new(
7287
customer_settings: customer,
7388
billing_entity_settings: customer.billing_entity,
74-
recurring:
75-
).issuing_date_adjustment
89+
recurring: recurring(invoice)
90+
)
91+
end
7692

77-
new_issuing_date_adjustment - old_issuing_date_adjustment
93+
def recurring(invoice)
94+
invoice.invoice_subscriptions.first&.recurring?
7895
end
7996

8097
def grace_period_payment_due_date(invoice)

app/services/invoices/create_generating_service.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def call
2929
timezone: customer.applicable_timezone,
3030
status: :generating,
3131
issuing_date:,
32+
expected_finalization_date:,
3233
payment_due_date:,
3334
net_payment_term: customer.applicable_net_payment_term,
3435
skip_charges:,
@@ -57,6 +58,13 @@ def issuing_date
5758
date + issuing_date_service.issuing_date_adjustment.days
5859
end
5960

61+
def expected_finalization_date
62+
date = datetime.in_time_zone(customer.applicable_timezone).to_date
63+
return date if !grace_period? || charge_in_advance
64+
65+
date + customer.applicable_invoice_grace_period.days
66+
end
67+
6068
def grace_period?
6169
return false unless invoice_type.to_sym == :subscription
6270

app/services/invoices/issuing_date_service.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def issuing_date_adjustment
1414
send("#{anchor}_#{adjustment}")
1515
end
1616

17+
def grace_period
18+
customer_settings[:invoice_grace_period] || billing_entity_settings[:invoice_grace_period] || 0
19+
end
20+
1721
private
1822

1923
attr_reader :customer_settings, :billing_entity_settings, :recurring
@@ -35,10 +39,6 @@ def next_period_start_align_with_finalization_date
3539
grace_period
3640
end
3741

38-
def grace_period
39-
customer_settings[:invoice_grace_period] || billing_entity_settings[:invoice_grace_period] || 0
40-
end
41-
4242
def anchor
4343
customer_settings[:subscription_invoice_issuing_date_anchor] || billing_entity_settings[:subscription_invoice_issuing_date_anchor]
4444
end

app/services/invoices/subscription_service.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,7 @@ def create_generating_invoice
134134
end
135135

136136
def grace_period?
137-
align_with_finalization_date =
138-
customer.applicable_subscription_invoice_issuing_date_adjustment == "align_with_finalization_date"
139-
140-
grace_period = customer.applicable_invoice_grace_period.positive?
141-
142-
@grace_period ||= (!recurring || align_with_finalization_date) && grace_period
137+
@grace_period ||= customer.applicable_invoice_grace_period.positive?
143138
end
144139

145140
def set_invoice_generated_status

app/services/invoices/update_issuing_date_from_billing_entity_service.rb

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def call
1515
return result unless invoice.draft?
1616

1717
invoice.issuing_date = invoice.issuing_date + issuing_date_adjustment.days
18+
invoice.expected_finalization_date = invoice.expected_finalization_date + grace_period_adjustment
1819
invoice.applied_grace_period = invoice.customer.applicable_invoice_grace_period
1920
invoice.payment_due_date = invoice.issuing_date + invoice.customer.applicable_net_payment_term.days
2021
invoice.save!
@@ -27,21 +28,37 @@ def call
2728
attr_reader :invoice, :previous_issuing_date_settings
2829

2930
def issuing_date_adjustment
30-
recurring = invoice.invoice_subscriptions.first&.recurring?
31+
new_issuing_date_adjustment = new_issuing_date_service.issuing_date_adjustment
32+
old_issuing_date_adjustment = old_issuing_date_service.issuing_date_adjustment
3133

32-
old_issuing_date_adjustment = Invoices::IssuingDateService.new(
34+
new_issuing_date_adjustment - old_issuing_date_adjustment
35+
end
36+
37+
def grace_period_adjustment
38+
new_grace_period = new_issuing_date_service.grace_period
39+
old_grace_period = old_issuing_date_service.grace_period
40+
41+
new_grace_period - old_grace_period
42+
end
43+
44+
def old_issuing_date_service
45+
Invoices::IssuingDateService.new(
3346
customer_settings: invoice.customer,
3447
billing_entity_settings: previous_issuing_date_settings,
3548
recurring:
36-
).issuing_date_adjustment
49+
)
50+
end
3751

38-
new_issuing_date_adjustment = Invoices::IssuingDateService.new(
52+
def new_issuing_date_service
53+
Invoices::IssuingDateService.new(
3954
customer_settings: invoice.customer,
4055
billing_entity_settings: invoice.billing_entity,
4156
recurring:
42-
).issuing_date_adjustment
57+
)
58+
end
4359

44-
new_issuing_date_adjustment - old_issuing_date_adjustment
60+
def recurring
61+
invoice.invoice_subscriptions.first&.recurring?
4562
end
4663
end
4764
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class AddExpectedFinalizationDateToInvoices < ActiveRecord::Migration[8.0]
4+
def change
5+
add_column :invoices, :expected_finalization_date, :date
6+
end
7+
end

db/structure.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3009,6 +3009,7 @@ CREATE TABLE public.invoices (
30093009
finalized_at timestamp without time zone,
30103010
voided_invoice_id uuid,
30113011
xml_file character varying,
3012+
expected_finalization_date date,
30123013
CONSTRAINT check_organizations_on_net_payment_term CHECK ((net_payment_term >= 0))
30133014
);
30143015

@@ -10223,6 +10224,7 @@ ALTER TABLE ONLY public.fixed_charges_taxes
1022310224
SET search_path TO "$user", public;
1022410225

1022510226
INSERT INTO "schema_migrations" (version) VALUES
10227+
('20251201084648'),
1022610228
('20251126135708'),
1022710229
('20251126134516'),
1022810230
('20251125174110'),

lib/tasks/invoices.rake

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ namespace :invoices do
3838
)
3939
end
4040
end
41+
42+
desc "Fill expected_finalization_date"
43+
task fill_expected_finalization_date: :environment do
44+
Invoice.in_batches.update_all("expected_finalization_date = COALESCE(expected_finalization_date, issuing_date)") # rubocop:disable Rails/SkipsModelValidations
45+
end
4146
end

0 commit comments

Comments
 (0)