diff --git a/Resources/roles.yml b/Resources/roles.yml index ef49feeb26..95fe3baba8 100644 --- a/Resources/roles.yml +++ b/Resources/roles.yml @@ -72,6 +72,13 @@ stats: - admin-module-stats # Can access to stats level: 40 +helper: + extends: user + perms: + - admin-module-users + - admin-module-account + level: 40 + manager: extends: user perms: diff --git a/Resources/templates/default/about/contact.php b/Resources/templates/default/about/contact.php index 513595e89b..8786ac0d02 100644 --- a/Resources/templates/default/about/contact.php +++ b/Resources/templates/default/about/contact.php @@ -52,6 +52,7 @@

+ insert($this->honeypot->template, $this->honeypot->params) ?>
diff --git a/Resources/templates/default/admin/projects/report.php b/Resources/templates/default/admin/projects/report.php index 52e23f1984..78d9b46a9f 100644 --- a/Resources/templates/default/admin/projects/report.php +++ b/Resources/templates/default/admin/projects/report.php @@ -2,7 +2,14 @@ section('admin-project-content') ?> - $this->project, 'account' => $this->account, 'Data' => $this->data, 'admin'=>true)) ?> + $this->project, + 'account' => $this->account, + 'contract' => $this->contract, + 'invests' => $this->invests, + 'Data' => $this->data, + 'admin' => true +]) ?> replace() ?> diff --git a/Resources/templates/default/partials/form/honeypot.php b/Resources/templates/default/partials/form/honeypot.php new file mode 100644 index 0000000000..00e76954c4 --- /dev/null +++ b/Resources/templates/default/partials/form/honeypot.php @@ -0,0 +1,9 @@ + + + +
+ \ No newline at end of file diff --git a/Resources/templates/legacy/project/invests_table.html.php b/Resources/templates/legacy/project/invests_table.html.php new file mode 100644 index 0000000000..9662e94e16 --- /dev/null +++ b/Resources/templates/legacy/project/invests_table.html.php @@ -0,0 +1,127 @@ +vat) { + $account->vat = 21; +} + +$projectFee = round($account->fee / 100, 2); + +function countTotal($invests, $method) +{ + $count = 0; + + foreach ($invests as $invest) { + if ($invest->status < 1) continue; + if ($invest->method !== $method) continue; + + $total = $count + 1; + } + + return $total; +} + +function calcTotal($invests, $method) +{ + $total = 0; + + foreach ($invests as $invest) { + if ($invest->status < 1) continue; + if ($invest->method !== $method) continue; + + $total = $total + $invest->amount; + } + + return $total; +} + +$tpvTotal = calcTotal($vars['invests'], 'tpv'); +$tpvProjectFee = $tpvTotal * $projectFee; +$tpvProjectVat = $tpvProjectFee * 0.21; +$tpvGatewayFee = $tpvTotal * 0.008; +$tpvGatewayVat = 0; + +$paypalTotal = calcTotal($vars['invests'], 'paypal'); +$paypalProjectFee = $paypalTotal * $projectFee; +$paypalProjectVat = $paypalProjectFee * 0.21; +$paypalGatewayFee = ($paypalTotal * 0.034) + (countTotal($vars['invests'], 'paypal') * 0.35); +$paypalGatewayVat = 0; + +$poolTotal = calcTotal($vars['invests'], 'pool'); +$poolProjectFee = $poolTotal * $projectFee; +$poolProjectVat = $poolProjectFee * 0.21; +$poolGatewayFee = $poolTotal * 0.02; +$poolGatewayVat = $poolGatewayFee * 0.21; + +$cashTotal = calcTotal($vars['invests'], 'cash'); +$cashProjectFee = $cashTotal * $projectFee; +$cashProjectVat = $cashProjectFee * 0.21; +$cashGatewayFee = $cashTotal * 0.02; +$cashGatewayVat = $cashGatewayFee * 0.21; + +$totalTotal = $cashTotal + $poolTotal + $paypalTotal + $tpvTotal; +$totalProjectFee = $cashProjectFee + $poolProjectFee + $paypalProjectFee + $tpvProjectFee; +$totalProjectVat = $cashProjectVat + $poolProjectVat + $paypalProjectVat + $tpvProjectVat; +$totalGatewayFee = $cashGatewayFee + $poolGatewayFee + $paypalGatewayFee + $tpvGatewayFee; +$totalGatewayVat = $cashGatewayVat + $poolGatewayVat + $paypalGatewayVat + $tpvGatewayVat; + +$reportData = [ + 'TPV' => [ + 'base' => \amount_format($tpvTotal, 2), + 'project_fee' => sprintf("%s (%s%%)", \amount_format($tpvProjectFee, 2), $account->fee,), + 'project_vat' => sprintf("%s (21%%)", \amount_format($tpvProjectVat, 2),), + 'gateway_fee' => sprintf("%s (0,8%%)", \amount_format($tpvGatewayFee, 2)), + 'gateway_vat' => sprintf("%s (21%%)", \amount_format($tpvGatewayVat, 2)) + ], + 'PAYPAL' => [ + 'base' => \amount_format($paypalTotal, 2), + 'project_fee' => sprintf("%s (%s%%)", \amount_format($paypalProjectFee, 2), $account->fee), + 'project_vat' => sprintf("%s (21%%)", \amount_format($paypalProjectVat, 2)), + 'gateway_fee' => sprintf("%s (3,4%% + 0,35 * trxs)", \amount_format($paypalGatewayFee, 2)), + 'gateway_vat' => sprintf("%s (21%%)", \amount_format($paypalGatewayVat, 2)) + ], + 'MONEDERO' => [ + 'base' => \amount_format($poolTotal, 2), + 'project_fee' => sprintf("%s (%s%%)", \amount_format($poolProjectFee, 2), $account->fee), + 'project_vat' => sprintf("%s (21%%)", \amount_format($poolProjectVat, 2)), + 'gateway_fee' => sprintf("%s (2%%)", \amount_format($poolGatewayFee, 2)), + 'gateway_vat' => sprintf("%s (21%%)", \amount_format($poolGatewayVat, 2)) + ], + 'MANUAL' => [ + 'base' => \amount_format($cashTotal, 2), + 'project_fee' => sprintf("%s (%s%%)", \amount_format($cashProjectFee, 2), $account->fee), + 'project_vat' => sprintf("%s (21%%)", \amount_format($cashProjectVat, 2)), + 'gateway_fee' => sprintf("%s (2%%)", \amount_format($cashGatewayFee, 2)), + 'gateway_vat' => sprintf("%s (21%%)", \amount_format($cashGatewayVat, 2)), + ], + 'TOTAL' => [ + 'base' => \amount_format($totalTotal, 2), + 'project_fee' => sprintf("%s", \amount_format($totalProjectFee, 2)), + 'project_vat' => sprintf("%s", \amount_format($totalProjectVat, 2)), + 'gateway_fee' => sprintf("%s", \amount_format($totalGatewayFee, 2)), + 'gateway_vat' => sprintf("%s", \amount_format($totalGatewayVat, 2)) + ] +]; + +?> + + + + + + + + + + $value): ?> + + + + + + + + + +
RECAUDACIÓNCOMISIÓN DE GOTEOIVACOMISIONES COBRADAS A GOTEOIVA
\ No newline at end of file diff --git a/Resources/templates/legacy/project/report.html.php b/Resources/templates/legacy/project/report.html.php index 78a50aef92..7070f4274f 100644 --- a/Resources/templates/legacy/project/report.html.php +++ b/Resources/templates/legacy/project/report.html.php @@ -1,33 +1,33 @@ vat) -{ - $account->vat=21; +if (!$account->vat) { + $account->vat = 21; } -$matchers=$project->getMatchers('active'); +$matchers = $project->getMatchers('active'); // prepare base to apply the vat -$var_percentage_applied=$account->tax_base_percentage ? $account->tax_base_percentage/100 : '0.5'; +$var_percentage_applied = $account->tax_base_percentage ? $account->tax_base_percentage / 100 : '0.5'; -$sumData['match_goteo']=0; -$matchfunding_invest=0; +$sumData['match_goteo'] = 0; +$matchfunding_invest = 0; $matchers_amounts = []; if ($matchers) { - foreach($matchers as $matcher){ + foreach ($matchers as $matcher) { $matchers_amounts[$matcher->id] = Invest::getList([ 'projects' => $project->id, 'users' => $matcher->owner, @@ -35,13 +35,13 @@ 'status' => Invest::$ACTIVE_STATUSES ], null, 0, 0, 'money'); - $matchfunding_invest+= $matchers_amounts[$matcher->id]; - $matcher_fee=round($matcher->fee / 100, 2); + $matchfunding_invest += $matchers_amounts[$matcher->id]; + $matcher_fee = round($matcher->fee / 100, 2); $matched_fee = ($matchers_amounts[$matcher->id] * $matcher_fee); - $sumData['match_goteo']+= $matched_fee; + $sumData['match_goteo'] += $matched_fee; } //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['match_goteo']+=(( $sumData['match_goteo']*$var_percentage_applied)*($account->vat/100)); + $sumData['match_goteo'] += (($sumData['match_goteo'] * $var_percentage_applied) * ($account->vat / 100)); } $called = $project->called; @@ -56,7 +56,7 @@ $admin = (isset($vars['admin']) && $vars['admin'] === true) ? true : false; //restamos a los aportes recibios de monedero lo correspondiente a matchers -$Data['pool']['total']['amount']=$Data['pool']['total']['amount']-$matchfunding_invest; +$Data['pool']['total']['amount'] = $Data['pool']['total']['amount'] - $matchfunding_invest; $total_issues = 0; foreach ($Data['issues'] as $issue) { @@ -68,7 +68,9 @@ $cName = "P-{$cNum}-{$cDate}"; ?>

Informe de financiación del proyecto
name) ?>

