= amount_format($invest->amount) ?>
@@ -93,7 +93,7 @@
- = $this->text('project-invest') ?>
+ = $invest->invest->getMethod()->isSubscription() ? $this->text('project-invest-from-subscription') : $this->text('project-invest') ?>
= amount_format($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'