Skip to content

Commit 9adf001

Browse files
committed
Aggregate proxy collective payouts by funding source
The payout creates one expense per project allocation, but the GitHub Sponsors CSV export groups by funding source. For dtolnay this means 11 separate ~$50 expenses on OC versus one $599 row in the CSV, making it impossible for OC to reconcile the two. payout_proxy_collectives now groups allocations by funding source and creates a single aggregated expense per maintainer, matching the CSV.
1 parent ee802c0 commit 9adf001

File tree

2 files changed

+82
-8
lines changed

2 files changed

+82
-8
lines changed

app/models/allocation.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ def group_projects_by_funding_source_and_platform
269269
end
270270

271271
def payout
272-
project_allocations.each(&:payout)
272+
project_allocations.reject(&:is_proxy_collective?).each(&:payout)
273+
payout_proxy_collectives
273274
end
274275

275276
def payout_open_source_collectives
@@ -281,10 +282,6 @@ def payout_open_collectives
281282
end
282283

283284
def payout_proxy_collectives
284-
project_allocations.select(&:is_proxy_collective?).each(&:payout)
285-
end
286-
287-
def payout_proxy_collectives_aggregated
288285
# Group proxy collective allocations by funding source
289286
proxy_allocations = project_allocations.includes(:funding_source, :project)
290287
.select(&:is_proxy_collective?)

test/models/allocation_test.rb

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,84 @@ def setup
107107
assert_equal 40_000, ProjectAllocation.find_by(project_id: @project1.id).amount_cents
108108
end
109109

110-
test 'payout_proxy_collectives_aggregated groups allocations by funding source' do
110+
test 'payout creates one expense per funding source for proxy collectives, not one per project' do
111+
funding_source = FundingSource.create!(
112+
url: 'https://github.com/sponsors/dtolnay',
113+
platform: 'github.com'
114+
)
115+
116+
project1 = Project.create!(
117+
url: 'https://github.com/dtolnay/syn',
118+
name: 'syn',
119+
licenses: ['mit'],
120+
registry_names: ['crates.io'],
121+
repository: { 'archived' => false },
122+
funding_source: funding_source,
123+
owner: { 'email' => 'dtolnay@example.com' }
124+
)
125+
126+
project2 = Project.create!(
127+
url: 'https://github.com/dtolnay/serde',
128+
name: 'serde',
129+
licenses: ['mit'],
130+
registry_names: ['crates.io'],
131+
repository: { 'archived' => false },
132+
funding_source: funding_source,
133+
owner: { 'email' => 'dtolnay@example.com' }
134+
)
135+
136+
project3 = Project.create!(
137+
url: 'https://github.com/dtolnay/anyhow',
138+
name: 'anyhow',
139+
licenses: ['mit'],
140+
registry_names: ['crates.io'],
141+
repository: { 'archived' => false },
142+
funding_source: funding_source,
143+
owner: { 'email' => 'dtolnay@example.com' }
144+
)
145+
146+
pa1 = ProjectAllocation.create!(allocation: @allocation, project: project1, fund: @fund, funding_source: funding_source, amount_cents: 20000, score: 0.3)
147+
pa2 = ProjectAllocation.create!(allocation: @allocation, project: project2, fund: @fund, funding_source: funding_source, amount_cents: 20000, score: 0.3)
148+
pa3 = ProjectAllocation.create!(allocation: @allocation, project: project3, fund: @fund, funding_source: funding_source, amount_cents: 19900, score: 0.3)
149+
150+
proxy_collective = ProxyCollective.create!(
151+
slug: 'esf-github-sponsors-dtolnay',
152+
name: 'https://github.com/sponsors/dtolnay',
153+
website: 'https://github.com/sponsors/dtolnay'
154+
)
155+
156+
ProxyCollective.stubs(:find_or_create_by_website).returns(proxy_collective)
157+
proxy_collective.stubs(:set_payout_method).returns(true)
158+
159+
expense_requests = []
160+
stub_request(:post, /opencollective/).to_return do |request|
161+
body = JSON.parse(request.body)
162+
expense_requests << body
163+
if body['query'].include?('createExpense')
164+
{
165+
status: 200,
166+
body: { data: { createExpense: { id: '1', legacyId: 789, status: 'APPROVED', amount: 59900, currency: 'USD', description: 'test', draftKey: 'abc', payee: { slug: proxy_collective.slug } } } }.to_json,
167+
headers: { 'Content-Type' => 'application/json' }
168+
}
169+
else
170+
{
171+
status: 200,
172+
body: { data: { processExpense: { id: '1', status: 'APPROVED' } } }.to_json,
173+
headers: { 'Content-Type' => 'application/json' }
174+
}
175+
end
176+
end
177+
178+
@allocation.payout
179+
180+
create_expense_calls = expense_requests.select { |r| r['query'].include?('createExpense') }
181+
assert_equal 1, create_expense_calls.length, "Expected 1 aggregated expense but got #{create_expense_calls.length} individual ones"
182+
183+
total_amount = create_expense_calls.first['variables']['expense']['items'].sum { |i| i['amount'] }
184+
assert_equal 59900, total_amount, "Expected aggregated amount of 59900 cents"
185+
end
186+
187+
test 'payout_proxy_collectives groups allocations by funding source' do
111188
funding_source = FundingSource.create!(
112189
url: 'https://github.com/sponsors/testuser',
113190
platform: 'github.com',
@@ -164,7 +241,7 @@ def setup
164241
assert_equal 10000, grouped[funding_source.id].sum(&:amount_cents)
165242
end
166243

167-
test 'payout_proxy_collectives_aggregated falls back to email when aggregated total still below minimum' do
244+
test 'payout_proxy_collectives falls back to email when aggregated total still below minimum' do
168245
funding_source = FundingSource.create!(
169246
url: 'https://github.com/sponsors/testuser',
170247
platform: 'github.com',
@@ -215,7 +292,7 @@ def setup
215292
headers: { 'Content-Type' => 'application/json' }
216293
)
217294

218-
@allocation.payout_proxy_collectives_aggregated
295+
@allocation.payout_proxy_collectives
219296

220297
# Should have logged payout_skipped events with fallback message for each allocation
221298
pa1.reload

0 commit comments

Comments
 (0)