@@ -81,44 +83,45 @@ $sumData['fail'] = $total_issues; // $sumData['shown'] = $sumData['total'] + $sumData['fail'] + $sumData['drop'] + $sumData['pool'] + $sumData['ghost']; $sumData['shown'] = Invest::getList(['projects' => $project->id, 'status' => - [Invest::STATUS_PENDING, - Invest::STATUS_CHARGED, - Invest::STATUS_CANCELLED, - Invest::STATUS_TO_POOL, - Invest::STATUS_PAID - ]], null, 0, 0, 'money'); + [ + Invest::STATUS_PENDING, + Invest::STATUS_CHARGED, + Invest::STATUS_CANCELLED, + Invest::STATUS_TO_POOL, + Invest::STATUS_PAID + ]], null, 0, 0, 'money'); $sumData['cancelled'] = Invest::getList(['projects' => $project->id, 'status' => [Invest::STATUS_CANCELLED, Invest::STATUS_TO_POOL]], null, 0, 0, 'money'); $sumData['tpv_fee_goteo'] = $Data['tpv']['total']['amount'] * 0.008; $sumData['cash_goteo'] = $Data['cash']['total']['amount'] * $GOTEO_FEE; //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['cash_goteo']=(($sumData['cash_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['cash_goteo']; + $sumData['cash_goteo'] = (($sumData['cash_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['cash_goteo']; $sumData['tpv_goteo'] = $Data['tpv']['total']['amount'] * $GOTEO_FEE; //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['tpv_goteo']=(($sumData['tpv_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['tpv_goteo']; + $sumData['tpv_goteo'] = (($sumData['tpv_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['tpv_goteo']; $sumData['pp_goteo'] = $Data['paypal']['total']['amount'] * $GOTEO_FEE; //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['pp_goteo']=(($sumData['pp_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['pp_goteo']; + $sumData['pp_goteo'] = (($sumData['pp_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['pp_goteo']; $sumData['drop_goteo'] = $Data['drop']['total']['amount'] * $CALL_FEE; //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['drop_goteo']=(($sumData['drop_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['drop_goteo']; + $sumData['drop_goteo'] = (($sumData['drop_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['drop_goteo']; $sumData['pool_goteo'] = $Data['pool']['total']['amount'] * $GOTEO_FEE; //Aplicamos el IVA al 50% de la comision de Goteo - $sumData['pool_goteo']=(($sumData['pool_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['pool_goteo']; + $sumData['pool_goteo'] = (($sumData['pool_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['pool_goteo']; $sumData['ghost_goteo'] = $Data['ghost']['total']['amount'] * $GOTEO_FEE; //Aplicamos el IVA - $sumData['ghost_goteo']=(($sumData['ghost_goteo']*$var_percentage_applied)*($account->vat/100))+$sumData['ghost_goteo']; + $sumData['ghost_goteo'] = (($sumData['ghost_goteo'] * $var_percentage_applied) * ($account->vat / 100)) + $sumData['ghost_goteo']; $sumData['pp_project'] = $Data['paypal']['total']['amount'] - $sumData['pp_goteo']; $sumData['pp_fee_goteo'] = ($Data['paypal']['total']['invests'] * 0.35) + ($Data['paypal']['total']['amount'] * 0.034); @@ -126,36 +129,36 @@ $sumData['pp_fee_project'] = ($Data['paypal']['total']['invests'] * 0.35) + ($sumData['pp_project'] * 0.034); $sumData['pp_net_project'] = $sumData['pp_project'] - $sumData['pp_fee_project']; $sumData['fee_goteo'] = $sumData['tpv_fee_goteo'] + $sumData['pp_fee_goteo']; - $sumData['goteo'] = $sumData['cash_goteo'] + $sumData['tpv_goteo'] + $sumData['pp_goteo'] + $sumData['drop_goteo'] + $sumData['pool_goteo'] + $sumData['ghost_goteo'] +$sumData['match_goteo']; // si que se descuenta la comisión sobre capital riego + $sumData['goteo'] = $sumData['cash_goteo'] + $sumData['tpv_goteo'] + $sumData['pp_goteo'] + $sumData['drop_goteo'] + $sumData['pool_goteo'] + $sumData['ghost_goteo'] + $sumData['match_goteo']; // si que se descuenta la comisión sobre capital riego // round to 2 decimal - $sumData['fee_goteo']=round($sumData['fee_goteo'],2); - $sumData['goteo']=round($sumData['goteo'],2); + $sumData['fee_goteo'] = round($sumData['fee_goteo'], 2); + $sumData['goteo'] = round($sumData['goteo'], 2); $sumData['total_fee_project'] = $sumData['fee_goteo'] + $sumData['goteo']; // este es el importe de la factura $sumData['project'] = $sumData['total'] - $sumData['fee_goteo'] - $sumData['goteo']; ?> -

- one_round) ? ' (y única)' : ''; - if (!empty($project->passed)) { - echo 'El proyecto terminó la primera'.$unique.' ronda el día '.date('d/m/Y', strtotime($project->passed)).'.
'; - } else { - echo 'El proyecto terminará la primera'.$unique.' ronda el día '.date('d/m/Y', strtotime($project->willpass)).'.
'; - } ?> - - one_round && !empty($project->success)) { - echo 'El proyecto terminó la segunda ronda el día '.date('d/m/Y', strtotime($project->success)).'.'; - } elseif (empty($project->success)) { - echo 'El proyecto terminará la segunda ronda el día '.date('d/m/Y', strtotime($project->willfinish)).'.
'; - } ?> -
-
- Envío correo electrónico user->email; ?> -

-
+

+ one_round) ? ' (y única)' : ''; + if (!empty($project->passed)) { + echo 'El proyecto terminó la primera' . $unique . ' ronda el día ' . date('d/m/Y', strtotime($project->passed)) . '.
'; + } else { + echo 'El proyecto terminará la primera' . $unique . ' ronda el día ' . date('d/m/Y', strtotime($project->willpass)) . '.
'; + } ?> + + one_round && !empty($project->success)) { + echo 'El proyecto terminó la segunda ronda el día ' . date('d/m/Y', strtotime($project->success)) . '.'; + } elseif (empty($project->success)) { + echo 'El proyecto terminará la segunda ronda el día ' . date('d/m/Y', strtotime($project->willfinish)) . '.
'; + } ?> +
+
+ Envío correo electrónico user->email; ?> +

+
@@ -163,45 +166,46 @@ +
     (Puede que no se haya llegado nunca a esta cifra si han devuelto aportes antes del cierre de campaña) + - - - + + + - - - + + + - - + + - - - + + +
-    Máximo mostrado en el termómetro de Goteo.org al cerrar la campaña (success) ? date('d/m/Y', strtotime($project->success)) : 'fecha'; ?>): -
     (Puede que no se haya llegado nunca a esta cifra si han devuelto aportes antes del cierre de campaña)
