Skip to content

Commit b3ed2c5

Browse files
committed
wip
1 parent dceb83a commit b3ed2c5

File tree

6 files changed

+413
-0
lines changed

6 files changed

+413
-0
lines changed

app/Enums/LicenseSource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ enum LicenseSource: string
77
case Stripe = 'stripe';
88
case Bifrost = 'bifrost';
99
case Manual = 'manual';
10+
case OpenCollective = 'opencollective';
1011
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use App\Enums\LicenseSource;
6+
use App\Enums\Subscription;
7+
use App\Jobs\CreateAnystackLicenseJob;
8+
use App\Models\User;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Support\Facades\Hash;
11+
use Illuminate\Support\Facades\Log;
12+
use Illuminate\Support\Str;
13+
14+
class OpenCollectiveWebhookController extends Controller
15+
{
16+
public function handle(Request $request)
17+
{
18+
// Verify webhook signature if secret is configured
19+
if (config('services.opencollective.webhook_secret')) {
20+
$this->verifySignature($request);
21+
}
22+
23+
$payload = $request->all();
24+
$type = $payload['type'] ?? null;
25+
26+
Log::info('OpenCollective webhook received', [
27+
'type' => $type,
28+
'payload' => $payload,
29+
]);
30+
31+
// Handle different webhook types
32+
match ($type) {
33+
'collective.transaction.created' => $this->handleContributionProcessed($payload),
34+
default => Log::info('Unhandled OpenCollective webhook type', ['type' => $type]),
35+
};
36+
37+
return response()->json(['status' => 'success']);
38+
}
39+
40+
protected function verifySignature(Request $request): void
41+
{
42+
$secret = config('services.opencollective.webhook_secret');
43+
$signature = $request->header('X-OpenCollective-Signature');
44+
45+
if (! $signature) {
46+
abort(401, 'Missing webhook signature');
47+
}
48+
49+
$payload = $request->getContent();
50+
$expectedSignature = hash_hmac('sha256', $payload, $secret);
51+
52+
if (! hash_equals($expectedSignature, $signature)) {
53+
abort(401, 'Invalid webhook signature');
54+
}
55+
}
56+
57+
protected function handleContributionProcessed(array $payload): void
58+
{
59+
$data = $payload['data'] ?? [];
60+
61+
// Extract transaction details
62+
$order = $data['order'] ?? [];
63+
$fromAccount = $order['fromAccount'] ?? [];
64+
65+
// Get contributor email and name
66+
$email = $fromAccount['email'] ?? null;
67+
$name = $fromAccount['name'] ?? null;
68+
69+
if (! $email) {
70+
Log::warning('OpenCollective contribution missing email', ['payload' => $payload]);
71+
72+
return;
73+
}
74+
75+
// Check if this is a recurring contribution (monthly sponsor)
76+
$frequency = $order['frequency'] ?? 'ONETIME';
77+
$amount = $data['amount'] ?? [];
78+
$value = $amount['value'] ?? 0;
79+
80+
// Only grant licenses for monthly sponsors above $10
81+
if ($frequency !== 'MONTHLY' || $value < 1000) { // Amount in cents
82+
Log::info('OpenCollective contribution does not qualify for license', [
83+
'email' => $email,
84+
'frequency' => $frequency,
85+
'value' => $value,
86+
]);
87+
88+
return;
89+
}
90+
91+
// Find or create user
92+
$user = User::firstOrCreate(
93+
['email' => $email],
94+
[
95+
'name' => $name ?? Str::before($email, '@'),
96+
'password' => Hash::make(Str::random(32)),
97+
]
98+
);
99+
100+
// Check if user already has a Mini license from OpenCollective
101+
$existingLicense = $user->licenses()
102+
->where('policy_name', Subscription::Mini->value)
103+
->where('source', LicenseSource::OpenCollective)
104+
->first();
105+
106+
if ($existingLicense) {
107+
Log::info('User already has OpenCollective Mini license', [
108+
'user_id' => $user->id,
109+
'license_id' => $existingLicense->id,
110+
]);
111+
112+
return;
113+
}
114+
115+
// Create Mini license
116+
$firstName = null;
117+
$lastName = null;
118+
119+
if ($name) {
120+
$nameParts = explode(' ', $name, 2);
121+
$firstName = $nameParts[0] ?? null;
122+
$lastName = $nameParts[1] ?? null;
123+
}
124+
125+
CreateAnystackLicenseJob::dispatch(
126+
user: $user,
127+
subscription: Subscription::Mini,
128+
subscriptionItemId: null,
129+
firstName: $firstName,
130+
lastName: $lastName,
131+
source: LicenseSource::OpenCollective
132+
);
133+
134+
Log::info('Mini license creation dispatched for OpenCollective sponsor', [
135+
'user_id' => $user->id,
136+
'email' => $email,
137+
]);
138+
}
139+
}

app/Http/Middleware/VerifyCsrfToken.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ class VerifyCsrfToken extends Middleware
1313
*/
1414
protected $except = [
1515
'stripe/webhook',
16+
'opencollective/contribution',
1617
];
1718
}

config/services.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@
3838
'bifrost' => [
3939
'api_key' => env('BIFROST_API_KEY'),
4040
],
41+
42+
'opencollective' => [
43+
'webhook_secret' => env('OPENCOLLECTIVE_WEBHOOK_SECRET'),
44+
],
4145
];

routes/web.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use App\Http\Controllers\Auth\CustomerAuthController;
55
use App\Http\Controllers\CustomerLicenseController;
66
use App\Http\Controllers\CustomerSubLicenseController;
7+
use App\Http\Controllers\OpenCollectiveWebhookController;
78
use App\Http\Controllers\ShowBlogController;
89
use App\Http\Controllers\ShowDocumentationController;
910
use Illuminate\Support\Facades\Route;
@@ -32,6 +33,9 @@
3233
Route::redirect('t-shirt', 'pricing');
3334
Route::redirect('tshirt', 'pricing');
3435

36+
// Webhook routes (must be outside web middleware for CSRF bypass)
37+
Route::post('opencollective/contribution', [OpenCollectiveWebhookController::class, 'handle'])->name('opencollective.webhook');
38+
3539
Route::view('/', 'welcome')->name('welcome');
3640
Route::view('pricing', 'pricing')->name('pricing');
3741
Route::view('alt-pricing', 'alt-pricing')->name('alt-pricing')->middleware('signed');

0 commit comments

Comments
 (0)