Skip to content

Commit 0f335ca

Browse files
CF P0 Issue / Duplicate Invoice Items: Wipe add_invoice_items before adding termination metadata (#1197)
1 parent aa6c901 commit 0f335ca

File tree

5 files changed

+41047
-3
lines changed

5 files changed

+41047
-3
lines changed

lib/stripe-force/translate/order.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,10 @@ def update_subscription_phases_from_order_amendments(contract_structure)
11731173
log.info 'updating subscription schedule with termination metadata', sf_order_amendment_id: sf_order_amendment
11741174
subscription_phases = OrderAmendment.delete_past_phases(@user, stripe_customer_id, subscription_phases)
11751175
mapper.add_termination_metadata(T.must(subscription_phases.last), sf_order_amendment)
1176+
1177+
log.info 'wiping out the last phase add_invoice_items before updating with termination metadata'
1178+
T.must(subscription_phases.last)[:add_invoice_items] = []
1179+
11761180
subscription_schedule.phases = OrderHelpers.sanitize_subscription_schedule_phase_params(subscription_phases)
11771181
subscription_schedule = T.cast(subscription_schedule.save({}, @user.stripe_credentials), Stripe::SubscriptionSchedule)
11781182
end
@@ -1206,14 +1210,13 @@ def update_subscription_phases_from_order_amendments(contract_structure)
12061210
subscription_schedule = apply_amendment_order_mappings(mapper, subscription_schedule, sf_order_amendment)
12071211

12081212
# note: to support stacked amendments, we want to update the local sub_schedule and sub_phases
1209-
# because Stripe converts 'now' to a timestamp
1213+
# because Stripe converts 'now' to a timestamp
12101214
# and we want to use that timestamp when there is a stacked amendment
12111215
subscription_schedule = T.cast(subscription_schedule.save({}, @user.stripe_credentials), Stripe::SubscriptionSchedule)
12121216

12131217
if @user.feature_enabled?(FeatureFlags::STRIPE_REVENUE_CONTRACT)
12141218
adjust_revenue_contract_from_sub_schedule(subscription_schedule, contract_structure.initial, sf_order_amendment, aggregate_phase_items, invoice_items_in_order, invoice_items_for_prorations + negative_invoice_items)
12151219
end
1216-
12171220
end
12181221
end
12191222

sorbet/custom/stripe.rbi

+6
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ class Stripe::SubscriptionSchedulePrebilling
236236
end
237237

238238
class Stripe::SubscriptionSchedule
239+
sig { returns(T::Hash[T.any(String, Symbol), T.untyped]) }
240+
def current_phase; end
241+
242+
sig { returns(Integer)}
243+
def created; end
244+
239245
sig { returns(Stripe::SubscriptionScheduleSettings) }
240246
def default_settings; end
241247

test/integration/amendments/test_amendments.rb

+98-1
Original file line numberDiff line numberDiff line change
@@ -1662,7 +1662,7 @@ class Critic::OrderAmendmentTranslation < Critic::OrderAmendmentFunctionalTest
16621662
# create the initial sf order
16631663
sf_order = create_subscription_order(
16641664
sf_product_id: sf_product_id,
1665-
contact_email: "syncs_three_stacked_diffruns",
1665+
contact_email: "syncs_three_stacked_diffruns_3",
16661666
additional_fields: {
16671667
CPQ_QUOTE_SUBSCRIPTION_START_DATE => format_date_for_salesforce(initial_order_start_date),
16681668
CPQ_QUOTE_BILLING_FREQUENCY => CPQBillingFrequencyOptions::ANNUAL.serialize,
@@ -1767,6 +1767,103 @@ class Critic::OrderAmendmentTranslation < Critic::OrderAmendmentFunctionalTest
17671767
assert_equal(-1 * (BigDecimal(TEST_DEFAULT_PRICE) * BigDecimal(amendment_3_term) / BigDecimal(contract_term)).round(MAX_STRIPE_PRICE_PRECISION), BigDecimal(credit_stripe_price.unit_amount_decimal))
17681768
end
17691769

1770+
it 'syncs stacked backdated amendments and ensure no duplicate invoice items' do
1771+
@user.disable_feature(FeatureFlags::SF_CACHING)
1772+
# initial order: 1yr contract, billed monthly, started 3 months ago
1773+
# amendment 1: started 2 months ago
1774+
# amendment 2: started 1 month ago
1775+
contract_term = TEST_DEFAULT_CONTRACT_TERM
1776+
initial_order_start_date = now_time - 3.months - 3.days
1777+
initial_order_end_date = initial_order_start_date + contract_term.months
1778+
1779+
amendment_1_term = 11
1780+
amendment_1_start_date = initial_order_start_date + (contract_term - amendment_1_term).months
1781+
1782+
amendment_2_term = 10
1783+
amendment_2_start_date = initial_order_start_date + (contract_term - amendment_2_term).months
1784+
1785+
1786+
sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price(
1787+
additional_product_fields: {
1788+
CPQ_QUOTE_BILLING_FREQUENCY => CPQBillingFrequencyOptions::MONTHLY.serialize,
1789+
}
1790+
)
1791+
1792+
# create the initial sf order
1793+
sf_order = create_subscription_order(
1794+
sf_product_id: sf_product_id,
1795+
contact_email: "syncs_three_stacked_diffruns",
1796+
additional_fields: {
1797+
CPQ_QUOTE_SUBSCRIPTION_START_DATE => format_date_for_salesforce(initial_order_start_date),
1798+
CPQ_QUOTE_BILLING_FREQUENCY => CPQBillingFrequencyOptions::MONTHLY.serialize,
1799+
CPQ_QUOTE_SUBSCRIPTION_TERM => contract_term,
1800+
}
1801+
)
1802+
1803+
# create the first amendment to increase quantity (+2)
1804+
sf_contract_1 = create_contract_from_order(sf_order)
1805+
amendment_quote = create_quote_data_from_contract_amendment(sf_contract_1)
1806+
amendment_quote["lineItems"].first["record"][CPQ_QUOTE_QUANTITY] = 3
1807+
amendment_quote["record"][CPQ_QUOTE_SUBSCRIPTION_START_DATE] = format_date_for_salesforce(amendment_1_start_date)
1808+
amendment_quote["record"][CPQ_QUOTE_SUBSCRIPTION_TERM] = amendment_1_term
1809+
sf_order_amendment_1 = create_order_from_quote_data(amendment_quote)
1810+
1811+
# translate the orders (initial order and first amendment)
1812+
StripeForce::Translate.perform_inline(@user, sf_order.Id)
1813+
sf_order.refresh
1814+
stripe_id = sf_order[prefixed_stripe_field(GENERIC_STRIPE_ID)]
1815+
1816+
subscription_schedule = Stripe::SubscriptionSchedule.retrieve(stripe_id, @user.stripe_credentials)
1817+
first_phase = T.must(subscription_schedule.phases.first)
1818+
second_phase = T.must(subscription_schedule.phases.second)
1819+
1820+
# first phase should start at the backdated date
1821+
assert_equal(0, first_phase.start_date - initial_order_start_date.to_i)
1822+
assert_equal(0, first_phase.end_date - second_phase.start_date)
1823+
# first phase should have an item with a quantity of 1 and no invoice items
1824+
assert_equal(1, first_phase.items.count)
1825+
first_phase_item = T.must(first_phase.items.first)
1826+
assert_equal(1, first_phase_item.quantity)
1827+
assert_empty(first_phase.add_invoice_items)
1828+
1829+
# second phase should start 'now' (since it was a backdated amendment)
1830+
# and have two products with total quantity of 2
1831+
assert(second_phase.start_date.to_i - now_time.to_i < SECONDS_IN_DAY)
1832+
assert_equal(initial_order_end_date.to_i, second_phase.end_date.to_i)
1833+
# second phase should have an item with a quantity of 3
1834+
assert_equal(2, second_phase.items.count)
1835+
second_phase_item_1 = T.must(second_phase.items.first)
1836+
second_phase_item_2 = T.must(second_phase.items.second)
1837+
assert_equal(1, second_phase_item_1.quantity)
1838+
assert_equal(2, second_phase_item_2.quantity)
1839+
1840+
# prorate the added items added since the amendment was backdated and missed a billing cycle
1841+
assert_equal(1, second_phase.add_invoice_items.count)
1842+
prorated_item = T.unsafe(second_phase.add_invoice_items.first)
1843+
assert_equal(2, prorated_item.quantity)
1844+
1845+
invoice_items_list = Stripe::InvoiceItem.list({customer: subscription_schedule.customer, created: {gt: subscription_schedule.created}}, @user.stripe_credentials)
1846+
assert_equal(1, invoice_items_list.count)
1847+
1848+
# create the second amendment to terminate the order
1849+
sf_contract_2 = create_contract_from_order(sf_order_amendment_1)
1850+
amendment_quote = create_quote_data_from_contract_amendment(sf_contract_2)
1851+
amendment_quote["lineItems"].first["record"][CPQ_QUOTE_QUANTITY] = 0
1852+
amendment_quote["record"][CPQ_QUOTE_SUBSCRIPTION_START_DATE] = format_date_for_salesforce(amendment_2_start_date)
1853+
amendment_quote["record"][CPQ_QUOTE_SUBSCRIPTION_TERM] = amendment_2_term
1854+
sf_order_amendment_2 = create_order_from_quote_data(amendment_quote)
1855+
1856+
StripeForce::Translate.perform_inline(@user, sf_order_amendment_2.Id)
1857+
sf_order.refresh
1858+
stripe_id = sf_order[prefixed_stripe_field(GENERIC_STRIPE_ID)]
1859+
1860+
# fetch the latest subscription schedule
1861+
subscription_schedule = Stripe::SubscriptionSchedule.retrieve(stripe_id, @user.stripe_credentials)
1862+
assert_equal(2, subscription_schedule.phases.count)
1863+
1864+
invoice_items_list = Stripe::InvoiceItem.list({customer: subscription_schedule.customer, created: {gt: subscription_schedule.created}}, @user.stripe_credentials)
1865+
assert_equal(1, invoice_items_list.count)
1866+
end
17701867
end
17711868

17721869
# describe 'metadata' do

0 commit comments

Comments
 (0)