-    Aportes cancelados manualmente desde el admin o devueltos al monedero del usuario: -
-    Incidencias (Usuarios/as que no tienen fondos en su cuenta, tarjetas desactualizadas, cancelaciones, reembolsos...) : (* ver listado más abajo)
-    Incidencias (Usuarios/as que no tienen fondos en su cuenta, tarjetas desactualizadas, cancelaciones, reembolsos...) : (* ver listado más abajo)
-    Total recaudado: (importe de las ayudas monetarias recibidas)
-    Total Capital Riego: (Transferencia del convocador 'called->user->name ?>' directamente al impulsor)
-    Total Capital Riego: (Transferencia del convocador 'called->user->name ?>' directamente al impulsor)
-    Riego obtenido del Canal name ?>: id]) ?> (A transferir de forma directa)
- Comisión matcher: fee.'%' ?> + Comisión matcher: fee . '%' ?>
-    Otro recibido: (Aporte manual sin ingreso bancario)
-    Otro recibido: (Aporte manual sin ingreso bancario)
-
+
@@ -211,34 +215,37 @@ - - - - + - +
-    Comisiones cobradas a Goteo por cada transferencia de tarjeta (0,8%) y PayPal (3,4% + 0,35 por transacción/usuario/a): total
-    Comisión del fee; ?>% de Goteo.org vat ? '(Incluye un '.$account->vat.'% de IVA para el '. $account->tax_base_percentage .'% de la cantidad)' : '' ?>: - called): ?> -
     (fee_projects_drop .'%)' ?> +
-    Comisión del fee; ?>% de Goteo.org vat ? '(Incluye un ' . $account->vat . '% de IVA para el ' . $account->tax_base_percentage . '% de la cantidad)' : '' ?>: + called): ?> +
     (fee_projects_drop . '%)' ?> - -
     () + +
     ()
Por el total de estas comisiones la Fundación Goteo ha emitido la factura [Número de factura] por importe de , a nombre de la persona o entidad que firma el contratoPor el total de estas comisiones la Fundación Goteo ha emitido la factura [Número de factura] por importe de , a nombre de la persona o entidad que firma el contrato
-
+
- + @@ -247,7 +254,7 @@
3) Transferencias de la Fundación Goteo (Goteo.org) a los/as impulsores/as3) Transferencias de la Fundación Goteo (Goteo.org) a los/as impulsores/as
-    Envío a través de cuenta bancaria: ()En estas cantidades ya se ha descontado el importe de la factura [Número de factura] por importe de
-
+
@@ -255,74 +262,74 @@
- - - - - - - - - - - - - - -
Desglose informativo de lo pagado mediante PayPal
- Cantidad transferida:
- Comisión aproximada cobrada al impulor:
- Cantidad aproximada recibida por el impulsor:
-
- - - -
- - - - - - - -
* Listado de usuarios/as con incidencias en su cuenta PayPal.
Estos son los aportes con problemas en payPal que no se han conseguido cobrar y se han cancelado.
- -
- - status == 1) ? ' style="color: red !important;"' : ''; + +
+
+ + + + + + + + + +
Números de seguimiento relevantes
- Número de contrato: Pnumber ?>
- Número de seguimiento: getNumericId() ?>
+
+ +
+ + + +
+ + + + + + + +
* Listado de usuarios/as con incidencias en su cuenta PayPal.
Estos son los aportes con problemas en payPal que no se han conseguido cobrar y se han cancelado.
+ +
+ + status == 1) ? ' style="color: red !important;"' : ''; ?> - - - - - >Usuario/a userName; ?>, statusName; ?>, amount . ' ss.'; if (!empty($warst)) echo ' (Aporte: '.$issue->invest.')';?> - - - -
invest.'" target="_blank"'.$warst.'>[Ir al aporte] Usuario ' . $issue->userName . ' ['.$issue->userEmail.'], ' . $issue->statusName . ', ' . $issue->amount . ' euros.'; if (!empty($warst)) echo ' (Aporte: '.$issue->invest.')'; ?>
- -
- - - - -
TOTAL (no cobrado):
- - - - - - - - - - - - - - - -
Notas para el admin al generar los datos del informe.
La mayoría harán referencia a las incidencias (o a aportes que no están en el estado que deberían en este punto de la campaña)
-
- - - -
+ + + invest . '" target="_blank"' . $warst . '>[Ir al aporte] Usuario ' . $issue->userName . ' [' . $issue->userEmail . '], ' . $issue->statusName . ', ' . $issue->amount . ' euros.'; + if (!empty($warst)) echo ' (Aporte: ' . $issue->invest . ')'; ?> + + >Usuario/a userName; ?>, statusName; ?>, amount . ' ss.'; + if (!empty($warst)) echo ' (Aporte: ' . $issue->invest . ')'; ?> + + + + + +
+ + + + +
TOTAL (no cobrado):
+ + + + + + + + + + + + + + + +
Notas para el admin al generar los datos del informe.
La mayoría harán referencia a las incidencias (o a aportes que no están en el estado que deberían en este punto de la campaña)
+
+ + \ No newline at end of file diff --git a/Resources/templates/responsive/project/participate.php b/Resources/templates/responsive/project/participate.php index c40df55fea..5a3c683cec 100644 --- a/Resources/templates/responsive/project/participate.php +++ b/Resources/templates/responsive/project/participate.php @@ -53,7 +53,7 @@
- text('project-invest') ?> + invest->getMethod()->isSubscription() ? $this->text('project-invest-from-subscription') : $this->text('project-invest') ?>
amount) ?> @@ -93,7 +93,7 @@
- text('project-invest') ?> + invest->getMethod()->isSubscription() ? $this->text('project-invest-from-subscription') : $this->text('project-invest') ?>
amount) ?> diff --git a/db/migrations/20240620092323_goteo_form_honeypot.php b/db/migrations/20240620092323_goteo_form_honeypot.php new file mode 100755 index 0000000000..267c41b747 --- /dev/null +++ b/db/migrations/20240620092323_goteo_form_honeypot.php @@ -0,0 +1,56 @@ +hasPerm('admin-module-account')) return true; + return parent::isAllowed($user, $node); } diff --git a/src/Goteo/Controller/Admin/ProjectsSubController.php b/src/Goteo/Controller/Admin/ProjectsSubController.php index 1cbd08b242..8aa5cf6885 100644 --- a/src/Goteo/Controller/Admin/ProjectsSubController.php +++ b/src/Goteo/Controller/Admin/ProjectsSubController.php @@ -206,11 +206,15 @@ public function reportAction($id) { // Datos para el informe de transacciones correctas $data = Model\Invest::getReportData($project->id, $project->status, $project->round, $project->passed); $account = Model\Project\Account::get($project->id); + $contract = Model\Contract::get($project->id); + $invests = Model\Invest::getAll($project->id); return array( 'template' => 'admin/projects/report', 'project' => $project, 'account' => $account, + 'contract' => $contract, + 'invests' => $invests, 'data' => $data ); } diff --git a/src/Goteo/Controller/Admin/UsersSubController.php b/src/Goteo/Controller/Admin/UsersSubController.php index 3fe2efc02d..0a4d35ec3b 100644 --- a/src/Goteo/Controller/Admin/UsersSubController.php +++ b/src/Goteo/Controller/Admin/UsersSubController.php @@ -69,6 +69,7 @@ public function __construct($node, User $user, Request $request) { static public function isAllowed(User $user, $node): bool { // Only central node or superadmins allowed here if( ! (Config::isMasterNode($node) || $user->hasRoleInNode($node, ['superadmin', 'root'])) ) return false; + if ($user->hasPerm('admin-module-users')) return true; return parent::isAllowed($user, $node); } diff --git a/src/Goteo/Controller/ChannelController.php b/src/Goteo/Controller/ChannelController.php index c6e6ee2b58..e507b181af 100644 --- a/src/Goteo/Controller/ChannelController.php +++ b/src/Goteo/Controller/ChannelController.php @@ -190,6 +190,9 @@ public function listProjectsAction(Request $request, $id, $type = 'available', $ $view= $channel->type=='normal' ? 'channel/list_projects' : 'channel/'.$channel->type.'/list_projects'; + $dataSetsRepository = new DataSetRepository(); + $dataSets = $dataSetsRepository->getListByChannel([$id]); + return $this->viewResponse( $view, [ @@ -198,7 +201,8 @@ public function listProjectsAction(Request $request, $id, $type = 'available', $ 'title_text' => $title_text, 'type' => $type, 'total' => $total, - 'limit' => $limit + 'limit' => $limit, + 'dataSets' => $dataSets ] ); } diff --git a/src/Goteo/Controller/ContactController.php b/src/Goteo/Controller/ContactController.php index 016d623713..c8117a3c2a 100644 --- a/src/Goteo/Controller/ContactController.php +++ b/src/Goteo/Controller/ContactController.php @@ -16,6 +16,7 @@ use Goteo\Core\Controller; use Goteo\Library; use Goteo\Library\Text; +use Goteo\Model\FormHoneypot; use Goteo\Model\Mail; use Goteo\Model\Page; use Goteo\Model\Template; @@ -84,6 +85,22 @@ public function indexAction (Request $request) { } } + // check honeypot trap + $trap = Session::get('form-honeypot'); + Session::del('form-honeypot'); + if (FormHoneypot::checkTrap($trap, $request)) { + $honeypot = new FormHoneypot; + $honeypot->trap = $trap; + $honeypot->prey = $request->request->get($trap); + + $honeypot->validate($honeypotErrors); + $honeypot->save($honeypotErrors); + + // Make robot makers think they have succeeded + Message::info('Mensaje de contacto enviado correctamente.'); + return $this->redirect('/contact'); + } + $data = array( 'tag' => $tag, 'subject' => $subject, @@ -104,19 +121,19 @@ public function indexAction (Request $request) { $user_template=Template::CONTACT_AUTO_REPLY_NEW_PROJECT; break; case 'contact-form-project-form-tag-name': - $to_admin = Config::get('mail.contact'); - $user_template=Template::CONTACT_AUTO_REPLY_PROJECT_FORM; + $to_admin = Config::get('mail.contact'); + $user_template=Template::CONTACT_AUTO_REPLY_PROJECT_FORM; break; case 'contact-form-dev-tag-name': - $to_admin = Config::get('mail.fail'); - $user_template=Template::CONTACT_AUTO_REPLY_DEV; + $to_admin = Config::get('mail.fail'); + $user_template=Template::CONTACT_AUTO_REPLY_DEV; break; case 'contact-form-relief-tag-name': $to_admin = Config::get('mail.donor'); $user_template=Template::CONTACT_AUTO_REPLY_RELIEF; break; case 'contact-form-service-tag-name': - $to_admin = Config::get('mail.management'); + $to_admin = Config::get('mail.management'); break; default: $to_admin = Config::get('mail.contact'); @@ -170,10 +187,15 @@ public function indexAction (Request $request) { $captcha->build(); Session::store('captcha-phrase', $captcha->getPhrase()); } + // Generate a new form token $token = sha1(uniqid(mt_rand(), true)); Session::store('form-token', $token); + // Generate honeypot fields + $honeypot = FormHoneypot::layTrap(); + Session::store('form-honeypot', $honeypot->trap); + return $this->viewResponse('about/contact', array( 'data' => $data, @@ -181,6 +203,7 @@ public function indexAction (Request $request) { 'token' => $token, 'page' => Page::get('contact'), 'captcha' => $captcha, + 'honeypot' => $honeypot, 'errors' => $errors ) ); diff --git a/src/Goteo/Controller/Dashboard/ProjectDashboardController.php b/src/Goteo/Controller/Dashboard/ProjectDashboardController.php index 990cc7cbce..46b7aa479f 100644 --- a/src/Goteo/Controller/Dashboard/ProjectDashboardController.php +++ b/src/Goteo/Controller/Dashboard/ProjectDashboardController.php @@ -49,6 +49,7 @@ use Goteo\Model\Project\Support; use Goteo\Model\Stories; use Goteo\Model\User; +use Goteo\Payment\Method\StripeSubscriptionPaymentMethod; use Goteo\Util\Form\Type\SubmitType; use Goteo\Util\Form\Type\TextareaType; use Goteo\Util\Form\Type\TextType; @@ -900,10 +901,16 @@ public static function getInvestFilters(Project $project, $filter = []): array foreach($project->getIndividualRewards() as $reward) { $filters['reward'][$reward->id] = $reward->getTitle(); } + if($project->getCall()) { $filters['others']['drop'] = Text::Get('dashboard-project-filter-by-drop'); $filters['others']['nondrop'] = Text::Get('dashboard-project-filter-by-nondrop'); } + + if ($project->isPermanent()) { + $filters['others']['from_subscription'] = Text::get('dashboard-project-filter-by-subscription'); + } + $status = [ Invest::STATUS_CHARGED, Invest::STATUS_PAID, @@ -919,6 +926,12 @@ public static function getInvestFilters(Project $project, $filter = []): array } if(array_key_exists($filter['others'], $filters['others'])) { $filter_by['types'] = $filter['others']; + + if($filter['others']['from_subscription']) { + $filter_by['methods'] = [ + StripeSubscriptionPaymentMethod::PAYMENT_METHOD_ID + ]; + } } if($filter['query']) { $filter_by['name'] = $filter['query']; diff --git a/src/Goteo/Controller/StripeSubscriptionController.php b/src/Goteo/Controller/StripeSubscriptionController.php index 1b459cdbcd..9f578a9a6c 100644 --- a/src/Goteo/Controller/StripeSubscriptionController.php +++ b/src/Goteo/Controller/StripeSubscriptionController.php @@ -18,8 +18,9 @@ use Goteo\Model\User; use Goteo\Payment\Method\StripeSubscriptionPaymentMethod; use Goteo\Repository\InvestRepository; +use Stripe\Charge as StripeCharge; use Stripe\Event; -use Stripe\Invoice; +use Stripe\Invoice as StripeInvoice; use Stripe\StripeClient; use Stripe\Webhook; use Symfony\Component\HttpFoundation\JsonResponse; @@ -48,9 +49,9 @@ public function subscriptionsWebhook(Request $request) switch ($event->type) { case Event::TYPE_INVOICE_PAYMENT_SUCCEEDED: - return $this->processInvoice($event->data->object->id); + return $this->processInvoice($event->data->object); case Event::CHARGE_REFUNDED: - return $this->processRefund($event); + return $this->processRefund($event->data->object); default: return new JsonResponse( ['data' => sprintf("The event %s is not supported.", $event->type)], @@ -60,17 +61,11 @@ public function subscriptionsWebhook(Request $request) } } - private function processRefund(Event $event): JsonResponse + private function processRefund(StripeCharge $charge): JsonResponse { - $object = $event->data->object; - if (!$object || !$object->invoice) { - return []; - } - - $invoice = $this->stripe->invoices->retrieve($object->invoice); - $subscription = $this->stripe->subscriptions->retrieve($invoice->subscription); + $invoice = $this->stripe->invoices->retrieve($charge->invoice); - $invests = $this->investRepository->getListByPayment($subscription->id); + $invests = $this->investRepository->getListByTransaction($invoice->id); foreach ($invests as $key => $invest) { $invest->setStatus(Invest::STATUS_CANCELLED); $invest->save(); @@ -79,21 +74,22 @@ private function processRefund(Event $event): JsonResponse return new JsonResponse(['data' => $invests], Response::HTTP_OK); } - private function processInvoice(string $invoiceId): JsonResponse + private function processInvoice(StripeInvoice $invoice): JsonResponse { - $invoice = $this->stripe->invoices->retrieve($invoiceId); - if ($invoice->billing_reason === Invoice::BILLING_REASON_SUBSCRIPTION_CREATE) { - return new JsonResponse([ - 'data' => Invest::get($invoice->lines->data[0]->price->metadata->invest), - Response::HTTP_OK - ]); - } - /** @var User */ $user = User::getByEmail($invoice->customer_email); - $subscription = $this->stripe->subscriptions->retrieve($invoice->subscription); + if ($invoice->billing_reason === StripeInvoice::BILLING_REASON_SUBSCRIPTION_CREATE) { + /** @var Invest */ + $invest = Invest::get($invoice->lines->data[0]->price->metadata->invest); + + $invest->setPayment($subscription->id); + $invest->setTransaction($invoice->id); + + return new JsonResponse(['data' => $invest], Response::HTTP_OK); + } + $invest = new Invest([ 'amount' => $invoice->amount_paid / 100, 'donate_amount' => 0, @@ -104,7 +100,8 @@ private function processInvoice(string $invoiceId): JsonResponse 'method' => StripeSubscriptionPaymentMethod::PAYMENT_METHOD_ID, 'status' => Invest::STATUS_CHARGED, 'invested' => date('Y-m-d'), - 'payment' => $subscription->id + 'payment' => $subscription->id, + 'transaction' => $invoice->id ]); $errors = array(); diff --git a/src/Goteo/Library/Buzz.php b/src/Goteo/Library/Buzz.php index d7c5283604..82edca2b1d 100644 --- a/src/Goteo/Library/Buzz.php +++ b/src/Goteo/Library/Buzz.php @@ -55,7 +55,7 @@ public static function getTweets( $query , $matchusers = false) { if ($doReq) { // autenticación (application-only) if (empty(self::$twitter_id) || empty(self::$twitter_secret)) { - throw new Exception("Faltan credenciales para twitter, OAUTH_TWITTER_ID y OAUTH_TWITTER_SECRET en config.php"); + throw new \Exception("Faltan credenciales para twitter, OAUTH_TWITTER_ID y OAUTH_TWITTER_SECRET en config.php"); } $credentials = base64_encode(rawurlencode(self::$twitter_id).':'.rawurlencode(self::$twitter_secret)); $grantstr = "grant_type=client_credentials"; diff --git a/src/Goteo/Model/FormHoneypot.php b/src/Goteo/Model/FormHoneypot.php new file mode 100644 index 0000000000..9dcf6b39fe --- /dev/null +++ b/src/Goteo/Model/FormHoneypot.php @@ -0,0 +1,81 @@ +validate($errors)) return false; + + $this->dbInsertUpdate(['id', 'trap', 'prey', 'template', 'datetime']); + } + + public function validate(&$errors = array()) + { + if (empty($errors)) + return true; + else + return false; + } + + /** + * Get a trapped form field that is invisible to humans and juicy for robots to fill + */ + public static function layTrap() + { + $honeypot = new FormHoneypot; + $honeypot->trap = "email_addr_confirm"; + $honeypot->prey = ""; + $honeypot->datetime = new \DateTime(); + $honeypot->params = [ + 'trap' => $honeypot->trap, + 'prey' => $honeypot->prey + ]; + + return $honeypot; + } + + /** + * Checks if something got caught in the trap + * @return bool `true` if caught something, `false` if not + */ + public static function checkTrap(string $trap, $data): bool + { + if ($data instanceof Request) { + return $data->request->get($trap) !== ""; + } + + return false; + } +} diff --git a/src/Goteo/Model/Invest.php b/src/Goteo/Model/Invest.php index 92666314eb..3049a10c49 100644 --- a/src/Goteo/Model/Invest.php +++ b/src/Goteo/Model/Invest.php @@ -19,6 +19,7 @@ use Goteo\Library\Text; use Goteo\Model\Invest\InvestLocation; use Goteo\Model\Project\Reward; +use Goteo\Payment\Method\StripeSubscriptionPaymentMethod; use Goteo\Payment\Payment; use Goteo\Repository\InvestOriginRepository; @@ -1289,7 +1290,8 @@ public static function investors ($project, $projNum = false, $showall = false, invest.call as `call`, invest.matcher as `matcher`, invest.anonymous as anonymous, - invest_msg.msg as msg + invest_msg.msg as msg, + invest.method FROM invest LEFT JOIN invest_msg ON invest_msg.invest=invest.id @@ -1306,6 +1308,10 @@ public static function investors ($project, $projNum = false, $showall = false, $investor->avatar = Image::get($investor->user_avatar); + $invest = new Invest(); + $invest->method = $investor->method; + $invest->user = $investor->user; + // si el usuario es hide o el aporte es anonymo, lo ponemos como el usuario anonymous (avatar 1) if (!$showall && ($investor->hide == 1 || $investor->anonymous == 1)) { @@ -1324,7 +1330,9 @@ public static function investors ($project, $projNum = false, $showall = false, 'droped' => $investor->droped, 'campaign' => $investor->campaign, 'call' => $investor->call, - 'msg' => $investor->msg + 'msg' => $investor->msg, + 'method' => $investor->method, + 'invest' => $invest, ); } else { @@ -1341,7 +1349,9 @@ public static function investors ($project, $projNum = false, $showall = false, 'campaign' => $investor->campaign, 'call' => $investor->call, 'matcher' => $investor->matcher, - 'msg' => $investor->msg + 'msg' => $investor->msg, + 'method' => $investor->method, + 'invest' => $invest ); } diff --git a/src/Goteo/Model/Node/NodeProject.php b/src/Goteo/Model/Node/NodeProject.php index 50357d12de..8c9224da3c 100644 --- a/src/Goteo/Model/Node/NodeProject.php +++ b/src/Goteo/Model/Node/NodeProject.php @@ -25,9 +25,9 @@ static public function get($id): NodeProject } /** - * @return NodeProject[] + * @return NodeProject[] | int */ - static public function getList(array $filters = [], int $offset = 0, int $limit = 10, bool $count = false, string $lang = null): array + static public function getList(array $filters = [], int $offset = 0, int $limit = 10, bool $count = false, string $lang = null) { $filter = []; $values = []; diff --git a/src/Goteo/Payment/Method/AbstractPaymentMethod.php b/src/Goteo/Payment/Method/AbstractPaymentMethod.php index 5a39dcec38..4e60fb7037 100644 --- a/src/Goteo/Payment/Method/AbstractPaymentMethod.php +++ b/src/Goteo/Payment/Method/AbstractPaymentMethod.php @@ -346,4 +346,9 @@ public function isInternal(): bool { return false; } + + public function isSubscription(): bool + { + return false; + } } diff --git a/src/Goteo/Payment/Method/PaymentMethodInterface.php b/src/Goteo/Payment/Method/PaymentMethodInterface.php index 2c5fc08524..e6564d5e60 100644 --- a/src/Goteo/Payment/Method/PaymentMethodInterface.php +++ b/src/Goteo/Payment/Method/PaymentMethodInterface.php @@ -127,4 +127,9 @@ public function calculateCommission($total_invests, $total_amount, $returned_inv * (pool) */ public function isInternal(): bool; + + /** + * Subscription payments are charged recurrently + */ + public function isSubscription(): bool; } diff --git a/src/Goteo/Payment/Method/PaypalPaymentMethod.php b/src/Goteo/Payment/Method/PaypalPaymentMethod.php index 84a8157a03..f9312afbb6 100644 --- a/src/Goteo/Payment/Method/PaypalPaymentMethod.php +++ b/src/Goteo/Payment/Method/PaypalPaymentMethod.php @@ -11,9 +11,12 @@ namespace Goteo\Payment\Method; use Goteo\Application\Currency; +use Goteo\Model\Project; use Omnipay\Common\Message\ResponseInterface; +use Omnipay\PayPal\ExpressGateway; -class PaypalPaymentMethod extends AbstractPaymentMethod { +class PaypalPaymentMethod extends AbstractPaymentMethod +{ public function getGatewayName(): string { @@ -22,19 +25,43 @@ public function getGatewayName(): string public function purchase(): ResponseInterface { + /** @var ExpressGateway */ $gateway = $this->getGateway(); + $invest = $this->getInvest(); + + $transactionId = sprintf("0000000000-%s", $invest->id); + if ($invest->project) { + $project = Project::get($invest->project); + $transactionId = sprintf("%s-%s", $project->getNumericId(), $invest->id); + } + + $invest->setPreapproval($transactionId); // You can specify your paypal gateway details in config/settings.yml - if(!$gateway->getLogoImageUrl()) $gateway->setLogoImageUrl(SRC_URL . '/goteo_logo.png'); + if (!$gateway->getLogoImageUrl()) $gateway->setLogoImageUrl(SRC_URL . '/goteo_logo.png'); + + $gateway->setCurrency(Currency::getDefault('id')); + + $request = $gateway->purchase([ + 'amount' => (float) $this->getTotalAmount(), + 'currency' => $gateway->getCurrency(), + 'description' => $this->getInvestDescription(), + 'returnUrl' => $this->getCompleteUrl(), + 'cancelUrl' => $this->getCompleteUrl(), + 'transactionId' => $transactionId, + ]); - return parent::purchase(); + return $request->send(); } public function completePurchase(): ResponseInterface { + /** @var ExpressGateway */ $gateway = $this->getGateway(); $invest = $this->getInvest(); + $gateway->setCurrency(Currency::getDefault('id')); + $payment = $gateway->completePurchase([ 'amount' => (float) $this->getTotalAmount(), 'description' => $this->getInvestDescription(), @@ -49,5 +76,4 @@ public function completePurchase(): ResponseInterface return $payment->send(); } - } diff --git a/src/Goteo/Payment/Method/StripeSubscriptionPaymentMethod.php b/src/Goteo/Payment/Method/StripeSubscriptionPaymentMethod.php index 0aa92d75ad..283a095df4 100644 --- a/src/Goteo/Payment/Method/StripeSubscriptionPaymentMethod.php +++ b/src/Goteo/Payment/Method/StripeSubscriptionPaymentMethod.php @@ -68,27 +68,24 @@ public function getGateway(): SubscriptionGateway public function purchase(): ResponseInterface { - $response = $this->getGateway()->purchase([ + return $this->getGateway()->purchase([ 'invest' => $this->invest, 'user' => $this->user ])->send(); + } + + public function completePurchase(): ResponseInterface + { + $response = $this->getGateway()->completePurchase(); /** @var Subscription */ - $subscription = $response->getData(); + $subscription = $response->getData()['subscription']; $this->invest->setPayment($subscription->id); return $response; } - public function completePurchase(): ResponseInterface - { - /** @var SubscriptionGateway */ - $gateway = $this->getGateway(); - - return $gateway->completePurchase(); - } - public function refundable(): bool { return false; @@ -103,4 +100,9 @@ public function isInternal(): bool { return false; } + + public function isSubscription(): bool + { + return true; + } } diff --git a/src/Goteo/Payment/Payment.php b/src/Goteo/Payment/Payment.php index d3bb7c1277..c511cf13d1 100644 --- a/src/Goteo/Payment/Payment.php +++ b/src/Goteo/Payment/Payment.php @@ -15,6 +15,7 @@ use Goteo\Model\user; use Goteo\Payment\Method\PaymentMethodInterface; +use Goteo\Payment\Method\StripeSubscriptionPaymentMethod; /** * A statically defined class to manage payments @@ -124,4 +125,30 @@ static public function defaultMethod($method = null) { } return self::$default_method; } + + /** + * @return PaymentMethodInterface[] + */ + static public function getSubscriptionMethods(): array + { + return array_filter(self::$methods, function($method) { + switch (\get_class($method)) { + case StripeSubscriptionPaymentMethod::class: + return true; + default: + return false; + break; + } + }); + } + + static public function isSubscriptionMethod(string $method): bool + { + if (!self::getMethod($method)) return false; + + $name = $method; + return 0 < count(array_filter(self::getSubscriptionMethods(), function ($method) use ($name) { + return $method::getId() === $name; + })); + } } diff --git a/src/Goteo/Repository/InvestRepository.php b/src/Goteo/Repository/InvestRepository.php index 1797d87fc8..2521630170 100644 --- a/src/Goteo/Repository/InvestRepository.php +++ b/src/Goteo/Repository/InvestRepository.php @@ -34,4 +34,13 @@ public function getListByPayment(string $payment): array return $this->query($sql, [$payment])->fetchAll(\PDO::FETCH_CLASS, Invest::class); } + + public function getListByTransaction(string $transaction): array + { + $sql = "SELECT * + FROM invest + WHERE invest.transaction = ?"; + + return $this->query($sql, [$transaction])->fetchAll(\PDO::FETCH_CLASS, Invest::class); + } } diff --git a/src/Omnipay/Stripe/Subscription/Message/DonationResponse.php b/src/Omnipay/Stripe/Subscription/Message/DonationResponse.php index e964fb8d4b..e434f06270 100644 --- a/src/Omnipay/Stripe/Subscription/Message/DonationResponse.php +++ b/src/Omnipay/Stripe/Subscription/Message/DonationResponse.php @@ -2,25 +2,20 @@ namespace Omnipay\Stripe\Subscription\Message; -use Goteo\Application\Config; use Omnipay\Common\Message\AbstractResponse; use Omnipay\Common\Message\RedirectResponseInterface; use Omnipay\Common\Message\RequestInterface; use Stripe\Checkout\Session as StripeSession; -use Stripe\StripeClient; class DonationResponse extends AbstractResponse implements RedirectResponseInterface { - private StripeClient $stripe; - private StripeSession $checkout; - public function __construct(RequestInterface $request, string $checkoutSessionId) + public function __construct(RequestInterface $request, StripeSession $checkout) { - parent::__construct($request, $checkoutSessionId); + parent::__construct($request, ['checkout' => $checkout]); - $this->stripe = new StripeClient(Config::get('payments.stripe.secretKey')); - $this->checkout = $this->stripe->checkout->sessions->retrieve($checkoutSessionId); + $this->checkout = $checkout; } public function isSuccessful() diff --git a/src/Omnipay/Stripe/Subscription/Message/SubscriptionRequest.php b/src/Omnipay/Stripe/Subscription/Message/SubscriptionRequest.php index 9e3ee57a17..a4c1cefaba 100644 --- a/src/Omnipay/Stripe/Subscription/Message/SubscriptionRequest.php +++ b/src/Omnipay/Stripe/Subscription/Message/SubscriptionRequest.php @@ -38,29 +38,34 @@ public function sendData($data) $user = $data['user']; $invest = $data['invest']; - /** @var Project */ - $project = $invest->getProject(); - $customer = $this->getStripeCustomer($user)->id; - $metadata = $this->getMetadata($project, $invest, $user); + $metadata = $this->getMetadata($invest); - $successUrl = sprintf('%s?session_id={CHECKOUT_SESSION_ID}', $this->getRedirectUrl( - 'invest', - $project->id, - $invest->id, - 'complete' - )); + $successUrl = $this->getRedirectUrl('pool', $invest->id, 'complete'); + if ($invest->getProject()) { + $successUrl = $this->getRedirectUrl( + 'invest', + $metadata['project'], + $invest->id, + 'complete' + ); + } + + $redirectUrl = $this->getRedirectUrl('dashboard', 'wallet'); + if ($invest->getProject()) { + $redirectUrl = $this->getRedirectUrl('project', $metadata['project']); + } - $session = $this->stripe->checkout->sessions->create([ + $checkout = $this->stripe->checkout->sessions->create([ 'customer' => $customer, - 'success_url' => $successUrl, - 'cancel_url' => $this->getRedirectUrl('project', $project->id), + 'success_url' => sprintf('%s?session_id={CHECKOUT_SESSION_ID}', $successUrl), + 'cancel_url' => $redirectUrl, 'mode' => CheckoutSession::MODE_SUBSCRIPTION, 'line_items' => [ [ 'price' => $this->stripe->prices->create([ 'unit_amount' => $invest->amount * 100, - 'currency' => $project->currency, + 'currency' => $this->getStripeCurrency($invest, $user), 'recurring' => ['interval' => 'month'], 'product' => $this->getStripeProduct($invest)->id, 'metadata' => $metadata @@ -71,59 +76,68 @@ public function sendData($data) 'metadata' => $metadata ]); - return new SubscriptionResponse($this, $session->id); + return new SubscriptionResponse($this, $checkout); } public function completePurchase(array $options = []) { // Dirty sanitization because something is double concatenating the ?session_id query param $sessionId = explode('?', $_REQUEST['session_id'])[0]; - $session = $this->stripe->checkout->sessions->retrieve($sessionId); - $metadata = $session->metadata->toArray(); + $checkout = $this->stripe->checkout->sessions->retrieve($sessionId); + $metadata = $checkout->metadata->toArray(); - if ($session->subscription) { - $this->stripe->subscriptions->update( - $session->subscription, - [ - 'metadata' => $metadata - ] - ); - - if ($metadata['donate_amount'] < 1) { - return new SubscriptionResponse($this, $session->id); - } + if (!$checkout->subscription) { + throw new \Exception("Could not retrieve Subscription from Stripe after checkout"); + } - $donation = $this->stripe->checkout->sessions->create([ - 'customer' => $this->getStripeCustomer(User::get($metadata['user']))->id, - 'success_url' => sprintf('%s?session_id={CHECKOUT_SESSION_ID}', $this->getRedirectUrl( - 'invest', - $metadata['project'], - $metadata['invest'], - 'complete' - )), - 'cancel_url' => $this->getRedirectUrl('project', $metadata['project']->id), - 'mode' => CheckoutSession::MODE_PAYMENT, - 'line_items' => [ - [ - 'price' => $this->stripe->prices->create([ - 'unit_amount' => $metadata['donate_amount'] * 100, - 'currency' => Config::get('currency'), - 'product_data' => [ - 'name' => Text::get('donate-meta-description') - ] - ])->id, - 'quantity' => 1 - ] - ], + $subscription = $this->stripe->subscriptions->retrieve($checkout->subscription); + $this->stripe->subscriptions->update( + $checkout->subscription, + [ 'metadata' => $metadata - ]); - - return new DonationResponse($this, $donation->id); + ] + ); + + if ($metadata['donate_amount'] < 1) { + return new SubscriptionResponse($this, $checkout, $subscription); } - if ($session->payment_intent) { - return new SubscriptionResponse($this, $session->id); + $successUrl = $this->getRedirectUrl('pool', $metadata['invest']); + if ($metadata['project'] !== '') { + $successUrl = $this->getRedirectUrl( + 'invest', + $metadata['project'], + $metadata['invest'], + 'complete' + ); } + + $cancelUrl = $this->getRedirectUrl('dashboard', 'wallet'); + if ($metadata['project'] !== '') { + $cancelUrl = $this->getRedirectUrl('project', $metadata['project']); + } + + $donationCheckout = $this->stripe->checkout->sessions->create([ + 'customer' => $this->getStripeCustomer(User::get($metadata['user']))->id, + 'success_url' => sprintf('%s?session_id={CHECKOUT_SESSION_ID}', $successUrl), + 'cancel_url' => $cancelUrl, + 'mode' => CheckoutSession::MODE_PAYMENT, + 'line_items' => [ + [ + 'price' => $this->stripe->prices->create([ + 'unit_amount' => $metadata['donate_amount'] * 100, + 'currency' => Config::get('currency'), + 'product_data' => [ + 'name' => Text::get('donate-meta-description') + ] + ])->id, + 'quantity' => 1 + ] + ], + 'metadata' => $metadata + ]); + + return new DonationResponse($this, $donationCheckout); } private function getRedirectUrl(...$args): string @@ -167,27 +181,21 @@ private function getStripeCustomer(User $user): Customer private function getStripeProduct(Invest $invest): Product { - /** @var User */ - $user = $invest->getUser(); - - /** @var Project */ - $project = $invest->getProject(); - - $productId = sprintf( - '%s_%s_%s', - $project->id, - $this->getInvestReward($invest, 'noreward'), - $user->id, - ); + $productId = $this->getProductId($invest); try { return $this->stripe->products->retrieve($productId); } catch (\Stripe\Exception\InvalidRequestException $e) { - $productDescription = sprintf( - '%s - %s', - $project->name, - $this->getInvestReward($invest, Text::get('invest-resign')) - ); + if ($project = $invest->getProject()) { + $productDescription = sprintf( + '%s - %s', + $project->name, + $this->getInvestReward($invest, Text::get('invest-resign')) + ); + } else { + $productDescription = Text::get('invest-pool-method'); + } + return $this->stripe->products->create([ 'id' => $productId, @@ -197,14 +205,70 @@ private function getStripeProduct(Invest $invest): Product } } - private function getMetadata(Project $project, Invest $invest, User $user): array + private function getMetadata(Invest $invest): array { + /** @var Project */ + $project = $invest->getProject(); + /** @var User */ + $user = $invest->getUser(); + + $projectId = ($project) ? $project->id : null; + return [ 'donate_amount' => $invest->donate_amount, - 'project' => $project->id, + 'project' => $projectId, 'invest' => $invest->id, 'reward' => $this->getInvestReward($invest, ''), 'user' => $user->id, ]; } + + private function getProductId(Invest $invest): string + { + if ($project = $invest->getProject()) + return $this->getProductWithProjectId($invest, $project); + + return $this->getProductWithoutProjectId($invest); + } + + private function getProductWithProjectId(Invest $invest, Project $project): string + { + /** @var User */ + $user = $invest->getUser(); + + return sprintf( + '%s_%s_%s', + $project->id, + $this->getInvestReward($invest, 'noreward'), + $user->id, + ); + } + + private function getProductWithoutProjectId(Invest $invest): string + { + /** @var User */ + $user = $invest->getUser(); + + return sprintf( + '%s_%s', + $invest->id, + $user->id + ); + } + + private function getStripeCurrency(Invest $invest, User $user): string + { + if ($project = $invest->getProject()) { + return $project->currency; + } + + /** @var stdClass */ + $preferences = User::getPreferences($user); + + if (\property_exists($preferences, 'currency')) { + return $preferences->currency; + } + + return Config::get('currency'); + } } diff --git a/src/Omnipay/Stripe/Subscription/Message/SubscriptionResponse.php b/src/Omnipay/Stripe/Subscription/Message/SubscriptionResponse.php index fd4c2f9f2f..3d9cb0d6ab 100644 --- a/src/Omnipay/Stripe/Subscription/Message/SubscriptionResponse.php +++ b/src/Omnipay/Stripe/Subscription/Message/SubscriptionResponse.php @@ -2,25 +2,24 @@ namespace Omnipay\Stripe\Subscription\Message; -use Goteo\Application\Config; use Omnipay\Common\Message\AbstractResponse; use Omnipay\Common\Message\RedirectResponseInterface; use Omnipay\Common\Message\RequestInterface; use Stripe\Checkout\Session as StripeSession; -use Stripe\StripeClient; +use Stripe\Subscription; class SubscriptionResponse extends AbstractResponse implements RedirectResponseInterface { - private StripeClient $stripe; - private StripeSession $checkout; - public function __construct(RequestInterface $request, string $checkoutSessionId) - { - parent::__construct($request, $checkoutSessionId); + public function __construct( + RequestInterface $request, + StripeSession $checkout, + ?Subscription $subscription = null + ) { + parent::__construct($request, ['checkout' => $checkout, 'subscription' => $subscription]); - $this->stripe = new StripeClient(Config::get('payments.stripe.secretKey')); - $this->checkout = $this->stripe->checkout->sessions->retrieve($checkoutSessionId); + $this->checkout = $checkout; } public function isSuccessful() @@ -37,4 +36,9 @@ public function getRedirectUrl() { return $this->checkout->url; } + + public function getTransactionReference() + { + return $this->checkout->invoice; + } } diff --git a/translations/ca/dashboard.yml b/translations/ca/dashboard.yml index 592f68ad50..7618719fb2 100644 --- a/translations/ca/dashboard.yml +++ b/translations/ca/dashboard.yml @@ -73,7 +73,7 @@ dashboard-menu-profile-personal: 'Dades personals' dashboard-menu-profile-preferences: 'Preferències' dashboard-menu-profile-profile: 'Edita el Perfil' dashboard-menu-profile-public: 'Perfil públic' -dashboard-menu-project-nid: 'Identificador numèric de projecte' +dashboard-menu-project-nid: 'Número de seguiment' dashboard-menu-projects: 'Projectes' dashboard-menu-projects-analytics: Analítica dashboard-menu-projects-commons: Retorns @@ -242,6 +242,7 @@ dashboard-project-filter-by-drop: 'Veure només les aportacions de matchfunding' dashboard-project-filter-by-nondrop: 'Amaga les aportacions de matchfunding' dashboard-project-filter-by-pending: 'Veure només les pendents' dashboard-project-filter-by-fulfilled: 'Veure només les completades' +dashboard-project-filter-by-subscription: 'Veure només les aportacions des de suscripcions' dashboard-project-no-invests: 'No hi ha aportacions per a aquest criteri de cerca' dashboard-new-message-to-donors: 'Nou missatge als cofinançadors' dashboard-message-donors-reward: 'Cofinançadors amb la recompensa %s' diff --git a/translations/ca/project.yml b/translations/ca/project.yml index 8d6cdeeb36..34edbf2eb6 100644 --- a/translations/ca/project.yml +++ b/translations/ca/project.yml @@ -23,6 +23,7 @@ project-help-license: 'Necessito ajuda per definir els retorns col·lectius i le project-hide-donors: 'Amagar cofinançadors' project-hide-needs: 'Amagar llistat de necessitats' project-invest: Aportació +project-invest-from-subscription: Subscripció project-invest-msg: 'Missatge de suport:' project-langs-header: 'Projecte en:' project-media-play_video: 'Veure vídeo' diff --git a/translations/ca/roles.yml b/translations/ca/roles.yml index 9f03171c85..b099ea618c 100644 --- a/translations/ca/roles.yml +++ b/translations/ca/roles.yml @@ -6,6 +6,7 @@ role-name-translator: 'Traductor de continguts' role-name-checker: 'Revisor de projectes' role-name-stats: 'Accés a stats' role-name-manager: 'Gestor de contractes' +role-name-helper: 'Ajudant' role-name-consultant: 'Assessor de projectes' role-name-admin: 'Administrador d''assessors' role-name-superadmin: 'Super-Administrador' diff --git a/translations/en/dashboard.yml b/translations/en/dashboard.yml index 43d50d5737..01b0ea75a9 100644 --- a/translations/en/dashboard.yml +++ b/translations/en/dashboard.yml @@ -241,6 +241,7 @@ dashboard-project-filter-by-drop: 'Show only matchfunding donations' dashboard-project-filter-by-nondrop: 'Hide matchfunding donations' dashboard-project-filter-by-pending: 'Show only pending' dashboard-project-filter-by-fulfilled: 'Show only completed' +dashboard-project-filter-by-subscription: 'Show only donations from subscriptions' dashboard-project-no-invests: 'No entries for the current search criteria' dashboard-new-message-to-donors: 'New message to donors' dashboard-message-donors-reward: 'Donors with the reward %s' diff --git a/translations/en/project.yml b/translations/en/project.yml index 9989910526..47dbed1da8 100644 --- a/translations/en/project.yml +++ b/translations/en/project.yml @@ -20,6 +20,7 @@ project-help-license: 'I need help with collective returns and licenses' project-hide-donors: 'Hide donors' project-hide-needs: 'Hide list of needs' project-invest: Donation +project-invest-from-subscription: Subscription project-invest-msg: 'Support message:' project-langs-header: 'Project in:' project-media-play_video: 'Watch video' diff --git a/translations/es/dashboard.yml b/translations/es/dashboard.yml index e433de0543..1fcaf3bf47 100644 --- a/translations/es/dashboard.yml +++ b/translations/es/dashboard.yml @@ -73,7 +73,7 @@ dashboard-menu-profile-personal: 'Datos personales' dashboard-menu-profile-preferences: 'Mis preferencias' dashboard-menu-profile-profile: 'Editar perfil' dashboard-menu-profile-public: 'Perfil público' -dashboard-menu-project-nid: 'Identificador numérico de proyecto' +dashboard-menu-project-nid: 'Número de seguimiento' dashboard-menu-projects: 'Mis proyectos' dashboard-menu-projects-analytics: Analítica dashboard-menu-projects-commons: Retornos @@ -243,6 +243,7 @@ dashboard-project-filter-by-drop: 'Ver sólo aportes de matchfunding' dashboard-project-filter-by-nondrop: 'Esconder aportes de matchfunding' dashboard-project-filter-by-pending: 'Ver sólo las pendientes' dashboard-project-filter-by-fulfilled: 'Ver sólo las completadas' +dashboard-project-filter-by-subscription: 'Ver sólo aportes desde subscripciones' dashboard-project-no-invests: 'No hay aportes para este criterio de búsqueda' dashboard-new-message-to-donors: 'Nuevo mensaje a cofinanciadores' dashboard-message-donors-reward: 'Los cofinanciadores con la recompensa %s' diff --git a/translations/es/project.yml b/translations/es/project.yml index 46cac0be01..12408136dd 100644 --- a/translations/es/project.yml +++ b/translations/es/project.yml @@ -23,6 +23,7 @@ project-help-license: 'Necesito ayuda para definir los retornos colectivos y las project-hide-donors: 'Ocultar cofinanciadores' project-hide-needs: 'Ocultar listado de necesidades' project-invest: Aportación +project-invest-from-subscription: Suscripción project-invest-msg: 'Mensaje de apoyo:' project-langs-header: 'Proyecto en' project-media-play_video: 'Ver video' diff --git a/translations/es/roles.yml b/translations/es/roles.yml index 616b3612e0..b409e3b988 100644 --- a/translations/es/roles.yml +++ b/translations/es/roles.yml @@ -14,6 +14,7 @@ role-name-superadmin: 'Super Administrador' role-name-owner: 'Impulsor' role-name-root: 'Root' role-name-matcher: 'Matcher' +role-name-helper: 'Ayudante' role-perm-name-create-project: 'Puede crear proyectos' role-perm-name-edit-project: 'Puede editar sus proyectos'