Page MenuHomePhorge

D1030.1775346553.diff
No OneTemporary

Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None

D1030.1775346553.diff

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

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)

Event Timeline