VerbFilter::class, 'actions' => [ 'donate' => ['post'], 'stripe-webhook' => ['post'], 'paypal-webhook' => ['post'], ], ]; return $behaviors; } protected function getAccessRules() { return [[ ContentContainerControllerAccess::RULE_USER_GROUP_ONLY => [ Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN, Space::USERGROUP_MODERATOR, Space::USERGROUP_USER, Space::USERGROUP_GUEST, ], ]]; } public function beforeAction($action) { if (in_array($action->id, ['stripe-webhook', 'paypal-webhook'], true)) { $this->enableCsrfValidation = false; $this->detachBehavior('containerControllerBehavior'); } return parent::beforeAction($action); } public function actionIndex() { $schemaReady = $this->isSchemaReady(); $goals = []; $providerConfig = null; if ($schemaReady) { $goals = DonationGoal::find() ->where([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, 'is_active' => 1, ]) ->orderBy(['id' => SORT_DESC]) ->all(); $providerConfig = DonationProviderConfig::findOne([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); } $dashboardData = $this->buildDonationDashboardData($schemaReady, $goals); $canDonate = $this->canDonateInSpace(); return $this->render('index', [ 'goals' => $goals, 'providerConfig' => $providerConfig, 'canDonate' => $canDonate, 'providerOptions' => $this->getEnabledProviderOptions($providerConfig), 'recurringOptions' => $this->getRecurringProviderKeys($providerConfig), 'space' => $this->contentContainer, 'schemaReady' => $schemaReady, 'dashboardData' => $dashboardData, ]); } public function actionDonate() { if (!$this->isSchemaReady()) { $this->view->error(Yii::t('DonationsModule.base', 'Donations setup has not been run yet.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } if (!$this->canDonateInSpace()) { throw new ForbiddenHttpException('You are not allowed to donate in this space.'); } $goalId = (int)Yii::$app->request->post('goal_id'); $goal = DonationGoal::findOne([ 'id' => $goalId, 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, 'is_active' => 1, ]); if (!$goal instanceof DonationGoal) { $this->view->error(Yii::t('DonationsModule.base', 'Donation goal not found.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $providerConfig = DonationProviderConfig::findOne([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); $provider = strtolower(trim((string)Yii::$app->request->post('provider', ''))); $mode = strtolower(trim((string)Yii::$app->request->post('mode', 'one_time'))); $amount = (float)Yii::$app->request->post('amount', 0); $isAnonymous = (int)Yii::$app->request->post('is_anonymous', 0) === 1; $enabledProviderOptions = $this->getEnabledProviderOptions($providerConfig); if (!isset($enabledProviderOptions[$provider])) { $this->view->error(Yii::t('DonationsModule.base', 'Selected provider is not enabled.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } if (!in_array($mode, ['one_time', 'recurring'], true)) { $this->view->error(Yii::t('DonationsModule.base', 'Invalid donation mode.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $recurringProviders = $this->getRecurringProviderKeys($providerConfig); if ($mode === 'recurring' && !in_array($provider, $recurringProviders, true)) { $this->view->error(Yii::t('DonationsModule.base', 'Selected provider does not have recurring enabled.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } if ($amount <= 0) { $this->view->error(Yii::t('DonationsModule.base', 'Please enter a valid donation amount.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $transaction = new DonationTransaction(); $transaction->contentcontainer_id = $this->contentContainer->contentcontainer_id; $transaction->donor_user_id = Yii::$app->user->isGuest ? null : (int)Yii::$app->user->id; $transaction->provider = $provider; $transaction->mode = $mode; $transaction->status = DonationTransaction::STATUS_PENDING; $transaction->amount = $amount; $transaction->currency = strtoupper((string)($providerConfig->default_currency ?? $goal->currency ?? 'USD')); $transaction->is_anonymous = $isAnonymous ? 1 : 0; $transaction->goal_id = (int)$goal->id; $transaction->goal_type = (string)$goal->goal_type; $transaction->target_animal_id = $goal->target_animal_id !== null ? (int)$goal->target_animal_id : null; $transaction->metadata_json = json_encode([ 'source' => 'mvp_intent_stub', 'goal_title' => $goal->title, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (!$transaction->save()) { $this->view->error(Yii::t('DonationsModule.base', 'Could not create donation intent. Please try again.')); Yii::error($transaction->getErrors(), 'donations.intent.save'); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $successUrl = Yii::$app->request->hostInfo . $this->contentContainer->createUrl('/donations/donations/complete', [ 'id' => (int)$transaction->id, ]); $cancelUrl = Yii::$app->request->hostInfo . $this->contentContainer->createUrl('/donations/donations/cancel', [ 'id' => (int)$transaction->id, ]); $gatewayService = new PaymentGatewayService(); $result = $gatewayService->createCheckout($transaction, $goal, $providerConfig, $successUrl, $cancelUrl); $metadata = json_decode((string)$transaction->metadata_json, true); if (!is_array($metadata)) { $metadata = []; } $metadata['checkout_result'] = [ 'success' => (bool)($result['success'] ?? false), 'provider' => $provider, 'mode' => $mode, 'checked_at' => date('c'), 'error' => (string)($result['error'] ?? ''), 'status' => (int)($result['status'] ?? 0), ]; $transaction->provider_checkout_id = (string)($result['checkout_id'] ?? $transaction->provider_checkout_id); $transaction->provider_payment_id = (string)($result['payment_id'] ?? $transaction->provider_payment_id); $transaction->provider_subscription_id = (string)($result['subscription_id'] ?? $transaction->provider_subscription_id); $transaction->provider_customer_id = (string)($result['customer_id'] ?? $transaction->provider_customer_id); $transaction->metadata_json = json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (!(bool)($result['success'] ?? false)) { $transaction->status = DonationTransaction::STATUS_FAILED; $transaction->save(false); $this->view->error(Yii::t('DonationsModule.base', 'Unable to start checkout: {message}', [ 'message' => (string)($result['error'] ?? 'Unknown provider error.'), ])); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $transaction->save(false); $redirectUrl = (string)($result['redirect_url'] ?? ''); if ($redirectUrl === '') { $this->view->error(Yii::t('DonationsModule.base', 'Provider did not return a checkout URL.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } return $this->redirect($redirectUrl); } public function actionComplete($id) { if (!$this->isSchemaReady()) { return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } $transaction = DonationTransaction::findOne([ 'id' => (int)$id, 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if ($transaction instanceof DonationTransaction) { $this->tryCapturePaypalOrderReturn($transaction); $this->trySettlePaypalRecurringReturn($transaction); if ($transaction->status === DonationTransaction::STATUS_PENDING) { $this->view->info(Yii::t('DonationsModule.base', 'Checkout completed. Awaiting webhook confirmation from {provider}.', [ 'provider' => strtoupper((string)$transaction->provider), ])); } elseif ($transaction->status === DonationTransaction::STATUS_SUCCEEDED) { $this->view->success(Yii::t('DonationsModule.base', 'Thank you. Your donation has been confirmed.')); } else { $this->view->info(Yii::t('DonationsModule.base', 'Checkout returned with status: {status}.', [ 'status' => (string)$transaction->status, ])); } } return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } private function tryCapturePaypalOrderReturn(DonationTransaction $transaction): void { if ($transaction->status !== DonationTransaction::STATUS_PENDING) { return; } if ($transaction->provider !== 'paypal' || $transaction->mode !== 'one_time') { return; } $expectedOrderId = trim((string)$transaction->provider_checkout_id); if ($expectedOrderId === '') { return; } $returnToken = trim((string)Yii::$app->request->get('token', '')); if ($returnToken === '' || $returnToken !== $expectedOrderId) { return; } $providerConfig = DonationProviderConfig::findOne([ 'contentcontainer_id' => $transaction->contentcontainer_id, ]); if (!$providerConfig instanceof DonationProviderConfig || (int)$providerConfig->paypal_enabled !== 1) { return; } $gatewayService = new PaymentGatewayService(); $captureResult = $gatewayService->capturePayPalOrder($transaction, $providerConfig); if (!($captureResult['success'] ?? false)) { $metadata = json_decode((string)$transaction->metadata_json, true); if (!is_array($metadata)) { $metadata = []; } $metadata['paypal_capture_result'] = [ 'success' => false, 'checked_at' => date('c'), 'status' => (int)($captureResult['status'] ?? 0), 'error' => (string)($captureResult['error'] ?? 'PayPal order capture failed.'), ]; $transaction->metadata_json = json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $transaction->save(false); Yii::warning([ 'transaction_id' => (int)$transaction->id, 'status' => (int)($captureResult['status'] ?? 0), 'error' => (string)($captureResult['error'] ?? ''), ], 'donations.paypal.capture'); return; } $transaction->provider_payment_id = (string)($captureResult['payment_id'] ?? $transaction->provider_payment_id); $settlement = new DonationSettlementService(); $settlement->markSucceededAndApply($transaction, [ 'paypal_order_captured_at' => date('c'), 'paypal_order_already_captured' => !empty($captureResult['already_captured']) ? 1 : 0, ]); } private function trySettlePaypalRecurringReturn(DonationTransaction $transaction): void { if ($transaction->status !== DonationTransaction::STATUS_PENDING) { return; } if ($transaction->provider !== 'paypal' || $transaction->mode !== 'recurring') { return; } $expected = trim((string)$transaction->provider_checkout_id); if ($expected === '') { return; } $subscriptionId = trim((string)Yii::$app->request->get('subscription_id', '')); $token = trim((string)Yii::$app->request->get('token', '')); if ($subscriptionId !== $expected && $token !== $expected) { return; } $settlement = new DonationSettlementService(); $settlement->markSucceededAndApply($transaction, [ 'paypal_return_confirmed_at' => date('c'), ]); } public function actionCancel($id) { if ($this->isSchemaReady()) { $transaction = DonationTransaction::findOne([ 'id' => (int)$id, 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if ($transaction instanceof DonationTransaction && $transaction->status === DonationTransaction::STATUS_PENDING) { $transaction->status = DonationTransaction::STATUS_CANCELLED; $transaction->save(false); } } $this->view->info(Yii::t('DonationsModule.base', 'Donation checkout was canceled.')); return $this->redirect($this->contentContainer->createUrl('/donations/donations/index')); } private function getEnabledProviderOptions(?DonationProviderConfig $providerConfig): array { $options = []; if ($providerConfig instanceof DonationProviderConfig && (int)$providerConfig->paypal_enabled === 1) { $options['paypal'] = Yii::t('DonationsModule.base', 'PayPal'); } if ($providerConfig instanceof DonationProviderConfig && (int)$providerConfig->stripe_enabled === 1) { $options['stripe'] = Yii::t('DonationsModule.base', 'Stripe'); } return $options; } private function getRecurringProviderKeys(?DonationProviderConfig $providerConfig): array { $providers = []; if ($providerConfig instanceof DonationProviderConfig && (int)$providerConfig->paypal_enabled === 1 && (int)$providerConfig->paypal_recurring_enabled === 1) { $providers[] = 'paypal'; } if ($providerConfig instanceof DonationProviderConfig && (int)$providerConfig->stripe_enabled === 1 && (int)$providerConfig->stripe_recurring_enabled === 1) { $providers[] = 'stripe'; } return $providers; } private function isSchemaReady(): bool { return Yii::$app->db->schema->getTableSchema(DonationGoal::tableName(), true) !== null && Yii::$app->db->schema->getTableSchema(DonationProviderConfig::tableName(), true) !== null && Yii::$app->db->schema->getTableSchema(DonationTransaction::tableName(), true) !== null; } private function canDonateInSpace(): bool { if ($this->contentContainer->can(Donate::class)) { return true; } if ($this->contentContainer instanceof Space && $this->contentContainer->isAdmin()) { return true; } if ($this->contentContainer instanceof Space) { $group = $this->contentContainer->getUserGroup(Yii::$app->user->getIdentity()); return in_array($group, [ Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN, Space::USERGROUP_MODERATOR, Space::USERGROUP_MEMBER, Space::USERGROUP_USER, Space::USERGROUP_GUEST, ], true); } return false; } private function buildDonationDashboardData(bool $schemaReady, array $goals): array { $result = [ 'isManager' => $this->canManageDonationDashboard(), 'months' => [], 'selectedMonth' => '', 'userRows' => [], 'userMonthlyHeaders' => [], 'userGrandTotal' => 0.0, 'adminRows' => [], 'adminTotals' => ['target' => 0.0, 'donated' => 0.0, 'percent' => 0.0], 'ytd' => ['year' => (int)date('Y'), 'donated' => 0.0, 'target' => 0.0, 'percent' => 0.0], 'previousYear' => null, ]; if (!$schemaReady) { return $result; } $transactions = DonationTransaction::find() ->where([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, 'status' => DonationTransaction::STATUS_SUCCEEDED, ]) ->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC]) ->all(); if (empty($transactions)) { return $result; } $goalById = []; $goalByAnimalId = []; foreach ($goals as $goal) { if (!$goal instanceof DonationGoal) { continue; } $goalById[(int)$goal->id] = $goal; if ((string)$goal->goal_type === DonationGoal::TYPE_ANIMAL && (int)$goal->target_animal_id > 0) { $goalByAnimalId[(int)$goal->target_animal_id] = $goal; } } $availableMonths = []; $donationByAnimalMonth = []; $donationByAnimalYear = []; $allAnimalIds = []; foreach ($transactions as $transaction) { $animalId = (int)($transaction->target_animal_id ?? 0); if ($animalId <= 0 && (int)($transaction->goal_id ?? 0) > 0) { $goal = $goalById[(int)$transaction->goal_id] ?? null; if ($goal instanceof DonationGoal) { $animalId = (int)($goal->target_animal_id ?? 0); } } if ($animalId <= 0) { continue; } $createdAt = trim((string)($transaction->created_at ?? '')); $timestamp = $createdAt !== '' ? strtotime($createdAt) : false; if ($timestamp === false) { continue; } $amount = max(0.0, (float)($transaction->amount ?? 0)); if ($amount <= 0) { continue; } $monthKey = date('Y-m', $timestamp); $yearKey = (int)date('Y', $timestamp); $availableMonths[$monthKey] = $monthKey; $allAnimalIds[$animalId] = $animalId; if (!isset($donationByAnimalMonth[$animalId])) { $donationByAnimalMonth[$animalId] = []; } $donationByAnimalMonth[$animalId][$monthKey] = ($donationByAnimalMonth[$animalId][$monthKey] ?? 0.0) + $amount; if (!isset($donationByAnimalYear[$animalId])) { $donationByAnimalYear[$animalId] = []; } $donationByAnimalYear[$animalId][$yearKey] = ($donationByAnimalYear[$animalId][$yearKey] ?? 0.0) + $amount; } if (empty($availableMonths)) { return $result; } rsort($availableMonths, SORT_STRING); $result['months'] = array_values($availableMonths); $requestedMonth = trim((string)Yii::$app->request->get('month', '')); $selectedMonth = in_array($requestedMonth, $result['months'], true) ? $requestedMonth : $result['months'][0]; $result['selectedMonth'] = $selectedMonth; $animalMetaById = $this->loadAnimalMeta(array_values($allAnimalIds)); if (!Yii::$app->user->isGuest) { $currentUserId = (int)Yii::$app->user->id; $userAnimalRows = []; foreach ($transactions as $transaction) { if ((int)($transaction->donor_user_id ?? 0) !== $currentUserId) { continue; } $animalId = (int)($transaction->target_animal_id ?? 0); if ($animalId <= 0 && (int)($transaction->goal_id ?? 0) > 0) { $goal = $goalById[(int)$transaction->goal_id] ?? null; if ($goal instanceof DonationGoal) { $animalId = (int)($goal->target_animal_id ?? 0); } } if ($animalId <= 0) { continue; } $createdAt = trim((string)($transaction->created_at ?? '')); $timestamp = $createdAt !== '' ? strtotime($createdAt) : false; if ($timestamp === false) { continue; } $amount = max(0.0, (float)($transaction->amount ?? 0)); if ($amount <= 0) { continue; } $monthKey = date('Y-m', $timestamp); $yearKey = (int)date('Y', $timestamp); $goal = $goalByAnimalId[$animalId] ?? null; $animalMeta = $animalMetaById[$animalId] ?? null; if (!isset($userAnimalRows[$animalId])) { $userAnimalRows[$animalId] = [ 'animalId' => $animalId, 'animalName' => $animalMeta['name'] ?? ('Animal #' . $animalId), 'animalUrl' => $animalMeta['url'] ?? '', 'imageUrl' => $animalMeta['image'] ?? '', 'goalTarget' => $goal instanceof DonationGoal ? (float)$goal->target_amount : 0.0, 'goalCurrent' => $goal instanceof DonationGoal ? (float)$goal->current_amount : 0.0, 'goalCurrency' => $goal instanceof DonationGoal ? (string)$goal->currency : 'USD', 'total' => 0.0, 'monthly' => [], 'annual' => [], ]; } $userAnimalRows[$animalId]['total'] += $amount; $userAnimalRows[$animalId]['monthly'][$monthKey] = ($userAnimalRows[$animalId]['monthly'][$monthKey] ?? 0.0) + $amount; $userAnimalRows[$animalId]['annual'][$yearKey] = ($userAnimalRows[$animalId]['annual'][$yearKey] ?? 0.0) + $amount; } uasort($userAnimalRows, static function (array $a, array $b): int { return strnatcasecmp((string)$a['animalName'], (string)$b['animalName']); }); $result['userRows'] = array_values($userAnimalRows); $result['userMonthlyHeaders'] = $result['months']; $result['userGrandTotal'] = array_reduce($result['userRows'], static function (float $carry, array $row): float { return $carry + (float)($row['total'] ?? 0.0); }, 0.0); } $adminAnimalIds = array_values(array_unique(array_merge(array_keys($goalByAnimalId), array_keys($donationByAnimalMonth)))); sort($adminAnimalIds, SORT_NUMERIC); $adminRows = []; $subtotalTarget = 0.0; $subtotalDonated = 0.0; foreach ($adminAnimalIds as $animalId) { $goal = $goalByAnimalId[$animalId] ?? null; $animalMeta = $animalMetaById[$animalId] ?? null; $targetAmount = $goal instanceof DonationGoal ? (float)$goal->target_amount : 0.0; $donatedAmount = (float)($donationByAnimalMonth[$animalId][$selectedMonth] ?? 0.0); $percent = $targetAmount > 0 ? min(100.0, ($donatedAmount / $targetAmount) * 100.0) : 0.0; $adminRows[] = [ 'animalId' => $animalId, 'animalName' => $animalMeta['name'] ?? ('Animal #' . $animalId), 'animalUrl' => $animalMeta['url'] ?? '', 'target' => $targetAmount, 'donated' => $donatedAmount, 'percent' => $percent, ]; $subtotalTarget += $targetAmount; $subtotalDonated += $donatedAmount; } usort($adminRows, static function (array $a, array $b): int { return strnatcasecmp((string)$a['animalName'], (string)$b['animalName']); }); $subtotalPercent = $subtotalTarget > 0 ? min(100.0, ($subtotalDonated / $subtotalTarget) * 100.0) : 0.0; $result['adminRows'] = $adminRows; $result['adminTotals'] = [ 'target' => $subtotalTarget, 'donated' => $subtotalDonated, 'percent' => $subtotalPercent, ]; $currentYear = (int)date('Y'); $previousYear = $currentYear - 1; $ytdDonated = 0.0; $previousYearDonated = 0.0; foreach ($donationByAnimalYear as $animalId => $yearRows) { $ytdDonated += (float)($yearRows[$currentYear] ?? 0.0); $previousYearDonated += (float)($yearRows[$previousYear] ?? 0.0); } $totalActiveTarget = 0.0; foreach ($goalByAnimalId as $goal) { if (!$goal instanceof DonationGoal) { continue; } $totalActiveTarget += max(0.0, (float)$goal->target_amount); } $result['ytd'] = [ 'year' => $currentYear, 'donated' => $ytdDonated, 'target' => $totalActiveTarget, 'percent' => $totalActiveTarget > 0 ? min(100.0, ($ytdDonated / $totalActiveTarget) * 100.0) : 0.0, ]; if ($previousYearDonated > 0.0) { $result['previousYear'] = [ 'year' => $previousYear, 'donated' => $previousYearDonated, 'target' => $totalActiveTarget, 'percent' => $totalActiveTarget > 0 ? min(100.0, ($previousYearDonated / $totalActiveTarget) * 100.0) : 0.0, ]; } return $result; } private function canManageDonationDashboard(): bool { if (!($this->contentContainer instanceof Space) || Yii::$app->user->isGuest) { return false; } $group = $this->contentContainer->getUserGroup(Yii::$app->user->getIdentity()); return in_array($group, [ Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN, Space::USERGROUP_MODERATOR, Space::USERGROUP_MEMBER, ], true); } private function loadAnimalMeta(array $animalIds): array { $result = []; $animalIds = array_values(array_unique(array_filter(array_map('intval', $animalIds)))); if (empty($animalIds)) { return $result; } $animalClass = 'humhub\\modules\\animal_management\\models\\Animal'; $galleryClass = 'humhub\\modules\\animal_management\\models\\AnimalGalleryItem'; if (!class_exists($animalClass) || Yii::$app->db->schema->getTableSchema($animalClass::tableName(), true) === null) { return $result; } $animals = $animalClass::find()->where([ 'id' => $animalIds, 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ])->all(); foreach ($animals as $animal) { $animalId = (int)($animal->id ?? 0); if ($animalId <= 0) { continue; } $imageUrl = ''; if (class_exists($galleryClass) && Yii::$app->db->schema->getTableSchema($galleryClass::tableName(), true) !== null) { $galleryItem = $galleryClass::find()->where(['animal_id' => $animalId])->orderBy(['id' => SORT_DESC])->one(); if ($galleryItem !== null && method_exists($galleryItem, 'getImageUrl')) { $imageUrl = trim((string)$galleryItem->getImageUrl()); } } $displayName = method_exists($animal, 'getDisplayName') ? (string)$animal->getDisplayName() : trim((string)($animal->name ?? '')); if ($displayName === '') { $displayName = 'Animal #' . $animalId; } $result[$animalId] = [ 'name' => $displayName, 'url' => $this->contentContainer->createUrl('/animal_management/animals/view', ['id' => $animalId]), 'image' => $imageUrl, ]; } return $result; } public function actionStripeWebhook() { Yii::$app->response->format = Response::FORMAT_JSON; if (!$this->isSchemaReady()) { Yii::$app->response->statusCode = 400; return ['ok' => false, 'message' => 'Donations schema not ready']; } $providerConfig = DonationProviderConfig::findOne([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if (!$providerConfig instanceof DonationProviderConfig || (int)$providerConfig->stripe_enabled !== 1) { Yii::$app->response->statusCode = 404; return ['ok' => false, 'message' => 'Stripe is not enabled for this space']; } $payload = (string)Yii::$app->request->getRawBody(); $signature = (string)Yii::$app->request->headers->get('Stripe-Signature', ''); $service = new StripeWebhookService(); $result = $service->process($payload, $signature, $providerConfig); Yii::$app->response->statusCode = (int)($result['status'] ?? 200); return [ 'ok' => (bool)($result['ok'] ?? false), 'message' => (string)($result['message'] ?? ''), ]; } public function actionPaypalWebhook() { Yii::$app->response->format = Response::FORMAT_JSON; if (!$this->isSchemaReady()) { Yii::$app->response->statusCode = 400; return ['ok' => false, 'message' => 'Donations schema not ready']; } $providerConfig = DonationProviderConfig::findOne([ 'contentcontainer_id' => $this->contentContainer->contentcontainer_id, ]); if (!$providerConfig instanceof DonationProviderConfig || (int)$providerConfig->paypal_enabled !== 1) { Yii::$app->response->statusCode = 404; return ['ok' => false, 'message' => 'PayPal is not enabled for this space']; } $payload = (string)Yii::$app->request->getRawBody(); $headers = Yii::$app->request->headers->toArray(); $service = new PayPalWebhookService(); $result = $service->process($payload, $headers, $providerConfig); Yii::$app->response->statusCode = (int)($result['status'] ?? 200); return [ 'ok' => (bool)($result['ok'] ?? false), 'message' => (string)($result['message'] ?? ''), ]; } }