Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117906447
D1249.1775390116.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
53 KB
Referenced Files
None
Subscribers
None
D1249.1775390116.diff
View Options
diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php
--- a/src/app/Console/Commands/WalletCharge.php
+++ b/src/app/Console/Commands/WalletCharge.php
@@ -2,11 +2,7 @@
namespace App\Console\Commands;
-use App\Domain;
-use App\User;
-use Carbon\Carbon;
use Illuminate\Console\Command;
-use Illuminate\Support\Facades\DB;
class WalletCharge extends Command
{
@@ -44,19 +40,15 @@
$wallets = \App\Wallet::all();
foreach ($wallets as $wallet) {
- $charge = $wallet->expectedCharges();
+ $charge = $wallet->chargeEntitlements();
if ($charge > 0) {
$this->info(
- "charging wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}"
+ "Charged wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}"
);
- $wallet->chargeEntitlements();
-
- if ($wallet->balance < 0) {
- // Disabled for now
- // \App\Jobs\WalletPayment::dispatch($wallet);
- }
+ // Top-up the wallet if auto-payment enabled for the wallet
+ \App\Jobs\WalletCharge::dispatch($wallet);
}
}
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -42,7 +42,7 @@
$result['provider'] = $provider->name();
$result['providerLink'] = $provider->customerLink($wallet);
- return $result;
+ return response()->json($result);
}
/**
diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/V4/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -222,15 +222,43 @@
}
/**
- * Charge a wallet with a "recurring" payment.
+ * Top up a wallet with a "recurring" payment.
*
* @param \App\Wallet $wallet The wallet to charge
- * @param int $amount The amount of money in cents
*
- * @return bool
+ * @return bool True if the payment has been initialized
*/
- public static function directCharge(Wallet $wallet, $amount): bool
+ public static function topUpWallet(Wallet $wallet): bool
{
+ if ((bool) $wallet->getSetting('mandate_disabled')) {
+ return false;
+ }
+
+ $min_balance = (int) (floatval($wallet->getSetting('mandate_balance')) * 100);
+ $amount = (int) (floatval($wallet->getSetting('mandate_amount')) * 100);
+
+ // The wallet balance is greater than the auto-payment threshold
+ if ($wallet->balance >= $min_balance) {
+ // Do nothing
+ return false;
+ }
+
+ // The defined top-up amount is not enough
+ // Disable auto-payment and notify the user
+ if ($wallet->balance + $amount < 0) {
+ // Disable (not remove) the mandate
+ $wallet->setSetting('mandate_disabled', 1);
+ \App\Jobs\PaymentMandateDisabledEmail::dispatch($wallet);
+ return false;
+ }
+
+ $provider = PaymentProvider::factory($wallet);
+ $mandate = (array) $provider->getMandate($wallet);
+
+ if (empty($mandate['isValid'])) {
+ return false;
+ }
+
$request = [
'type' => PaymentProvider::TYPE_RECURRING,
'currency' => 'CHF',
@@ -238,13 +266,9 @@
'description' => \config('app.name') . ' Recurring Payment',
];
- $provider = PaymentProvider::factory($wallet);
-
- if ($result = $provider->payment($wallet, $request)) {
- return true;
- }
+ $result = $provider->payment($wallet, $request);
- return false;
+ return !empty($result);
}
/**
@@ -263,6 +287,7 @@
$mandate['amount'] = (int) (PaymentProvider::MIN_AMOUNT / 100);
$mandate['balance'] = 0;
+ $mandate['isDisabled'] = (bool) $wallet->getSetting('mandate_disabled');
foreach (['amount', 'balance'] as $key) {
if (($value = $wallet->getSetting("mandate_{$key}")) !== null) {
diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -45,7 +45,7 @@
/**
* Display the specified resource.
*
- * @param int $id
+ * @param string $id
*
* @return \Illuminate\Http\JsonResponse
*/
diff --git a/src/app/Jobs/PaymentEmail.php b/src/app/Jobs/PaymentEmail.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/PaymentEmail.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Payment;
+use App\Providers\PaymentProvider;
+use App\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+
+class PaymentEmail implements ShouldQueue
+{
+ use Dispatchable;
+ use InteractsWithQueue;
+ use Queueable;
+ use SerializesModels;
+
+ /** @var int The number of times the job may be attempted. */
+ public $tries = 2;
+
+ /** @var int The number of seconds to wait before retrying the job. */
+ public $retryAfter = 10;
+
+ /** @var bool Delete the job if the wallet no longer exist. */
+ public $deleteWhenMissingModels = true;
+
+ /** @var \App\Payment A payment object */
+ protected $payment;
+
+ /** @var \App\User A wallet controller */
+ protected $controller;
+
+
+ /**
+ * Create a new job instance.
+ *
+ * @param \App\Payment $payment A payment object
+ * @param \App\User $controller A wallet controller
+ *
+ * @return void
+ */
+ public function __construct(Payment $payment, User $controller = null)
+ {
+ $this->payment = $payment;
+ $this->controller = $controller;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $wallet = $this->payment->wallet;
+
+ if (empty($this->controller)) {
+ $this->controller = $wallet->owner;
+ }
+
+ $ext_email = $this->controller->getSetting('external_email');
+ $cc = [];
+
+ if ($ext_email && $ext_email != $this->controller->email) {
+ $cc[] = $ext_email;
+ }
+
+ if ($this->payment->status == PaymentProvider::STATUS_PAID) {
+ $mail = new \App\Mail\PaymentSuccess($this->payment, $this->controller);
+ } elseif (
+ $this->payment->status == PaymentProvider::STATUS_EXPIRED
+ || $this->payment->status == PaymentProvider::STATUS_FAILED
+ ) {
+ $mail = new \App\Mail\PaymentFailure($this->payment, $this->controller);
+ } else {
+ return;
+ }
+
+ Mail::to($this->controller->email)->cc($cc)->send($mail);
+
+ /*
+ // Send the email to all wallet controllers too
+ if ($wallet->owner->id == $this->controller->id) {
+ $this->wallet->controllers->each(function ($controller) {
+ self::dispatch($this->payment, $controller);
+ }
+ });
+ */
+ }
+}
diff --git a/src/app/Jobs/PaymentMandateDisabledEmail.php b/src/app/Jobs/PaymentMandateDisabledEmail.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/PaymentMandateDisabledEmail.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Mail\PaymentMandateDisabled;
+use App\User;
+use App\Wallet;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Support\Facades\Mail;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Queue\InteractsWithQueue;
+
+class PaymentMandateDisabledEmail implements ShouldQueue
+{
+ use Dispatchable;
+ use InteractsWithQueue;
+ use Queueable;
+ use SerializesModels;
+
+ /** @var int The number of times the job may be attempted. */
+ public $tries = 2;
+
+ /** @var int The number of seconds to wait before retrying the job. */
+ public $retryAfter = 10;
+
+ /** @var bool Delete the job if the wallet no longer exist. */
+ public $deleteWhenMissingModels = true;
+
+ /** @var \App\Wallet A wallet object */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller */
+ protected $controller;
+
+
+ /**
+ * Create a new job instance.
+ *
+ * @param \App\Wallet $wallet A wallet object
+ * @param \App\User $controller An email recipient (wallet controller)
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet, User $controller = null)
+ {
+ $this->wallet = $wallet;
+ $this->controller = $controller;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ if (empty($this->controller)) {
+ $this->controller = $this->wallet->owner;
+ }
+
+ $ext_email = $this->controller->getSetting('external_email');
+ $cc = [];
+
+ if ($ext_email && $ext_email != $this->controller->email) {
+ $cc[] = $ext_email;
+ }
+
+ $mail = new PaymentMandateDisabled($this->wallet, $this->controller);
+
+ Mail::to($this->controller->email)->cc($cc)->send($mail);
+
+ /*
+ // Send the email to all controllers too
+ if ($this->controller->id == $this->wallet->owner->id) {
+ $this->wallet->controllers->each(function ($controller) {
+ self::dispatch($this->wallet, $controller);
+ }
+ });
+ */
+ }
+}
diff --git a/src/app/Jobs/WalletPayment.php b/src/app/Jobs/WalletCharge.php
rename from src/app/Jobs/WalletPayment.php
rename to src/app/Jobs/WalletCharge.php
--- a/src/app/Jobs/WalletPayment.php
+++ b/src/app/Jobs/WalletCharge.php
@@ -10,25 +10,30 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
-class WalletPayment implements ShouldQueue
+class WalletCharge implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
+ /** @var \App\Wallet A wallet object */
protected $wallet;
+ /** @var int The number of seconds to wait before retrying the job. */
+ public $retryAfter = 10;
+
+ /** @var int How many times retry the job if it fails. */
public $tries = 5;
- /** @var bool Delete the job if its models no longer exist. */
+ /** @var bool Delete the job if the wallet no longer exist. */
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*
- * @param \App\Wallet $wallet The wallet to charge.
+ * @param \App\Wallet $wallet The wallet that has been charged.
*
* @return void
*/
@@ -44,8 +49,6 @@
*/
public function handle()
{
- if ($this->wallet->balance < 0) {
- PaymentsController::directCharge($this->wallet, $this->wallet->balance * -1);
- }
+ PaymentsController::topUpWallet($this->wallet);
}
}
diff --git a/src/app/Mail/PaymentFailure.php b/src/app/Mail/PaymentFailure.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/PaymentFailure.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Mail;
+
+use App\Payment;
+use App\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class PaymentFailure extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @var \App\Payment A payment operation */
+ protected $payment;
+
+ /** @var \App\User A wallet controller to whom the email is being send */
+ protected $user;
+
+
+ /**
+ * Create a new message instance.
+ *
+ * @param \App\Payment $payment A payment operation
+ * @param \App\User $user An email recipient
+ *
+ * @return void
+ */
+ public function __construct(Payment $payment, User $user)
+ {
+ $this->payment = $payment;
+ $this->user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $user = $this->user;
+
+ $subject = \trans('mail.paymentfailure-subject', ['site' => \config('app.name')]);
+
+ $this->view('email.payment_failure')
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'subject' => $subject,
+ 'username' => $user->name(true),
+ // 'balance' => $this->wallet->money($this->wallet->balance),
+ 'wallet_link' => '' // TODO
+ ]);
+
+ return $this;
+ }
+}
diff --git a/src/app/Mail/PaymentMandateDisabled.php b/src/app/Mail/PaymentMandateDisabled.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/PaymentMandateDisabled.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Mail;
+
+use App\User;
+use App\Wallet;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class PaymentMandateDisabled extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @var \App\Wallet A wallet for which the mandate has been disabled */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller to whom the email is being send */
+ protected $user;
+
+
+ /**
+ * Create a new message instance.
+ *
+ * @param \App\Wallet $wallet A wallet that has been charged
+ * @param \App\User $user An email recipient
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet, User $user)
+ {
+ $this->wallet = $wallet;
+ $this->user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $user = $this->user;
+
+ $subject = \trans('mail.paymentmandatedisabled-subject', ['site' => \config('app.name')]);
+
+ $this->view('email.payment_mandate_disabled')
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'subject' => $subject,
+ 'username' => $user->name(true),
+ // 'balance' => $this->wallet->money($this->wallet->balance),
+ 'wallet_link' => '' // TODO
+ ]);
+
+ return $this;
+ }
+}
diff --git a/src/app/Mail/PaymentSuccess.php b/src/app/Mail/PaymentSuccess.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/PaymentSuccess.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Mail;
+
+use App\Payment;
+use App\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class PaymentSuccess extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @var \App\Payment A payment operation */
+ protected $payment;
+
+ /** @var \App\User A wallet controller to whom the email is being send */
+ protected $user;
+
+
+ /**
+ * Create a new message instance.
+ *
+ * @param \App\Payment $payment A payment operation
+ * @param \App\User $user An email recipient
+ *
+ * @return void
+ */
+ public function __construct(Payment $payment, User $user)
+ {
+ $this->payment = $payment;
+ $this->user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $user = $this->user;
+
+ $subject = \trans('mail.paymentsuccess-subject', ['site' => \config('app.name')]);
+
+ $this->view('email.payment_success')
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'subject' => $subject,
+ 'username' => $user->name(true),
+ // 'balance' => $this->wallet->money($this->wallet->balance),
+ 'wallet_link' => '' // TODO
+ ]);
+
+ return $this;
+ }
+}
diff --git a/src/app/Mail/WalletCharge.php b/src/app/Mail/WalletCharge.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/WalletCharge.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Mail;
+
+use App\Wallet;
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Queue\SerializesModels;
+
+class WalletCharge extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ public const TOPUP_NONE = 0;
+ public const TOPUP_INIT = 1;
+ public const TOPUP_NOTENOUGH = 2;
+
+ /** @var Wallet A wallet that has been charged */
+ protected $wallet;
+
+ /** @var array Additional parameters */
+ protected $params;
+
+
+ /**
+ * Create a new message instance.
+ *
+ * @param \App\Wallet $wallet A wallet that has been charged
+ * @param array $params Additional parameters
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet, array $params = [])
+ {
+ $this->wallet = $wallet;
+ $this->params = $params;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $user = $this->wallet->owner;
+
+ $subject = \trans('mail.walletcharge-subject', ['site' => \config('app.name')]);
+
+ $view = 'emails.walletcharge';
+
+ switch ($this->params['type'] ?? self::TOPUP_NONE) {
+ case self::TOPUP_INIT:
+ $view = 'emails.walletcharge-topup-init';
+ break;
+ case self::TOPUP_NOTENOUGH:
+ $view = 'emails.walletcharge-topup-notenough';
+ break;
+ }
+
+ $this->view($view)
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'username' => $user->name(true),
+ 'balance' => $this->wallet->money($this->params['balance'] ?? 0),
+ 'charged' => $this->wallet->money($this->params['charged'] ?? 0),
+ ]);
+
+ return $this;
+ }
+}
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -268,6 +268,7 @@
// Update the balance, if it wasn't already
if ($payment->status != self::STATUS_PAID && $payment->amount > 0) {
$payment->wallet->credit($payment->amount);
+ $notify = true;
}
} elseif ($mollie_payment->hasRefunds()) {
// The payment has been (partially) refunded.
@@ -281,15 +282,23 @@
} elseif ($mollie_payment->isFailed()) {
// Note: I didn't find a way to get any description of the problem with a payment
\Log::info(sprintf('Mollie payment failed (%s)', $payment->id));
+ $notify = true;
+ // TODO: If this was a recurring payment, should we disable the mandate?
}
// This is a sanity check, just in case the payment provider api
- // sent us open -> paid -> open -> paid. So, we lock the payment after it's paid.
- if ($payment->status != self::STATUS_PAID) {
+ // sent us open -> paid -> open -> paid. So, we lock the payment after
+ // recivied a "final" state.
+ $pending_states = [self::STATUS_OPEN, self::STATUS_PENDING, self::STATUS_AUTHORIZED];
+ if (in_array($payment->status, $pending_states)) {
$payment->status = $mollie_payment->status;
$payment->save();
}
+ if (!empty($notify)) {
+ \App\Jobs\PaymentEmail::dispatch($payment);
+ }
+
return 200;
}
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -6,6 +6,7 @@
use App\Utils;
use App\Wallet;
use App\WalletSetting;
+use Illuminate\Support\Facades\Request;
use Stripe as StripeAPI;
class Stripe extends \App\Providers\PaymentProvider
@@ -260,8 +261,11 @@
*/
public function webhook(): int
{
- $payload = file_get_contents('php://input');
- $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
+ // We cannot just use php://input as it's already "emptied" by the framework
+ // $payload = file_get_contents('php://input');
+ $request = Request::instance();
+ $payload = $request->getContent();
+ $sig_header = $request->header('Stripe-Signature');
// Parse and validate the input
try {
@@ -270,7 +274,7 @@
$sig_header,
\config('services.stripe.webhook_secret')
);
- } catch (\UnexpectedValueException $e) {
+ } catch (\Exception $e) {
// Invalid payload
return 400;
}
@@ -282,6 +286,10 @@
$intent = $event->data->object; // @phpstan-ignore-line
$payment = Payment::find($intent->id);
+ if (empty($payment)) {
+ return 404;
+ }
+
switch ($intent->status) {
case StripeAPI\PaymentIntent::STATUS_CANCELED:
$status = self::STATUS_CANCELED;
@@ -290,7 +298,7 @@
$status = self::STATUS_PAID;
break;
default:
- $status = self::STATUS_PENDING;
+ $status = self::STATUS_FAILED;
}
if ($status == self::STATUS_PAID) {
@@ -298,18 +306,24 @@
if ($payment->status != self::STATUS_PAID) {
$payment->wallet->credit($payment->amount);
}
- } elseif (!empty($intent->last_payment_error)) {
- // See https://stripe.com/docs/error-codes for more info
- \Log::info(sprintf(
- 'Stripe payment failed (%s): %s',
- $payment->id,
- json_encode($intent->last_payment_error)
- ));
+ } else {
+ if (!empty($intent->last_payment_error)) {
+ // See https://stripe.com/docs/error-codes for more info
+ \Log::info(sprintf(
+ 'Stripe payment failed (%s): %s',
+ $payment->id,
+ json_encode($intent->last_payment_error)
+ ));
+ }
}
if ($payment->status != self::STATUS_PAID) {
$payment->status = $status;
$payment->save();
+
+ if ($status != self::STATUS_CANCELED) {
+ \App\Jobs\PaymentEmail::dispatch($payment);
+ }
}
break;
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -120,6 +120,28 @@
return $this->chargeEntitlements(false);
}
+ /**
+ * A helper to display human-readable amount of money using
+ * the wallet currency and specified locale.
+ *
+ * @param int $amount A amount of money (in cents)
+ * @param string $locale A locale for the output
+ *
+ * @return string String representation, e.g. "9.99 CHF"
+ */
+ public function money(int $amount, $locale = 'de_DE')
+ {
+ $amount = round($amount / 100, 2);
+
+ // Prefer intl extension's number formatter
+ if (class_exists('NumberFormatter')) {
+ $nf = new \NumberFormatter($locale, \NumberFormatter::DECIMAL);
+ return $nf->formatCurrency($amount, $this->currency);
+ }
+
+ return sprintf('%.2f %s', $amount, $this->currency);
+ }
+
/**
* Remove a controller from this wallet.
*
diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php
--- a/src/resources/lang/en/mail.php
+++ b/src/resources/lang/en/mail.php
@@ -22,4 +22,13 @@
'signupcode-subject' => ":site Registration",
'signupcode-body' => "This is your verification code for the :site registration process: :code.\n"
. "You can also click the link below to continue the registration process:",
+
+ 'paymentmandatedisabled-subject' => ":site Auto-payment Problem",
+ 'paymentmandatedisabled-body' => "",
+
+ 'paymentfailure-subject' => ":site Payment Failed",
+ 'paymentfailure-body' => "",
+
+ 'paymentsuccess-subject' => ":site Payment Succeeded",
+ 'paymentsuccess-body' => "",
];
diff --git a/src/resources/views/emails/payment_failure.blade.php b/src/resources/views/emails/payment_failure.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/payment_failure.blade.php
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.paymentfailure-body') }}</p>
+
+ <p>{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/payment_mandate_disabled.blade.php b/src/resources/views/emails/payment_mandate_disabled.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/payment_mandate_disabled.blade.php
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.paymentmandatedisabled-body') }}</p>
+
+ <p>{!! $wallet_link !!}</p>
+
+ <p>{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/payment_success.blade.php b/src/resources/views/emails/payment_success.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/payment_success.blade.php
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.paymentsuccess-body') }}</p>
+
+ <p>{{ __('mail.footer', ['site' => $site, 'appurl' => config('app.url')]) }}</p>
+ </body>
+</html>
diff --git a/src/tests/Browser/Pages/PaymentMollie.php b/src/tests/Browser/Pages/PaymentMollie.php
--- a/src/tests/Browser/Pages/PaymentMollie.php
+++ b/src/tests/Browser/Pages/PaymentMollie.php
@@ -58,7 +58,7 @@
->waitFor('button.form__button');
}
- $browser->click('@status-table input[value="paid"]')
+ $browser->click('input[value="paid"]')
->click('button.form__button');
}
}
diff --git a/src/tests/BrowserAddonTrait.php b/src/tests/BrowserAddonTrait.php
new file mode 100644
--- /dev/null
+++ b/src/tests/BrowserAddonTrait.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Tests;
+
+use Facebook\WebDriver\Chrome\ChromeOptions;
+use Facebook\WebDriver\Remote\RemoteWebDriver;
+use Facebook\WebDriver\Remote\DesiredCapabilities;
+use Laravel\Dusk\Chrome\SupportsChrome;
+
+trait BrowserAddonTrait
+{
+ use SupportsChrome;
+
+
+ /**
+ * Initialize and start Chrome driver and browser
+ *
+ * @returns Browser The browser
+ */
+ protected function startBrowser(): Browser
+ {
+ $driver = retry(5, function () {
+ return $this->driver();
+ }, 50);
+
+ $this->browser = new Browser($driver);
+
+ $screenshots_dir = __DIR__ . '/Browser/screenshots/';
+ Browser::$storeScreenshotsAt = $screenshots_dir;
+ if (!file_exists($screenshots_dir)) {
+ mkdir($screenshots_dir, 0777, true);
+ }
+
+ return $this->browser;
+ }
+
+ /**
+ * (Automatically) stop the browser and driver process
+ *
+ * @afterClass
+ */
+ protected function stopBrowser(): void
+ {
+ if ($this->browser) {
+ $this->browser->quit();
+ static::stopChromeDriver();
+ $this->browser = null;
+ }
+ }
+
+ /**
+ * Initialize and start Chrome driver
+ */
+ protected function driver()
+ {
+ static::startChromeDriver();
+
+ $options = (new ChromeOptions())->addArguments([
+ '--lang=en_US',
+ '--disable-gpu',
+ '--headless',
+ ]);
+
+ return RemoteWebDriver::create(
+ 'http://localhost:9515',
+ DesiredCapabilities::chrome()->setCapability(
+ ChromeOptions::CAPABILITY,
+ $options
+ )
+ );
+ }
+
+ /**
+ * Register an "after class" tear down callback.
+ *
+ * @param \Closure $callback
+ */
+ public static function afterClass(\Closure $callback): void
+ {
+ // This method is required by SupportsChrome trait
+ }
+}
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieTest.php
@@ -8,11 +8,13 @@
use App\Wallet;
use App\WalletSetting;
use GuzzleHttp\Psr7\Response;
+use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class PaymentsMollieTest extends TestCase
{
use \Tests\MollieMocksTrait;
+ use \Tests\BrowserAddonTrait;
/**
* {@inheritDoc}
@@ -26,7 +28,6 @@
$john = $this->getTestUser('john@kolab.org');
$wallet = $john->wallets()->first();
- $john->setSetting('mollie_id', null);
Payment::where('wallet_id', $wallet->id)->delete();
Wallet::where('id', $wallet->id)->update(['balance' => 0]);
WalletSetting::where('wallet_id', $wallet->id)->delete();
@@ -39,7 +40,6 @@
{
$john = $this->getTestUser('john@kolab.org');
$wallet = $john->wallets()->first();
- $john->setSetting('mollie_id', null);
Payment::where('wallet_id', $wallet->id)->delete();
Wallet::where('id', $wallet->id)->update(['balance' => 0]);
WalletSetting::where('wallet_id', $wallet->id)->delete();
@@ -122,6 +122,7 @@
$this->assertEquals('Credit Card', $json['method']);
$this->assertSame(true, $json['isPending']);
$this->assertSame(false, $json['isValid']);
+ $this->assertSame(false, $json['isDisabled']);
$mandate_id = $json['id'];
@@ -143,6 +144,9 @@
$responseStack = $this->mockMollie();
$responseStack->append(new Response(200, [], json_encode($mollie_response)));
+ $wallet = $user->wallets()->first();
+ $wallet->setSetting('mandate_disabled', 1);
+
$response = $this->actingAs($user)->get("api/v4/payments/mandate");
$response->assertStatus(200);
@@ -153,6 +157,7 @@
$this->assertEquals('Visa (**** **** **** 4242)', $json['method']);
$this->assertSame(false, $json['isPending']);
$this->assertSame(true, $json['isValid']);
+ $this->assertSame(true, $json['isDisabled']);
// Test updating mandate details (invalid input)
$post = [];
@@ -218,6 +223,8 @@
*/
public function testStoreAndWebhook(): void
{
+ Bus::fake();
+
// Unauth access not allowed
$response = $this->post("api/v4/payments", []);
$response->assertStatus(401);
@@ -279,6 +286,13 @@
$this->assertSame('paid', $payment->fresh()->status);
$this->assertEquals(1234, $wallet->fresh()->balance);
+ // Assert that email notification job has been dispatched
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) {
+ $job_payment = $this->getObjectProperty($job, 'payment');
+ return $job_payment->id === $payment->id;
+ });
+
// Verify "paid -> open -> paid" scenario, assert that balance didn't change
$mollie_response['status'] = 'open';
unset($mollie_response['paidAt']);
@@ -299,6 +313,39 @@
$this->assertSame('paid', $payment->fresh()->status);
$this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Test for notification on payment failure
+ Bus::fake();
+
+ $payment->refresh();
+ $payment->status = PaymentProvider::STATUS_OPEN;
+ $payment->save();
+
+ $mollie_response = [
+ "resource" => "payment",
+ "id" => $payment->id,
+ "status" => "failed",
+ "mode" => "test",
+ ];
+
+ // We'll trigger the webhook with payment id and use mocking for
+ // a request to the Mollie payments API. We cannot force Mollie
+ // to make the payment status change.
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $response = $this->post("api/webhooks/payment/mollie", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('failed', $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Assert that email notification job has been dispatched
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) {
+ $job_payment = $this->getObjectProperty($job, 'payment');
+ return $job_payment->id === $payment->id;
+ });
}
/**
@@ -306,20 +353,83 @@
*
* @group mollie
*/
- public function testDirectCharge(): void
+ public function testTopUp(): void
{
+ Bus::fake();
+
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
- // Expect false result, as there's no mandate
- $result = PaymentsController::directCharge($wallet, 1234);
+ // Create a valid mandate first
+ $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 10]);
+
+ // Expect a recurring payment as we have a valid mandate at this point
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertTrue($result);
+
+ // Check that the payments table contains a new record with proper amount
+ // There should be two records, one for the first payment and another for
+ // the recurring payment
+ $this->assertCount(1, $wallet->payments()->get());
+ $this->assertSame(2010, $wallet->payments()->first()->amount);
+
+ // Expect no payment if the mandate is disabled
+ $wallet->setSetting('mandate_disabled', 1);
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ // Expect no payment if balance is ok
+ $wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = 1000;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ // Expect no payment if the top-up amount is not enough
+ $wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = -2050;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) {
+ $job_wallet = $this->getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+
+ // Expect no payment if there's no mandate
+ $wallet->setSetting('mollie_mandate_id', null);
+ $wallet->balance = 0;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
+ }
+
+ /**
+ * Create Mollie's auto-payment mandate using our API and Chrome browser
+ */
+ protected function createMandate(Wallet $wallet, array $params)
+ {
+ // Use the API to create a first payment with a mandate
+ $response = $this->actingAs($wallet->owner)->post("api/v4/payments/mandate", $params);
+ $response->assertStatus(200);
+ $json = $response->json();
- // Problem with this is we need to have a valid mandate
- // And there's no easy way to confirm a created mandate.
+ // There's no easy way to confirm a created mandate.
// The only way seems to be to fire up Chrome on checkout page
- // and do some actions with use of Dusk browser.
+ // and do actions with use of Dusk browser.
+ $this->startBrowser()
+ ->visit($json['redirectUrl'])
+ ->click('input[value="paid"]')
+ ->click('button.form__button');
- $this->markTestIncomplete();
+ $this->stopBrowser();
}
}
diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php
--- a/src/tests/Feature/Controller/PaymentsStripeTest.php
+++ b/src/tests/Feature/Controller/PaymentsStripeTest.php
@@ -8,6 +8,7 @@
use App\Wallet;
use App\WalletSetting;
use GuzzleHttp\Psr7\Response;
+use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class PaymentsStripeTest extends TestCase
@@ -26,7 +27,6 @@
$john = $this->getTestUser('john@kolab.org');
$wallet = $john->wallets()->first();
- $john->setSetting('mollie_id', null);
Payment::where('wallet_id', $wallet->id)->delete();
Wallet::where('id', $wallet->id)->update(['balance' => 0]);
WalletSetting::where('wallet_id', $wallet->id)->delete();
@@ -39,7 +39,6 @@
{
$john = $this->getTestUser('john@kolab.org');
$wallet = $john->wallets()->first();
- $john->setSetting('mollie_id', null);
Payment::where('wallet_id', $wallet->id)->delete();
Wallet::where('id', $wallet->id)->update(['balance' => 0]);
WalletSetting::where('wallet_id', $wallet->id)->delete();
@@ -119,6 +118,7 @@
$this->assertEquals(20.10, $json['amount']);
$this->assertEquals(0, $json['balance']);
+ $this->assertSame(false, $json['isDisabled']);
// We would have to invoke a browser to accept the "first payment" to make
// the mandate validated/completed. Instead, we'll mock the mandate object.
@@ -128,7 +128,8 @@
"created": 123456789,
"payment_method": "pm_YYY",
"status": "succeeded",
- "usage": "off_session"
+ "usage": "off_session",
+ "customer": null
}';
$paymentMethod = '{
@@ -151,6 +152,7 @@
// I.e. we have to fake the mandate id
$wallet = $user->wallets()->first();
$wallet->setSetting('stripe_mandate_id', 'AAA');
+ $wallet->setSetting('mandate_disabled', 1);
$response = $this->actingAs($user)->get("api/v4/payments/mandate");
$response->assertStatus(200);
@@ -162,6 +164,7 @@
$this->assertEquals('Visa (**** **** **** 4242)', $json['method']);
$this->assertSame(false, $json['isPending']);
$this->assertSame(true, $json['isValid']);
+ $this->assertSame(true, $json['isDisabled']);
// Test updating mandate details (invalid input)
$post = [];
@@ -211,6 +214,8 @@
*/
public function testStoreAndWebhook(): void
{
+ Bus::fake();
+
// Unauth access not allowed
$response = $this->post("api/v4/payments", []);
$response->assertStatus(401);
@@ -247,7 +252,81 @@
$this->assertSame('open', $payment->status);
$this->assertEquals(0, $wallet->balance);
- // TODO: Test the webhook
+ // Test the webhook
+
+ $post = [
+ 'id' => "evt_1GlZ814fj3SIEU8wtxMZ4Nsa",
+ 'object' => "event",
+ 'api_version' => "2020-03-02",
+ 'created' => 1590147209,
+ 'data' => [
+ 'object' => [
+ 'id' => $payment->id,
+ 'object' => "payment_intent",
+ 'amount' => 1234,
+ 'amount_capturable' => 0,
+ 'amount_received' => 1234,
+ 'capture_method' => "automatic",
+ 'client_secret' => "pi_1GlZ7w4fj3SIEU8w1RlBpN4l_secret_UYRNDTUUU7nkYHpOLZMb3uf48",
+ 'confirmation_method' => "automatic",
+ 'created' => 1590147204,
+ 'currency' => "chf",
+ 'customer' => "cus_HKDZ53OsKdlM83",
+ 'last_payment_error' => null,
+ 'livemode' => false,
+ 'metadata' => [],
+ 'receipt_email' => "payment-test@kolabnow.com",
+ 'status' => "succeeded"
+ ]
+ ],
+ 'type' => "payment_intent.succeeded"
+ ];
+
+ // Test payment succeeded event
+ $response = $this->webhookRequest($post);
+ $response->assertStatus(200);
+
+ $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Assert that email notification job has been dispatched
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) {
+ $job_payment = $this->getObjectProperty($job, 'payment');
+ return $job_payment->id === $payment->id;
+ });
+
+ // Test that balance didn't change if the same event is posted
+ $response = $this->webhookRequest($post);
+ $response->assertStatus(200);
+
+ $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Test for notification on payment failure
+ Bus::fake();
+
+ $payment->refresh();
+ $payment->status = PaymentProvider::STATUS_OPEN;
+ $payment->save();
+
+ $post['type'] = "payment_intent.payment_failed";
+ $post['data']['object']['status'] = 'failed';
+
+ $response = $this->webhookRequest($post);
+ $response->assertStatus(200);
+
+ $this->assertSame(PaymentProvider::STATUS_FAILED, $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Assert that email notification job has been dispatched
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentEmail::class, function ($job) use ($payment) {
+ $job_payment = $this->getObjectProperty($job, 'payment');
+ return $job_payment->id === $payment->id;
+ });
+
+ // TODO: Test setup_intent.succeeded and payment_intent.canceled events
}
/**
@@ -255,20 +334,125 @@
*
* @group stripe
*/
- public function testDirectCharge(): void
+ public function testTopUp(): void
{
+ Bus::fake();
+
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
- // Expect false result, as there's no mandate
- $result = PaymentsController::directCharge($wallet, 1234);
+ // Stripe API does not allow us to create a mandate easily
+ // That's why we we'll mock API responses
+ // Create a fake mandate
+ $wallet->setSettings([
+ 'mandate_amount' => 20.10,
+ 'mandate_balance' => 10,
+ 'stripe_mandate_id' => 'AAA',
+ ]);
+
+ $setupIntent = json_encode([
+ "id" => "AAA",
+ "object" => "setup_intent",
+ "created" => 123456789,
+ "payment_method" => "pm_YYY",
+ "status" => "succeeded",
+ "usage" => "off_session",
+ "customer" => null
+ ]);
+
+ $paymentMethod = json_encode([
+ "id" => "pm_YYY",
+ "object" => "payment_method",
+ "card" => [
+ "brand" => "visa",
+ "country" => "US",
+ "last4" => "4242"
+ ],
+ "created" => 123456789,
+ "type" => "card"
+ ]);
+
+ $paymentIntent = json_encode([
+ "id" => "pi_XX",
+ "object" => "payment_intent",
+ "created" => 123456789,
+ "amount" => 2010,
+ "currency" => "chf",
+ "description" => "Kolab Recurring Payment"
+ ]);
+
+ $client = $this->mockStripe();
+ $client->addResponse($setupIntent);
+ $client->addResponse($paymentMethod);
+ $client->addResponse($setupIntent);
+ $client->addResponse($paymentIntent);
+
+ // Expect a recurring payment as we have a valid mandate at this point
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertTrue($result);
+
+ // Check that the payments table contains a new record with proper amount
+ // There should be two records, one for the first payment and another for
+ // the recurring payment
+ $this->assertCount(1, $wallet->payments()->get());
+ $payment = $wallet->payments()->first();
+ $this->assertSame(2010, $payment->amount);
+ $this->assertSame("Kolab Recurring Payment", $payment->description);
+ $this->assertSame("pi_XX", $payment->id);
+
+ // Expect no payment if the mandate is disabled
+ $wallet->setSetting('mandate_disabled', 1);
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ // Expect no payment if balance is ok
+ $wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = 1000;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ // Expect no payment if the top-up amount is not enough
+ $wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = -2050;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
+ $this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
+ Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) {
+ $job_wallet = $this->getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+
+ // Expect no payment if there's no mandate
+ $wallet->setSetting('mollie_mandate_id', null);
+ $wallet->balance = 0;
+ $wallet->save();
+ $result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
+ $this->assertCount(1, $wallet->payments()->get());
+
+ Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
+
+ $this->unmockStripe();
+ }
+
+ /**
+ * Generate Stripe-Signature header for a webhook payload
+ */
+ protected function webhookRequest($post)
+ {
+ $secret = \config('services.stripe.webhook_secret');
+ $ts = time();
- // Problem with this is we need to have a valid mandate
- // And there's no easy way to confirm a created mandate.
- // The only way seems to be to fire up Chrome on checkout page
- // and do some actions with use of Dusk browser.
+ $payload = "$ts." . json_encode($post);
+ $sig = sprintf('t=%d,v1=%s', $ts, \hash_hmac('sha256', $payload, $secret));
- $this->markTestIncomplete();
+ return $this->withHeaders(['Stripe-Signature' => $sig])
+ ->json('POST', "api/webhooks/payment/stripe", $post);
}
}
diff --git a/src/tests/Feature/Jobs/PaymentEmailTest.php b/src/tests/Feature/Jobs/PaymentEmailTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/PaymentEmailTest.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Tests\Feature\Jobs;
+
+use App\Jobs\PaymentEmail;
+use App\Mail\PaymentFailure;
+use App\Mail\PaymentSuccess;
+use App\Payment;
+use App\Providers\PaymentProvider;
+use Illuminate\Support\Facades\Mail;
+use Tests\TestCase;
+
+class PaymentEmailTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('PaymentEmail@UserAccount.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('PaymentEmail@UserAccount.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @return void
+ */
+ public function testHandle()
+ {
+ $user = $this->getTestUser('PaymentEmail@UserAccount.com');
+ $user->setSetting('external_email', 'ext@email.tld');
+ $wallet = $user->wallets()->first();
+
+ $payment = new Payment();
+ $payment->id = 'test-payment';
+ $payment->wallet_id = $wallet->id;
+ $payment->amount = 100;
+ $payment->status = PaymentProvider::STATUS_PAID;
+ $payment->description = 'test';
+ $payment->save();
+
+ Mail::fake();
+
+ // Assert that no jobs were pushed...
+ Mail::assertNothingSent();
+
+ $job = new PaymentEmail($payment);
+ $job->handle();
+
+ // Assert the email sending job was pushed once
+ Mail::assertSent(PaymentSuccess::class, 1);
+
+ // Assert the mail was sent to the user's email
+ Mail::assertSent(PaymentSuccess::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && $mail->hasCc('ext@email.tld');
+ });
+
+ $payment->status = PaymentProvider::STATUS_FAILED;
+ $payment->save();
+
+ $job = new PaymentEmail($payment);
+ $job->handle();
+
+ // Assert the email sending job was pushed once
+ Mail::assertSent(PaymentFailure::class, 1);
+
+ // Assert the mail was sent to the user's email
+ Mail::assertSent(PaymentFailure::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && $mail->hasCc('ext@email.tld');
+ });
+
+ $payment->status = PaymentProvider::STATUS_EXPIRED;
+ $payment->save();
+
+ $job = new PaymentEmail($payment);
+ $job->handle();
+
+ // Assert the email sending job was pushed twice
+ Mail::assertSent(PaymentFailure::class, 2);
+
+ // None of statuses below should trigger an email
+ Mail::fake();
+
+ $states = [
+ PaymentProvider::STATUS_OPEN,
+ PaymentProvider::STATUS_CANCELED,
+ PaymentProvider::STATUS_PENDING,
+ PaymentProvider::STATUS_AUTHORIZED,
+ ];
+
+ foreach ($states as $state) {
+ $payment->status = $state;
+ $payment->save();
+
+ $job = new PaymentEmail($payment);
+ $job->handle();
+ }
+
+ // Assert that no mailables were sent...
+ Mail::assertNothingSent();
+ }
+}
diff --git a/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php b/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/PaymentMandateDisabledEmailTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Tests\Feature\Jobs;
+
+use App\Jobs\PaymentMandateDisabledEmail;
+use App\Mail\PaymentMandateDisabled;
+use Illuminate\Support\Facades\Mail;
+use Tests\TestCase;
+
+class PaymentMandateDisabledEmailTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('PaymentEmail@UserAccount.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return void
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('PaymentEmail@UserAccount.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @return void
+ */
+ public function testHandle()
+ {
+ $user = $this->getTestUser('PaymentEmail@UserAccount.com');
+ $user->setSetting('external_email', 'ext@email.tld');
+ $wallet = $user->wallets()->first();
+
+ Mail::fake();
+
+ // Assert that no jobs were pushed...
+ Mail::assertNothingSent();
+
+ $job = new PaymentMandateDisabledEmail($wallet);
+ $job->handle();
+
+ // Assert the email sending job was pushed once
+ Mail::assertSent(PaymentMandateDisabled::class, 1);
+
+ // Assert the mail was sent to the user's email
+ Mail::assertSent(PaymentMandateDisabled::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && $mail->hasCc('ext@email.tld');
+ });
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 5, 11:55 AM (1 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833378
Default Alt Text
D1249.1775390116.diff (53 KB)
Attached To
Mode
D1249: Wallet charge: top-ups and notifications
Attached
Detach File
Event Timeline