Page MenuHomePhorge

D1900.1774835859.diff
No OneTemporary

Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None

D1900.1774835859.diff

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
@@ -21,7 +21,7 @@
$user = Auth::guard()->user();
// TODO: Wallet selection
- $wallet = $user->wallets->first();
+ $wallet = $user->wallets()->first();
$mandate = self::walletMandate($wallet);
@@ -40,28 +40,10 @@
$current_user = Auth::guard()->user();
// TODO: Wallet selection
- $wallet = $current_user->wallets->first();
+ $wallet = $current_user->wallets()->first();
- $rules = [
- 'amount' => 'required|numeric',
- 'balance' => 'required|numeric|min:0',
- ];
-
- // Check required fields
- $v = Validator::make($request->all(), $rules);
-
- // TODO: allow comma as a decimal point?
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
- }
-
- $amount = (int) ($request->amount * 100);
-
- // Validate the minimum value
- if ($amount < PaymentProvider::MIN_AMOUNT) {
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
- $errors = ['amount' => \trans('validation.minamount', ['amount' => $min])];
+ // Input validation
+ if ($errors = self::mandateValidate($request, $wallet)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
@@ -70,15 +52,20 @@
'mandate_balance' => $request->balance,
]);
- $request = [
+ $mandate = [
'currency' => 'CHF',
- 'amount' => $amount,
'description' => \config('app.name') . ' Auto-Payment Setup',
];
+ // Normally the auto-payment setup operation is 0, if the balance is below the threshold
+ // we'll top-up the wallet with the configured auto-payment amount
+ if ($wallet->balance < intval($request->balance * 100)) {
+ $mandate['amount'] = intval($request->amount * 100);
+ }
+
$provider = PaymentProvider::factory($wallet);
- $result = $provider->createMandate($wallet, $request);
+ $result = $provider->createMandate($wallet, $mandate);
$result['status'] = 'success';
@@ -95,7 +82,7 @@
$user = Auth::guard()->user();
// TODO: Wallet selection
- $wallet = $user->wallets->first();
+ $wallet = $user->wallets()->first();
$provider = PaymentProvider::factory($wallet);
@@ -121,8 +108,43 @@
$current_user = Auth::guard()->user();
// TODO: Wallet selection
- $wallet = $current_user->wallets->first();
+ $wallet = $current_user->wallets()->first();
+
+ // Input validation
+ if ($errors = self::mandateValidate($request, $wallet)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ $wallet->setSettings([
+ 'mandate_amount' => $request->amount,
+ 'mandate_balance' => $request->balance,
+ // Re-enable the mandate to give it a chance to charge again
+ // after it has been disabled (e.g. because the mandate amount was too small)
+ 'mandate_disabled' => null,
+ ]);
+
+ // Trigger auto-payment if the balance is below the threshold
+ if ($wallet->balance < intval($request->balance * 100)) {
+ \App\Jobs\WalletCharge::dispatch($wallet);
+ }
+ $result = self::walletMandate($wallet);
+ $result['status'] = 'success';
+ $result['message'] = \trans('app.mandate-update-success');
+
+ return response()->json($result);
+ }
+
+ /**
+ * Validate an auto-payment mandate request.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param \App\Wallet $wallet The wallet
+ *
+ * @return array|null List of errors on error or Null on success
+ */
+ protected static function mandateValidate(Request $request, Wallet $wallet)
+ {
$rules = [
'amount' => 'required|numeric',
'balance' => 'required|numeric|min:0',
@@ -134,43 +156,27 @@
// TODO: allow comma as a decimal point?
if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ return $v->errors();
}
$amount = (int) ($request->amount * 100);
// Validate the minimum value
- if ($amount < PaymentProvider::MIN_AMOUNT) {
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
- $errors = ['amount' => \trans('validation.minamount', ['amount' => $min])];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ // It has to be at least minimum payment amount and must cover current debt
+ if (
+ $wallet->balance < 0
+ && $wallet->balance * -1 > PaymentProvider::MIN_AMOUNT
+ && $wallet->balance + $amount < 0
+ ) {
+ return ['amount' => \trans('validation.minamountdebt')];
}
- // If the mandate is disabled the update will trigger
- // an auto-payment and the amount must cover the debt
- if ($wallet->getSetting('mandate_disabled')) {
- if ($wallet->balance < 0 && $wallet->balance + $amount < 0) {
- $errors = ['amount' => \trans('validation.minamountdebt')];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- $wallet->setSetting('mandate_disabled', null);
-
- if ($wallet->balance < intval($request->balance * 100)) {
- \App\Jobs\WalletCharge::dispatch($wallet);
- }
+ if ($amount < PaymentProvider::MIN_AMOUNT) {
+ $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ return ['amount' => \trans('validation.minamount', ['amount' => $min])];
}
- $wallet->setSettings([
- 'mandate_amount' => $request->amount,
- 'mandate_balance' => $request->balance,
- ]);
-
- $result = self::walletMandate($wallet);
- $result['status'] = 'success';
- $result['message'] = \trans('app.mandate-update-success');
-
- return response()->json($result);
+ return null;
}
/**
@@ -185,7 +191,7 @@
$current_user = Auth::guard()->user();
// TODO: Wallet selection
- $wallet = $current_user->wallets->first();
+ $wallet = $current_user->wallets()->first();
$rules = [
'amount' => 'required|numeric',
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
@@ -37,7 +37,7 @@
*
* @param \App\Wallet $wallet The wallet
* @param array $payment Payment data:
- * - amount: Value in cents
+ * - amount: Value in cents (optional)
* - currency: The operation currency
* - description: Operation desc.
*
@@ -50,10 +50,14 @@
// Register the user in Mollie, if not yet done
$customer_id = self::mollieCustomerId($wallet, true);
+ if (!isset($payment['amount'])) {
+ $payment['amount'] = 0;
+ }
+
$request = [
'amount' => [
'currency' => $payment['currency'],
- 'value' => '0.00',
+ 'value' => sprintf('%.2f', $payment['amount'] / 100),
],
'customerId' => $customer_id,
'sequenceType' => 'first',
@@ -71,6 +75,13 @@
$wallet->setSetting('mollie_mandate_id', $response->mandateId);
}
+ // Store the payment reference in database
+ $payment['status'] = $response->status;
+ $payment['id'] = $response->id;
+ $payment['type'] = self::TYPE_MANDATE;
+
+ $this->storePayment($payment, $wallet->id);
+
return [
'id' => $response->id,
'redirectUrl' => $response->getCheckoutUrl(),
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
@@ -56,7 +56,7 @@
*
* @param \App\Wallet $wallet The wallet
* @param array $payment Payment data:
- * - amount: Value in cents
+ * - amount: Value in cents (not used)
* - currency: The operation currency
* - description: Operation desc.
*
@@ -77,12 +77,14 @@
'mode' => 'setup',
];
+ // Note: Stripe does not allow to set amount for 'setup' operation
+ // We'll dispatch WalletCharge job when we receive a webhook request
+
$session = StripeAPI\Checkout\Session::create($request);
- $payment = [
- 'id' => $session->setup_intent,
- 'type' => self::TYPE_MANDATE,
- ];
+ $payment['amount'] = 0;
+ $payment['id'] = $session->setup_intent;
+ $payment['type'] = self::TYPE_MANDATE;
$this->storePayment($payment, $wallet->id);
@@ -355,6 +357,12 @@
if ($status == self::STATUS_PAID) {
$payment->wallet->setSetting('stripe_mandate_id', $intent->id);
+ $threshold = intval($payment->wallet->getSetting('mandate_balance') * 100);
+
+ // Top-up the wallet if balance is below the threshold
+ if ($payment->wallet->balance < $threshold && $payment->status != self::STATUS_PAID) {
+ \App\Jobs\WalletCharge::dispatch($payment->wallet);
+ }
}
$payment->status = $status;
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
@@ -80,6 +80,7 @@
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
// Test creating a mandate (invalid input)
$post = [];
@@ -104,7 +105,7 @@
$this->assertCount(1, $json['errors']);
$this->assertSame('The balance must be a number.', $json['errors']['balance'][0]);
- // Test creating a mandate (invalid input)
+ // Test creating a mandate (amount smaller than the minimum value)
$post = ['amount' => -100, 'balance' => 0];
$response = $this->actingAs($user)->post("api/v4/payments/mandate", $post);
$response->assertStatus(422);
@@ -116,6 +117,18 @@
$min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
+ // Test creating a mandate (negative balance, amount too small)
+ Wallet::where('id', $wallet->id)->update(['balance' => -2000]);
+ $post = ['amount' => PaymentProvider::MIN_AMOUNT / 100, 'balance' => 0];
+ $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The specified amount does not cover the balance on the account.", $json['errors']['amount']);
+
// Test creating a mandate (valid input)
$post = ['amount' => 20.10, 'balance' => 0];
$response = $this->actingAs($user)->post("api/v4/payments/mandate", $post);
@@ -126,6 +139,13 @@
$this->assertSame('success', $json['status']);
$this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']);
+ // Assert the proper payment amount has been used
+ $payment = Payment::where('id', $json['id'])->first();
+ $this->assertSame(2010, $payment->amount);
+ $this->assertSame($wallet->id, $payment->wallet_id);
+ $this->assertSame("Kolab Now Auto-Payment Setup", $payment->description);
+ $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
+
// Test fetching the mandate information
$response = $this->actingAs($user)->get("api/v4/payments/mandate");
$response->assertStatus(200);
@@ -176,6 +196,8 @@
Bus::fake();
$wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = 1000;
+ $wallet->save();
// Test updating mandate details (invalid input)
$post = [];
@@ -202,7 +224,7 @@
// Test updating a mandate (valid input)
$responseStack->append(new Response(200, [], json_encode($mollie_response)));
- $post = ['amount' => 30.10, 'balance' => 1];
+ $post = ['amount' => 30.10, 'balance' => 10];
$response = $this->actingAs($user)->put("api/v4/payments/mandate", $post);
$response->assertStatus(200);
@@ -216,7 +238,9 @@
$wallet->refresh();
$this->assertEquals(30.10, $wallet->getSetting('mandate_amount'));
- $this->assertEquals(1, $wallet->getSetting('mandate_balance'));
+ $this->assertEquals(10, $wallet->getSetting('mandate_balance'));
+
+ Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 0);
// Test updating a disabled mandate (invalid input)
$wallet->setSetting('mandate_disabled', 1);
@@ -449,19 +473,24 @@
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
- // Create a valid mandate first
- $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 10]);
+ // Create a valid mandate first (balance=0, so there's no extra payment yet)
+ $this->createMandate($wallet, ['amount' => 20.10, 'balance' => 0]);
+
+ $wallet->setSetting('mandate_balance', 10);
// Expect a recurring payment as we have a valid mandate at this point
+ // and the balance is below the threshold
$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);
+ // Check that the payments table contains a new record with proper amount.
+ // There should be two records, one for the mandate payment and another for
+ // the top-up payment
+ $payments = $wallet->payments()->orderBy('amount')->get();
+ $this->assertCount(2, $payments);
+ $this->assertSame(0, $payments[0]->amount);
+ $this->assertSame(2010, $payments[1]->amount);
+ $payment = $payments[1];
// In mollie we don't have to wait for a webhook, the response to
// PaymentIntent already sets the status to 'paid', so we can test
@@ -488,7 +517,7 @@
$wallet->setSetting('mandate_disabled', 1);
$result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
- $this->assertCount(1, $wallet->payments()->get());
+ $this->assertCount(2, $wallet->payments()->get());
// Expect no payment if balance is ok
$wallet->setSetting('mandate_disabled', null);
@@ -496,7 +525,7 @@
$wallet->save();
$result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
- $this->assertCount(1, $wallet->payments()->get());
+ $this->assertCount(2, $wallet->payments()->get());
// Expect no payment if the top-up amount is not enough
$wallet->setSetting('mandate_disabled', null);
@@ -504,7 +533,7 @@
$wallet->save();
$result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
- $this->assertCount(1, $wallet->payments()->get());
+ $this->assertCount(2, $wallet->payments()->get());
Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
Bus::assertDispatched(\App\Jobs\PaymentMandateDisabledEmail::class, function ($job) use ($wallet) {
@@ -518,7 +547,7 @@
$wallet->save();
$result = PaymentsController::topUpWallet($wallet);
$this->assertFalse($result);
- $this->assertCount(1, $wallet->payments()->get());
+ $this->assertCount(2, $wallet->payments()->get());
Bus::assertDispatchedTimes(\App\Jobs\PaymentMandateDisabledEmail::class, 1);
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
@@ -72,6 +72,7 @@
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
// Test creating a mandate (invalid input)
$post = [];
@@ -108,6 +109,18 @@
$min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
+ // Test creating a mandate (negative balance, amount too small)
+ Wallet::where('id', $wallet->id)->update(['balance' => -2000]);
+ $post = ['amount' => PaymentProvider::MIN_AMOUNT / 100, 'balance' => 0];
+ $response = $this->actingAs($user)->post("api/v4/payments/mandate", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(1, $json['errors']);
+ $this->assertSame("The specified amount does not cover the balance on the account.", $json['errors']['amount']);
+
// Test creating a mandate (valid input)
$post = ['amount' => 20.10, 'balance' => 0];
$response = $this->actingAs($user)->post("api/v4/payments/mandate", $post);
@@ -118,6 +131,13 @@
$this->assertSame('success', $json['status']);
$this->assertRegExp('|^cs_test_|', $json['id']);
+ // Assert the proper payment amount has been used
+ // Stripe in 'setup' mode does not allow to set the amount
+ $payment = Payment::where('wallet_id', $wallet->id)->first();
+ $this->assertSame(0, $payment->amount);
+ $this->assertSame("Kolab Now Auto-Payment Setup", $payment->description);
+ $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
+
// Test fetching the mandate information
$response = $this->actingAs($user)->get("api/v4/payments/mandate");
$response->assertStatus(200);
@@ -176,6 +196,8 @@
// Test updating mandate details (invalid input)
$wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = 1000;
+ $wallet->save();
$user->refresh();
$post = [];
$response = $this->actingAs($user)->put("api/v4/payments/mandate", $post);
@@ -202,7 +224,7 @@
$client->addResponse($setupIntent);
$client->addResponse($paymentMethod);
- $post = ['amount' => 30.10, 'balance' => 1];
+ $post = ['amount' => 30.10, 'balance' => 10];
$response = $this->actingAs($user)->put("api/v4/payments/mandate", $post);
$response->assertStatus(200);
@@ -211,7 +233,7 @@
$this->assertSame('success', $json['status']);
$this->assertSame('The auto-payment has been updated.', $json['message']);
$this->assertEquals(30.10, $wallet->getSetting('mandate_amount'));
- $this->assertEquals(1, $wallet->getSetting('mandate_balance'));
+ $this->assertEquals(10, $wallet->getSetting('mandate_balance'));
$this->assertSame('AAA', $json['id']);
$this->assertFalse($json['isDisabled']);
@@ -406,6 +428,7 @@
{
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
+ Wallet::where('id', $wallet->id)->update(['balance' => -1000]);
// Test creating a mandate (valid input)
$post = ['amount' => 20.10, 'balance' => 0];
@@ -438,6 +461,8 @@
'type' => "setup_intent.succeeded"
];
+ Bus::fake();
+
// Test payment succeeded event
$response = $this->webhookRequest($post);
$response->assertStatus(200);
@@ -447,6 +472,13 @@
$this->assertSame(PaymentProvider::STATUS_PAID, $payment->status);
$this->assertSame($payment->id, $wallet->fresh()->getSetting('stripe_mandate_id'));
+ // Expect a WalletCharge job if the balance is negative
+ Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 1);
+ Bus::assertDispatched(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) {
+ $job_wallet = TestCase::getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+
// TODO: test other setup_intent.* events
}
diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php
--- a/src/tests/Feature/Controller/WalletsTest.php
+++ b/src/tests/Feature/Controller/WalletsTest.php
@@ -190,6 +190,8 @@
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$wallet = $john->wallets()->first();
+ $wallet->balance = -100;
+ $wallet->save();
// Accessing a wallet of someone else
$response = $this->actingAs($jack)->get("api/v4/wallets/{$wallet->id}");
@@ -209,7 +211,6 @@
$this->assertSame('CHF', $json['currency']);
$this->assertSame($wallet->balance, $json['balance']);
$this->assertTrue(empty($json['description']));
- // TODO: This assertion does not work after a longer while from seeding
$this->assertTrue(!empty($json['notice']));
}

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 30, 1:57 AM (6 d, 16 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18786564
Default Alt Text
D1900.1774835859.diff (22 KB)

Event Timeline