Skip to content

Commit 3a90b22

Browse files
iHiDclaude
andauthored
Replace billing email heuristic with checkout session check (#8337)
* Fix Stripe reconciliation: status is not a valid list filter PaymentIntent.list doesn't accept a status parameter. Filter client-side instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace billing email heuristic with checkout session check The bootcamp payment filter used billing_details.email presence to distinguish bootcamp payments from donations. This was wrong - some donors have billing emails too, causing their payments to be silently dropped. Bootcamp payments were created via Checkout Sessions while donations are not, so use that as the heuristic instead. Also removes the invalid status parameter from PaymentIntent.list in the reconciliation command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8053c7d commit 3a90b22

3 files changed

Lines changed: 25 additions & 29 deletions

File tree

app/commands/payments/stripe/payment_intent/handle_success.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,8 @@ def subscription_data
4040
def should_record_payment?
4141
return true if subscription_data
4242

43-
charge = Stripe::Charge.retrieve(payment_intent.latest_charge)
44-
45-
# If there is an email, then it's the Bootcamp.
46-
# If there's not an email, then through our integration.
47-
# This is terrible, I know.
48-
charge.billing_details.email.blank?
43+
# Bootcamp payments were created via Checkout Sessions; donations are not.
44+
Stripe::Checkout::Session.list({ payment_intent: payment_intent.id }).data.empty?
4945
rescue StandardError
5046
true
5147
end

app/commands/payments/stripe/reconcile_payments.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ def sync_payment_intent(payment_intent)
4141
def should_record_payment?(payment_intent)
4242
return true if payment_intent.invoice
4343

44-
charge = payment_intent.latest_charge
45-
charge&.billing_details&.email.blank?
44+
# Bootcamp payments were created via Checkout Sessions; donations are not.
45+
Stripe::Checkout::Session.list({ payment_intent: payment_intent.id }).data.empty?
4646
rescue StandardError
4747
true
4848
end

test/commands/payments/stripe/reconcile_payments_test.rb

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ class Payments::Stripe::ReconcilePaymentsTest < Payments::TestBase
3939
end
4040
end
4141

42-
test "skips bootcamp payments with billing email" do
42+
test "skips bootcamp payments with checkout session" do
4343
freeze_time do
4444
create :user, stripe_customer_id: "cus_123"
4545
since = 90.days.ago
4646

47-
pis = [build_payment_intent("pi_bootcamp", customer: "cus_123",
48-
amount: 4900, billing_email: "buyer@example.com")]
47+
pis = [build_payment_intent("pi_bootcamp", customer: "cus_123", amount: 4900)]
4948
stub_payment_intents_list(pis, created_gte: since.to_i)
49+
stub_checkout_session("pi_bootcamp", exists: true)
5050

5151
Payments::Stripe::ReconcilePayments.(since:)
5252

@@ -151,23 +151,14 @@ class Payments::Stripe::ReconcilePaymentsTest < Payments::TestBase
151151
end
152152
end
153153

154-
test "records subscription payment even with billing email" do
154+
test "records donation with billing email but no checkout session" do
155155
freeze_time do
156-
user = create :user, stripe_customer_id: "cus_123"
157-
create :payments_subscription, user: user, external_id: "sub_abc", provider: :stripe
158-
invoice_id = "in_sub_email"
156+
create :user, stripe_customer_id: "cus_123"
159157
since = 90.days.ago
160158

161-
pis = [build_payment_intent("pi_sub_email", customer: "cus_123", amount: 999,
162-
billing_email: "someone@example.com", invoice: invoice_id)]
159+
pis = [build_payment_intent("pi_email_donor", customer: "cus_123", amount: 1500)]
163160
stub_payment_intents_list(pis, created_gte: since.to_i)
164-
165-
stub_request(:get, "https://api.stripe.com/v1/invoices/#{invoice_id}").
166-
to_return(
167-
status: 200,
168-
body: { id: invoice_id, object: "invoice", subscription: "sub_abc" }.to_json,
169-
headers: { 'Content-Type': 'application/json' }
170-
)
161+
stub_checkout_session("pi_email_donor", exists: false)
171162

172163
Payments::Stripe::ReconcilePayments.(since:)
173164

@@ -242,8 +233,20 @@ def stub_payment_intents_request(body:, created_gte:, starting_after: nil)
242233
)
243234
end
244235

236+
def stub_checkout_session(payment_intent_id, exists:)
237+
data = exists ? [{ id: "cs_#{SecureRandom.hex(8)}", object: "checkout.session" }] : []
238+
stub_request(:get, "https://api.stripe.com/v1/checkout/sessions?payment_intent=#{payment_intent_id}").
239+
to_return(
240+
status: 200,
241+
body: { object: "list", data:, has_more: false, url: "/v1/checkout/sessions" }.to_json,
242+
headers: { 'Content-Type': 'application/json' }
243+
)
244+
end
245+
245246
def build_payment_intent(id, customer:, amount:, receipt_url: nil,
246-
billing_email: nil, invoice: nil, created: Time.current.to_i)
247+
invoice: nil, created: Time.current.to_i)
248+
stub_checkout_session(id, exists: false)
249+
247250
{
248251
id:,
249252
object: "payment_intent",
@@ -255,10 +258,7 @@ def build_payment_intent(id, customer:, amount:, receipt_url: nil,
255258
latest_charge: {
256259
id: "ch_#{SecureRandom.hex(8)}",
257260
object: "charge",
258-
receipt_url:,
259-
billing_details: {
260-
email: billing_email
261-
}
261+
receipt_url:
262262
}
263263
}
264264
end

0 commit comments

Comments
 (0)