Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117881336
D1030.1775346553.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
D1030.1775346553.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
@@ -52,6 +52,10 @@
);
$wallet->chargeEntitlements();
+
+ if ($wallet->balance < 0) {
+ \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,207 @@
+<?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' => url('/wallet'), // required for non-recurring payments
+ 'webhookUrl' => url('/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, $id)
+ {
+ $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
+ }
+ }
+
+ $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' => url('/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 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,10 @@
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.url) {
+ 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');
}
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 11:49 PM (4 h, 48 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831631
Default Alt Text
D1030.1775346553.diff (14 KB)
Attached To
Mode
D1030: Mollie payments
Attached
Detach File
Event Timeline