verifyWebhook($request)) { return response()->json(['message' => 'Invalid webhook signature'], 401); } $payload = $request->all(); // NOTE: Map this based on the actual Xendit product you use. // For Invoice webhook, you typically get invoice id + status. $providerRefId = $payload['id'] ?? $payload['data']['id'] ?? null; $status = strtoupper($payload['status'] ?? $payload['data']['status'] ?? ''); if (!$providerRefId) { return response()->json(['message' => 'Missing provider_ref_id'], 422); } $intent = PaymentIntent::where('provider_ref_id', $providerRefId)->first(); if (!$intent) { // idempotent: ignore unknown references return response()->json(['ok' => true]); } // Convert Xendit statuses to our statuses $mapped = match ($status) { 'PAID', 'SETTLED' => 'PAID', 'EXPIRED' => 'EXPIRED', 'FAILED' => 'FAILED', 'CANCELED', 'CANCELLED' => 'CANCELLED', default => 'PENDING', }; $intent->status = $mapped; $intent->raw_payload = $payload; if ($mapped === 'PAID') { $intent->paid_at = now(); } $intent->save(); if ($mapped === 'PAID') { $reg = Registration::find($intent->registration_id); if ($reg && $reg->status !== 'CONFIRMED') { $reg->status = 'CONFIRMED'; $reg->save(); } dispatch(new \App\Jobs\AssignPairingJob((int)$reg->event_id)); } return response()->json(['ok' => true]); } }