Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117744531
D1030.1775170170.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None
D1030.1775170170.diff
View Options
diff --git a/src/.env.example b/src/.env.example
--- a/src/.env.example
+++ b/src/.env.example
@@ -54,6 +54,8 @@
SWOOLE_HTTP_HOST=127.0.0.1
SWOOLE_HTTP_PORT=8000
+MOLLIE_KEY=
+
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
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
@@ -52,6 +52,11 @@
);
$wallet->chargeEntitlements();
+
+ if ($wallet->balance < 0) {
+ // Disabled for now
+ // \App\Jobs\WalletPayment::dispatch($wallet);
+ }
}
}
}
diff --git a/src/app/Http/Controllers/API/PaymentsController.php b/src/app/Http/Controllers/API/PaymentsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/PaymentsController.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Payment;
+use App\Wallet;
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Validator;
+
+class PaymentsController extends Controller
+{
+ /**
+ * Create a new API\PaymentsController instance.
+ *
+ * Ensures that the correct authentication middleware is applied except for /webhook
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ $this->middleware('auth:api', ['except' => ['webhook']]);
+ }
+
+ /**
+ * Create a new payment.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function store(Request $request)
+ {
+ $current_user = Auth::guard()->user();
+
+ // TODO: Wallet selection
+ $wallet = $current_user->wallets()->first();
+
+ // Check required fields
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'amount' => 'required|int|min:1',
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ // Register the user in Mollie, if not yet done
+ // FIXME: Maybe Mollie ID should be bound to a wallet, but then
+ // The same customer could technicly have multiple
+ // Mollie IDs, then we'd need to use some "virtual" email
+ // address (e.g. <wallet-id>@<user-domain>) instead of the user email address
+ $customer_id = $current_user->getSetting('mollie_id');
+ $seq_type = 'oneoff';
+
+ if (empty($customer_id)) {
+ $customer = mollie()->customers()->create([
+ 'name' => $current_user->name,
+ 'email' => $current_user->email,
+ ]);
+
+ $seq_type = 'first';
+ $customer_id = $customer->id;
+ $current_user->setSetting('mollie_id', $customer_id);
+ }
+
+ $payment_request = [
+ 'amount' => [
+ 'currency' => 'CHF',
+ // a number with two decimals is required
+ 'value' => sprintf('%.2f', $request->amount / 100),
+ ],
+ 'customerId' => $customer_id,
+ 'sequenceType' => $seq_type, // 'first' / 'oneoff' / 'recurring'
+ 'description' => 'Kolab Now Payment', // required
+ 'redirectUrl' => self::serviceUrl('/wallet'), // required for non-recurring payments
+ 'webhookUrl' => self::serviceUrl('/api/v4/payments/webhook'),
+ 'locale' => 'en_US',
+ ];
+
+ // Create the payment in Mollie
+ $payment = mollie()->payments()->create($payment_request);
+
+ // Store the payment reference in database
+ self::storePayment($payment, $wallet->id, $request->amount);
+
+ return response()->json([
+ 'status' => 'success',
+ 'redirectUrl' => $payment->getCheckoutUrl(),
+ ]);
+ }
+
+ /**
+ * Update payment status (and balance).
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\Response The response
+ */
+ public function webhook(Request $request)
+ {
+ $db_payment = Payment::find($request->id);
+
+ // Mollie recommends to return "200 OK" even if the payment does not exist
+ if (empty($db_payment)) {
+ return response('Success', 200);
+ }
+
+ // Get the payment details from Mollie
+ $payment = mollie()->payments()->get($request->id);
+
+ if (empty($payment)) {
+ return response('Success', 200);
+ }
+
+ if ($payment->isPaid()) {
+ if (!$payment->hasRefunds() && !$payment->hasChargebacks()) {
+ // The payment is paid and isn't refunded or charged back.
+ // Update the balance, if it wasn't already
+ if ($db_payment->status != 'paid') {
+ $db_payment->wallet->credit($db_payment->amount);
+ }
+ } elseif ($payment->hasRefunds()) {
+ // The payment has been (partially) refunded.
+ // The status of the payment is still "paid"
+ // TODO: Update balance
+ } elseif ($payment->hasChargebacks()) {
+ // The payment has been (partially) charged back.
+ // The status of the payment is still "paid"
+ // TODO: Update balance
+ }
+ }
+
+ // 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 ($db_payment->status != 'paid') {
+ $db_payment->status = $payment->status;
+ $db_payment->save();
+ }
+
+ return response('Success', 200);
+ }
+
+ /**
+ * Charge 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
+ */
+ public static function directCharge(Wallet $wallet, $amount): bool
+ {
+ $customer_id = $wallet->owner->getSetting('mollie_id');
+
+ if (empty($customer_id)) {
+ return false;
+ }
+
+ // Check if there's at least one valid mandate
+ $mandates = mollie()->mandates()->listFor($customer_id)->filter(function ($mandate) {
+ return $mandate->isValid();
+ });
+
+ if (empty($mandates)) {
+ return false;
+ }
+
+ $payment_request = [
+ 'amount' => [
+ 'currency' => 'CHF',
+ // a number with two decimals is required
+ 'value' => sprintf('%.2f', $amount / 100),
+ ],
+ 'customerId' => $customer_id,
+ 'sequenceType' => 'recurring',
+ 'description' => 'Kolab Now Recurring Payment',
+ 'webhookUrl' => self::serviceUrl('/api/v4/payments/webhook'),
+ ];
+
+ // Create the payment in Mollie
+ $payment = mollie()->payments()->create($payment_request);
+
+ // Store the payment reference in database
+ self::storePayment($payment, $wallet->id, $amount);
+
+ return true;
+ }
+
+ /**
+ * Create self URL
+ *
+ * @param string $route Route/Path
+ *
+ * @return string Full URL
+ */
+ protected static function serviceUrl(string $route): string
+ {
+ $url = \url($route);
+
+ // When testing the host might be e.g. 127.0.0.1:8000.
+ // This will not be accepted by Mollie. Let's use our fqdn instead.
+ // This does not have to be working URL, we do not require Mollie
+ // to come back (yet).
+ if (preg_match('|^https?://[0-9][^/]+|', $url, $matches)) {
+ $url = str_replace($matches[0], 'https://' . \config('app.domain'), $url);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Create a payment record in DB
+ *
+ * @param object $payment Mollie payment
+ * @param string $wallet_id Wallet ID
+ * @param int $amount Amount of money in cents
+ */
+ protected static function storePayment($payment, $wallet_id, $amount): void
+ {
+ $db_payment = new Payment();
+ $db_payment->id = $payment->id;
+ $db_payment->description = $payment->description;
+ $db_payment->status = $payment->status;
+ $db_payment->amount = $amount;
+ $db_payment->wallet_id = $wallet_id;
+ $db_payment->save();
+ }
+}
diff --git a/src/app/Jobs/WalletPayment.php b/src/app/Jobs/WalletPayment.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/WalletPayment.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Wallet;
+use App\Http\Controllers\API\PaymentsController;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+
+class WalletPayment implements ShouldQueue
+{
+ use Dispatchable;
+ use InteractsWithQueue;
+ use Queueable;
+ use SerializesModels;
+
+ protected $wallet;
+
+ public $tries = 5;
+
+ /** @var bool Delete the job if its models no longer exist. */
+ public $deleteWhenMissingModels = true;
+
+
+ /**
+ * Create a new job instance.
+ *
+ * @param \App\Wallet $wallet The wallet to charge.
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet)
+ {
+ $this->wallet = $wallet;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ if (!$this->wallet->balance < 0) {
+ PaymentsController::directCharge($this->wallet, $this->wallet->balance * -1);
+ }
+ }
+}
diff --git a/src/app/Observers/WalletObserver.php b/src/app/Observers/WalletObserver.php
--- a/src/app/Observers/WalletObserver.php
+++ b/src/app/Observers/WalletObserver.php
@@ -60,5 +60,11 @@
if ($wallet->entitlements()->count() > 0) {
return false;
}
+/*
+ // can't remove a wallet that has payments attached.
+ if ($wallet->payments()->count() > 0) {
+ return false;
+ }
+*/
}
}
diff --git a/src/app/Payment.php b/src/app/Payment.php
new file mode 100644
--- /dev/null
+++ b/src/app/Payment.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * A payment operation on a wallet.
+ *
+ * @property int $amount Amount of money in cents
+ * @property string $description Payment description
+ * @property string $id Mollie's Payment ID
+ * @property int $wallet_id The ID of the wallet
+ */
+class Payment extends Model
+{
+ public $incrementing = false;
+ protected $keyType = 'string';
+
+ protected $casts = [
+ 'amount' => 'integer'
+ ];
+
+ /**
+ * The wallet to which this payment belongs.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function wallet()
+ {
+ return $this->belongsTo(
+ '\App\Wallet',
+ 'wallet_id', /* local */
+ 'id' /* remote */
+ );
+ }
+}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -93,7 +93,6 @@
return $charges;
}
-
/**
* Calculate the expected charges to this wallet.
*
@@ -121,11 +120,11 @@
/**
* Add an amount of pecunia to this wallet's balance.
*
- * @param float $amount The amount of pecunia to add.
+ * @param int $amount The amount of pecunia to add (in cents).
*
- * @return Wallet
+ * @return Wallet Self
*/
- public function credit(float $amount)
+ public function credit(int $amount): Wallet
{
$this->balance += $amount;
@@ -137,11 +136,11 @@
/**
* Deduct an amount of pecunia from this wallet's balance.
*
- * @param float $amount The amount of pecunia to deduct.
+ * @param int $amount The amount of pecunia to deduct (in cents).
*
- * @return Wallet
+ * @return Wallet Self
*/
- public function debit(float $amount)
+ public function debit(int $amount): Wallet
{
$this->balance -= $amount;
@@ -184,4 +183,14 @@
{
return $this->belongsTo('App\User', 'user_id', 'id');
}
+
+ /**
+ * Payments on this wallet.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function payments()
+ {
+ return $this->hasMany('App\Payment');
+ }
}
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -22,6 +22,7 @@
"kolab/net_ldap3": "dev-master",
"laravel/framework": "6.*",
"laravel/tinker": "^1.0",
+ "mollie/laravel-mollie": "^2.9",
"morrislaptop/laravel-queue-clear": "^1.2",
"silviolleite/laravelpwa": "^1.0",
"spatie/laravel-translatable": "^4.2",
diff --git a/src/database/migrations/2020_03_16_100000_create_payments.php b/src/database/migrations/2020_03_16_100000_create_payments.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_03_16_100000_create_payments.php
@@ -0,0 +1,40 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class CreatePayments extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'payments',
+ function (Blueprint $table) {
+ $table->string('id', 16)->primary();
+ $table->string('wallet_id', 36);
+ $table->string('status', 16);
+ $table->integer('amount');
+ $table->text('description');
+ $table->timestamps();
+
+ $table->foreign('wallet_id')->references('id')->on('wallets')->onDelete('cascade');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('payments');
+ }
+}
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -160,8 +160,8 @@
.badge {
position: absolute;
- top: .5rem;
- right: .5rem;
+ top: 0.5rem;
+ right: 0.5rem;
}
}
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -7,6 +7,7 @@
<p>Current account balance is
<span :class="balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(balance) }}</strong></span>
</p>
+ <button type="button" class="btn btn-primary" @click="payment()">Add 10 bucks to my wallet</button>
</div>
</div>
</div>
@@ -28,6 +29,14 @@
})
},
methods: {
+ payment() {
+ axios.post('/api/v4/payments', {amount: 1000})
+ .then(response => {
+ if (response.data.redirectUrl) {
+ location.href = response.data.redirectUrl
+ }
+ })
+ }
}
}
</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -49,5 +49,8 @@
Route::apiResource('skus', API\SkusController::class);
Route::apiResource('users', API\UsersController::class);
Route::apiResource('wallets', API\WalletsController::class);
+
+ Route::post('payments', 'API\PaymentsController@store');
+ Route::post('payments/webhook', 'API\PaymentsController@webhook');
}
);
diff --git a/src/tests/Browser/Pages/Wallet.php b/src/tests/Browser/Pages/PaymentMollie.php
copy from src/tests/Browser/Pages/Wallet.php
copy to src/tests/Browser/Pages/PaymentMollie.php
--- a/src/tests/Browser/Pages/Wallet.php
+++ b/src/tests/Browser/Pages/PaymentMollie.php
@@ -4,7 +4,7 @@
use Laravel\Dusk\Page;
-class Wallet extends Page
+class PaymentMollie extends Page
{
/**
* Get the URL for the page.
@@ -13,7 +13,7 @@
*/
public function url(): string
{
- return '/wallet';
+ return '';
}
/**
@@ -25,9 +25,7 @@
*/
public function assert($browser)
{
- $browser->assertPathIs($this->url())
- ->waitUntilMissing('@app .app-loader')
- ->assertSeeIn('#wallet .card-title', 'Account balance');
+ $browser->waitFor('#container');
}
/**
@@ -38,7 +36,11 @@
public function elements(): array
{
return [
- '@app' => '#app',
+ '@form' => '#container',
+ '@title' => '#container .header__info',
+ '@amount' => '#container .header__amount',
+ '@methods' => '#payment-method-list',
+ '@status-table' => 'table.table--select-status',
];
}
}
diff --git a/src/tests/Browser/Pages/Wallet.php b/src/tests/Browser/Pages/Wallet.php
--- a/src/tests/Browser/Pages/Wallet.php
+++ b/src/tests/Browser/Pages/Wallet.php
@@ -39,6 +39,7 @@
{
return [
'@app' => '#app',
+ '@main' => '#wallet'
];
}
}
diff --git a/src/tests/Browser/PaymentTest.php b/src/tests/Browser/PaymentTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/PaymentTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Tests\Browser;
+
+use App\Wallet;
+use Tests\Browser;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\PaymentMollie;
+use Tests\Browser\Pages\Wallet as WalletPage;
+use Tests\TestCaseDusk;
+
+class PaymentTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('payment-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('payment-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test a payment process
+ */
+ public function testPayment(): void
+ {
+ $user = $this->getTestUser('payment-test@kolabnow.com', [
+ 'password' => 'simple123',
+ ]);
+
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('payment-test@kolabnow.com', 'simple123', true)
+ ->on(new Dashboard())
+ ->click('@links .link-wallet')
+ ->on(new WalletPage())
+ ->click('@main button')
+ ->on(new PaymentMollie())
+ ->screenshot('sf')
+ ->assertSeeIn('@title', 'Kolab Now Payment')
+ ->assertSeeIn('@amount', 'CHF 10.00');
+
+ // Looks like the Mollie testing mode is limited.
+ // We'll select credit card method and mark the payment as paid
+ // We can't do much more, we have to trust Mollie their page works ;)
+
+ // For some reason I don't get the method selection form, it
+ // immediately jumps to the next step. Let's detect that
+ if ($browser->element('@methods')) {
+ $browser->click('@methods button.grid-button-creditcard')
+ ->waitFor('button.form__button');
+ }
+
+ $browser->click('@status-table input[value="paid"]')
+ ->click('button.form__button');
+
+ // Now it should redirect back to wallet page and in background
+ // use the webhook to update payment status (and balance).
+ // However, in testing environment we may use fake URLs,
+ // so we don't expect the redirect nor webhook working.
+
+ // TODO: There's also a great chance that the webhook will not
+ // reach us before the redirect, which means we should have
+ // some way to tell the user "waiting for the payment status update"
+ // and update the balance on the wallet page when that happens.
+ });
+ }
+}
diff --git a/src/tests/Feature/Controller/PaymentsTest.php b/src/tests/Feature/Controller/PaymentsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/PaymentsTest.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Payment;
+use App\Wallet;
+use GuzzleHttp\Psr7\Response;
+use Tests\TestCase;
+
+class PaymentsTest extends TestCase
+{
+ use \Tests\MollieMocksTrait;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $wallet = $john->wallets()->first();
+ Payment::where('wallet_id', $wallet->id)->delete();
+ Wallet::where('id', $wallet->id)->update(['balance' => 0]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $wallet = $john->wallets()->first();
+ Payment::where('wallet_id', $wallet->id)->delete();
+ Wallet::where('id', $wallet->id)->update(['balance' => 0]);
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test creating a payment and receiving a status via webhook)
+ *
+ * @group mollie
+ */
+ public function testStoreAndWebhook(): void
+ {
+ // Unauth access not allowed
+ $response = $this->post("api/v4/payments", []);
+ $response->assertStatus(401);
+
+ $user = $this->getTestUser('john@kolab.org');
+
+ $post = ['amount' => -1];
+ $response = $this->actingAs($user)->post("api/v4/payments", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame('The amount must be at least 1.', $json['errors']['amount'][0]);
+
+ $post = ['amount' => 1234];
+ $response = $this->actingAs($user)->post("api/v4/payments", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']);
+
+ $wallet = $user->wallets()->first();
+ $payments = Payment::where('wallet_id', $wallet->id)->get();
+
+ $this->assertCount(1, $payments);
+ $payment = $payments[0];
+ $this->assertSame(1234, $payment->amount);
+ $this->assertSame('Kolab Now Payment', $payment->description);
+ $this->assertSame('open', $payment->status);
+ $this->assertEquals(0, $wallet->balance);
+
+ // Test the webhook
+ // Note: Webhook end-point does not require authentication
+
+ $mollie_response = [
+ "resource" => "payment",
+ "id" => $payment->id,
+ "status" => "paid",
+ // Status is not enough, paidAt is used to distinguish the state
+ "paidAt" => date('c'),
+ "mode" => "test",
+/*
+ "createdAt" => "2018-03-20T13:13:37+00:00",
+ "amount" => {
+ "value" => "10.00",
+ "currency" => "EUR"
+ },
+ "description" => "Order #12345",
+ "method" => null,
+ "metadata" => {
+ "order_id" => "12345"
+ },
+ "isCancelable" => false,
+ "locale" => "nl_NL",
+ "restrictPaymentMethodsToCountry" => "NL",
+ "expiresAt" => "2018-03-20T13:28:37+00:00",
+ "details" => null,
+ "profileId" => "pfl_QkEhN94Ba",
+ "sequenceType" => "oneoff",
+ "redirectUrl" => "https://webshop.example.org/order/12345/",
+ "webhookUrl" => "https://webshop.example.org/payments/webhook/",
+*/
+ ];
+
+ // 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)));
+
+ $post = ['id' => $payment->id];
+ $response = $this->post("api/v4/payments/webhook", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('paid', $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ // Verify "paid -> open -> paid" scenario, assert that balance didn't change
+ $mollie_response['status'] = 'open';
+ unset($mollie_response['paidAt']);
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $response = $this->post("api/v4/payments/webhook", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('paid', $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+
+ $mollie_response['status'] = 'paid';
+ $mollie_response['paidAt'] = date('c');
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $response = $this->post("api/v4/payments/webhook", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('paid', $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+ }
+
+ public function testDirectCharge(): void
+ {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/MollieMocksTrait.php b/src/tests/MollieMocksTrait.php
new file mode 100644
--- /dev/null
+++ b/src/tests/MollieMocksTrait.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Tests;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Middleware;
+use Mollie\Api\MollieApiClient;
+
+trait MollieMocksTrait
+{
+ public $mollieRequestHistory = [];
+
+ /**
+ * Make Mollie's Guzzle instance use a mock handler.
+ *
+ * @see http://docs.guzzlephp.org/en/stable/testing.html
+ *
+ * @return \GuzzleHttp\Handler\MockHandler
+ */
+ public function mockMollie()
+ {
+ $handler = HandlerStack::create(
+ $mockHandler = new MockHandler()
+ );
+
+ $handler->push(
+ Middleware::history($this->mollieRequestHistory)
+ );
+
+ $guzzle = new Client(['handler' => $handler]);
+
+ $this->app->forgetInstance('mollie.api.client');
+ $this->app->forgetInstance('mollie.api');
+ $this->app->forgetInstance('mollie');
+
+ $this->app->singleton('mollie.api.client', function () use ($guzzle) {
+ return new MollieApiClient($guzzle);
+ });
+
+ return $mockHandler;
+ }
+
+ public function unmockMollie()
+ {
+ $this->app->forgetInstance('mollie.api.client');
+ $this->app->forgetInstance('mollie.api');
+ $this->app->forgetInstance('mollie');
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 10:49 PM (21 h, 49 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821337
Default Alt Text
D1030.1775170170.diff (27 KB)
Attached To
Mode
D1030: Mollie payments
Attached
Detach File
Event Timeline