Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117539814
D2239.1774856688.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
38 KB
Referenced Files
None
Subscribers
None
D2239.1774856688.diff
View Options
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
@@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Providers\PaymentProvider;
use App\Wallet;
+use App\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
@@ -55,6 +56,7 @@
$mandate = [
'currency' => 'CHF',
'description' => \config('app.name') . ' Auto-Payment Setup',
+ 'methodId' => $request->methodId
];
// Normally the auto-payment setup operation is 0, if the balance is below the threshold
@@ -217,8 +219,9 @@
$request = [
'type' => PaymentProvider::TYPE_ONEOFF,
- 'currency' => 'CHF',
+ 'currency' => $request->currency,
'amount' => $amount,
+ 'methodId' => $request->methodId,
'description' => \config('app.name') . ' Payment',
];
@@ -231,6 +234,32 @@
return response()->json($result);
}
+ /**
+ * Delete a pending payment.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function cancel(Request $request)
+ {
+ $current_user = Auth::guard()->user();
+
+ // TODO: Wallet selection
+ $wallet = $current_user->wallets()->first();
+
+ $paymentId = $request->payment;
+
+ $provider = PaymentProvider::factory($wallet);
+
+ $result = $provider->cancel($wallet, $paymentId);
+
+ //TODO handle errors?
+ $result['status'] = 'success';
+
+ return response()->json($result);
+ }
+
/**
* Update payment status (and balance).
*
@@ -325,4 +354,134 @@
return $mandate;
}
+
+
+ /**
+ * Return an array of whitelisted payment methods with override values.
+ *
+ * @param string $type The payment type for which we require a method.
+ *
+ * @return array Array of methods
+ */
+ private static function paymentMethodsWhitelist($type): ?array
+ {
+ switch ($type) {
+ case PaymentProvider::TYPE_ONEOFF:
+ return [
+ 'creditcard' => [
+ 'id' => 'creditcard',
+ 'icon' => ['prefix' => 'far', 'name' => 'credit-card']
+ ],
+ 'paypal' => [
+ 'id' => 'paypal',
+ 'icon' => ['prefix' => 'fab', 'name' => 'paypal']
+ ],
+ 'banktransfer' => [
+ 'id' => 'banktransfer',
+ 'icon' => ['prefix' => 'fas', 'name' => 'university']
+ ]
+ ];
+ case PaymentProvider::TYPE_RECURRING:
+ return [
+ 'creditcard' => [
+ 'id' => 'creditcard',
+ 'icon' => ['prefix' => 'far', 'name' => 'credit-card']
+ ],
+ 'paypal' => [
+ 'id' => 'paypal',
+ 'icon' => ['prefix' => 'fab', 'name' => 'paypal']
+ ]
+ ];
+ }
+ }
+
+
+ /**
+ * List supported payment methods.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public static function paymentMethods(Request $request)
+ {
+ $user = Auth::guard()->user();
+
+ // TODO: Wallet selection
+ $wallet = $user->wallets()->first();
+
+ $provider = PaymentProvider::factory($wallet);
+
+ $providerMethods = $provider->paymentMethods($request->type);
+
+ \Log::debug("Provider methods" . var_export(json_encode($providerMethods), true));
+
+ $whitelistMethods = self::paymentMethodsWhitelist($request->type);
+
+ $methods = [];
+
+ // Use only whitelisted methods, and apply values from whitelist (overriding the backend)
+ foreach ($whitelistMethods as $id => $whitelistMethod) {
+ if (array_key_exists($id, $providerMethods)) {
+ $methods[] = array_merge($providerMethods[$id], $whitelistMethod);
+ }
+ }
+ return response()->json($methods);
+ }
+
+
+ /**
+ * List pending payments.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public static function payments(Request $request)
+ {
+ $user = Auth::guard()->user();
+
+ // TODO: Wallet selection
+ $wallet = $user->wallets()->first();
+
+ $provider = PaymentProvider::factory($wallet);
+ $pageSize = 10;
+ $page = intval(request()->input('page')) ?: 1;
+ $hasMore = false;
+ $result = Payment::where('wallet_id', $wallet->id)
+ ->where('type', 'oneoff')
+ ->whereIn('status', ['open', 'pending', 'authorized'])
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ $result = $result->map(function ($item) use ($provider) {
+ $payment = $provider->getPayment($item->id);
+ $entry = [
+ 'id' => $item->id,
+ 'createdAt' => $item->created_at->format('Y-m-d H:i'),
+ 'type' => $item->type,
+ 'description' => $item->description,
+ 'amount' => $item->amount,
+ 'status' => $item->status,
+ 'isCancelable' => $payment['isCancelable'],
+ 'checkoutUrl' => $payment['checkoutUrl']
+ ];
+
+ return $entry;
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ 'page' => $page,
+ ]);
+ }
}
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -7,11 +7,13 @@
/**
* A payment operation on a wallet.
*
- * @property int $amount Amount of money in cents
+ * @property int $amount Amount of money in cents of CHF
* @property string $description Payment description
* @property string $id Mollie's Payment ID
* @property \App\Wallet $wallet The wallet
* @property string $wallet_id The ID of the wallet
+ * @property int $amountInCurrency Amount of money in cents of $currency
+ * @property string $currency Currency of this payment
*/
class Payment extends Model
{
@@ -30,6 +32,8 @@
'provider',
'status',
'type',
+ 'currency',
+ 'amountInCurrency',
];
/**
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
@@ -66,7 +66,7 @@
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
'redirectUrl' => Utils::serviceUrl('/wallet'),
'locale' => 'en_US',
- // 'method' => 'creditcard',
+ 'method' => $payment['methodId']
];
// Create the payment in Mollie
@@ -135,7 +135,8 @@
'id' => $mandate->id,
'isPending' => $mandate->isPending(),
'isValid' => $mandate->isValid(),
- 'method' => self::paymentMethod($mandate, 'Unknown method')
+ 'method' => self::paymentMethod($mandate, 'Unknown method'),
+ 'methodId' => $mandate->method
];
return $result;
@@ -151,6 +152,17 @@
return 'mollie';
}
+
+ private function exchangeRate(string $currency): float
+ {
+ //TODO pass in wallet and do conversion there?
+ if ($currency != 'CHF') {
+ //FIXME
+ return 0.8;
+ }
+ return 1.0;
+ }
+
/**
* Create a new payment.
*
@@ -174,20 +186,24 @@
// Register the user in Mollie, if not yet done
$customer_id = self::mollieCustomerId($wallet, true);
+ $amount = $payment['amount'];
+ $exchangeRate = $this->exchangeRate($wallet, $payment['currency']);
+ $amount = $amount * $exchangeRate;
+ $payment['amountInCurrency'] = $amount;
// Note: Required fields: description, amount/currency, amount/value
$request = [
'amount' => [
'currency' => $payment['currency'],
// a number with two decimals is required
- 'value' => sprintf('%.2f', $payment['amount'] / 100),
+ 'value' => sprintf('%.2f', $amount / 100),
],
'customerId' => $customer_id,
'sequenceType' => $payment['type'],
'description' => $payment['description'],
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
'locale' => 'en_US',
- // 'method' => 'creditcard',
+ 'method' => $payment['methodId'],
'redirectUrl' => Utils::serviceUrl('/wallet') // required for non-recurring payments
];
@@ -210,6 +226,34 @@
];
}
+
+ /**
+ * Cancel a pending payment.
+ *
+ * @param \App\Wallet $wallet The wallet
+ * @param array $paymentId Payment Id
+ *
+ * @return array Provider payment data:
+ * - id: Operation identifier
+ * - redirectUrl: the location to redirect to
+ */
+ public function cancel(Wallet $wallet, $paymentId): ?array
+ {
+
+ $response = mollie()->payments()->delete($paymentId);
+
+ $payment['id'] = $response->id;
+ $payment['status'] = $response->status;
+
+ $this->storePayment($payment, $wallet->id);
+
+ return [
+ 'id' => $payment['id'],
+ 'redirectUrl' => $response->getCheckoutUrl(),
+ ];
+ }
+
+
/**
* Create a new automatic payment operation.
*
@@ -243,7 +287,7 @@
'description' => $payment['description'],
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
'locale' => 'en_US',
- // 'method' => 'creditcard',
+ 'method' => $payment['methodId'],
'mandateId' => $mandate->id
];
@@ -333,6 +377,7 @@
'description' => $refund->description,
'amount' => round(floatval($refund->amount->value) * 100),
'type' => self::TYPE_REFUND,
+ //FIXME Convert amount from currency to wallet currency
// Note: we assume this is the original payment/wallet currency
];
}
@@ -348,6 +393,7 @@
'id' => $chargeback->id,
'amount' => round(floatval($chargeback->amount->value) * 100),
'type' => self::TYPE_CHARGEBACK,
+ //FIXME Convert amount from currency to wallet currency
// Note: we assume this is the original payment/wallet currency
];
}
@@ -510,4 +556,80 @@
return $default;
}
+
+ /**
+ * List supported payment methods.
+ *
+ * @param string $type type: oneoff/recurring
+ *
+ * @return array Array of array with available payment methods:
+ * - id: id of the method
+ * - name: User readable name of the payment method
+ * - minimumAmount: Minimum amount to be charged
+ * - currency: Currency used for the method
+ * - image|icon: An image (url) or icon (icon name) representing the method
+ */
+ public function paymentMethods($type): ?array
+ {
+ $availableMethods = [];
+
+ $mapResult = function ($result) use (&$availableMethods) {
+ foreach ($result as $method) {
+ $availableMethods[$method->id] = [
+ 'id' => $method->id,
+ 'name' => $method->description,
+ 'minimumAmount' => round(floatval($method->minimumAmount->value) * 100), // Converted to cents
+ 'currency' => $method->minimumAmount->currency,
+ 'exchangeRate' => $this->exchangeRate($method->minimumAmount->currency)
+ ];
+ }
+ };
+
+ // Fallback to EUR methods
+ $mapResult(mollie()->methods()->allActive(
+ [
+ 'sequenceType' => $type,
+ 'amount' => [
+ 'value' => '1.00',
+ 'currency' => 'EUR'
+ ]
+ ]
+ ));
+
+ // Prefer CHF methods
+ $mapResult(mollie()->methods()->allActive(
+ [
+ 'sequenceType' => $type,
+ 'amount' => [
+ 'value' => '1.00',
+ 'currency' => 'CHF'
+ ]
+ ]
+ ));
+
+ return $availableMethods;
+ }
+
+ /**
+ * Get a payment.
+ *
+ * @param string $paymentId Payment identifier
+ *
+ * @return array|null Payment information:
+ * - id: Payment identifier
+ * - status: Payment status
+ * - isCancelable: The payment can be canceled
+ * - chceckoutUrl: The checkout url to complete the payment or null if non
+ */
+ public function getPayment($paymentId): ?array
+ {
+ $payment = mollie()->payments()->get($paymentId);
+
+ return [
+ 'id' => $payment->id,
+ 'status' => $payment->status,
+ 'isCancelable' => $payment->isCancelable,
+ 'checkoutUrl' => $payment->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
@@ -470,4 +470,71 @@
return $default;
}
+
+ /**
+ * List supported payment methods.
+ *
+ * @param string $type type: oneoff/recurring
+ *
+ * @return array Array of array with available payment methods:
+ * - id: id of the method
+ * - name: User readable name of the payment method
+ * - minimumAmount: Minimum amount to be charged
+ * - currency: Currency used for the method
+ * - image|icon: An image (url) or icon (icon name) representing the method
+ */
+ public function paymentMethods($type): ?array
+ {
+ //TODO get this from the stripe API?
+ switch ($type) {
+ case self::TYPE_ONEOFF:
+ return [
+ [
+ 'id' => 'creditcard',
+ 'name' => "Credit Card",
+ 'minimumAmount' => "10.00",
+ 'currency' => 'CHF',
+ 'icon' => "credit-card"
+ ],
+ [
+ 'id' => 'paypal',
+ 'name' => "PayPal",
+ 'minimumAmount' => "10.00",
+ 'currency' => 'CHF',
+ 'icon' => "paypal"
+ ]
+ ];
+ case self::TYPE_RECURRING:
+ return [
+ [
+ 'id' => 'creditcard',
+ 'name' => "Credit Card",
+ 'minimumAmount' => "10.00",
+ 'currency' => 'CHF',
+ 'icon' => "credit-card"
+ ]
+ ];
+ }
+ }
+
+ /**
+ * Get a payment.
+ *
+ * @param string $paymentId Payment identifier
+ *
+ * @return array|null Payment information:
+ * - id: Payment identifier
+ * - status: Payment status
+ * - isCancelable: The payment can be canceled
+ * - chceckoutUrl: The checkout url to complete the payment or null if non
+ */
+ public function getPayment($paymentId): ?array
+ {
+ return [
+ 'id' => $paymentId,
+ 'status' => null,
+ 'isCancelable' => false,
+ 'checkoutUrl' => null
+ ];
+ }
}
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -194,4 +194,31 @@
$this->storePayment($refund, $wallet->id);
}
+
+ /**
+ * List supported payment methods.
+ *
+ * @param string $type type: oneoff/recurring
+ *
+ * @return array Array of array with available payment methods:
+ * - id: id of the method
+ * - name: User readable name of the payment method
+ * - minimumAmount: Minimum amount to be charged
+ * - currency: Currency used for the method
+ * - image|icon: An image (url) or icon (icon name) representing the method
+ */
+ abstract public function paymentMethods($type): ?array;
+
+ /**
+ * Get a payment.
+ *
+ * @param string $paymentId Payment identifier
+ *
+ * @return array|null Payment information:
+ * - id: Payment identifier
+ * - status: Payment status
+ * - isCancelable: The payment can be canceled
+ * - chceckoutUrl: The checkout url to complete the payment or null if non
+ */
+ abstract public function getPayment($paymentId): ?array;
}
diff --git a/src/resources/js/fontawesome.js b/src/resources/js/fontawesome.js
--- a/src/resources/js/fontawesome.js
+++ b/src/resources/js/fontawesome.js
@@ -15,6 +15,7 @@
faDownload,
faEnvelope,
faGlobe,
+ faUniversity,
faExclamationCircle,
faInfoCircle,
faLock,
@@ -30,6 +31,10 @@
faWallet
} from '@fortawesome/free-solid-svg-icons'
+import {
+ faPaypal
+} from '@fortawesome/free-brands-svg-icons'
+
// Register only these icons we need
library.add(
faCheck,
@@ -37,6 +42,8 @@
faCheckSquare,
faComments,
faCreditCard,
+ faPaypal,
+ faUniversity,
faDownload,
faEnvelope,
faExclamationCircle,
diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss
--- a/src/resources/themes/app.scss
+++ b/src/resources/themes/app.scss
@@ -293,6 +293,33 @@
}
}
+#payment-method-selection {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ & > button {
+ padding: 1rem;
+ text-align: center;
+ white-space: nowrap;
+ margin: 0.25rem;
+ text-decoration: none;
+ width: 150px;
+ }
+
+ img {
+ width: 6rem;
+ height: 6rem;
+ margin: auto;
+ }
+
+ svg {
+ width: 6rem;
+ height: 6rem;
+ margin: auto;
+ }
+}
+
#logon-form {
flex-basis: auto; // Bootstrap issue? See logon page with width < 992
}
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
@@ -5,8 +5,48 @@
<div class="card-title">Account balance <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
<div class="card-text">
<p v-if="wallet.notice" id="wallet-notice">{{ wallet.notice }}</p>
- <p>Add credit to your account or setup an automatic payment by using the button below.</p>
- <button type="button" class="btn btn-primary" @click="paymentDialog()">Add credit</button>
+
+ <div v-if="showPendingPayments" class="alert alert-warning">
+ You have a payments that are still in progress. See the payments tab below.
+ </div>
+ <p>
+ <button type="button" class="btn btn-primary" @click="paymentMethodForm('manual')">Add credit</button>
+ </p>
+ <div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending">
+ <template v-if="mandate.id && !mandate.isValid">
+ <div class="alert alert-danger">
+ The setup of automatic payments failed. Restart the process to enable automatic top-ups.
+ </div>
+ <p>
+ <button type="button" class="btn btn-danger" @click="autoPaymentDelete">Cancel auto-payment</button>
+ </p>
+ </template>
+ <button type="button" class="btn btn-primary" @click="paymentMethodForm('auto')">Set up auto-payment</button>
+ </div>
+ <div id="mandate-info" v-else>
+ <div v-if="mandate.isDisabled" class="disabled-mandate alert alert-danger">
+ The configured auto-payment has been disabled. Top up your wallet or
+ raise the auto-payment amount.
+ </div>
+ <template v-else>
+ <p>
+ Auto-payment is <b>enabled</b> to add <b>{{ mandate.amount }} CHF</b>
+ to your account whenever your balance runs below <b>{{ mandate.balance }} CHF</b>.
+ </p>
+ <p>
+ Method of payment: {{ mandate.method }}
+ </p>
+ </template>
+ <div v-if="mandate.isPending" class="alert alert-warning">
+ The setup of the automatic payment is still in progress.
+ </div>
+ <p>
+ <button type="button" class="btn btn-danger" @click="autoPaymentDelete">Cancel auto-payment</button>
+ </p>
+ <p>
+ <button type="button" class="btn btn-primary" @click="autoPaymentChange">Change auto-payment</button>
+ </p>
+ </div>
</div>
</div>
</div>
@@ -22,6 +62,11 @@
History
</a>
</li>
+ <li v-if="showPendingPayments" class="nav-item">
+ <a class="nav-link" id="tab-payments" href="#wallet-payments" role="tab" aria-controls="wallet-payments" aria-selected="false">
+ Pending Payments
+ </a>
+ </li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="wallet-receipts" role="tabpanel" aria-labelledby="tab-receipts">
@@ -53,6 +98,11 @@
<transaction-log v-if="walletId && loadTransactions" class="card-text" :wallet-id="walletId"></transaction-log>
</div>
</div>
+ <div class="tab-pane show" id="wallet-payments" role="tabpanel" aria-labelledby="tab-payments">
+ <div class="card-body">
+ <payment-log v-if="walletId && loadPayments" class="card-text" :wallet-id="walletId"></payment-log>
+ </div>
+ </div>
</div>
<div id="payment-dialog" class="modal" tabindex="-1" role="dialog">
@@ -65,7 +115,26 @@
</button>
</div>
<div class="modal-body">
- <div id="payment" v-if="paymentForm == 'init'">
+ <div id="payment-method" v-if="paymentForm == 'method'">
+ <form data-validation-prefix="mandate_">
+ <div id="payment-method-selection">
+ <button v-for="method in paymentMethods" :key="method.id" @click="selectPaymentMethod(method)" class="card link-profile">
+ <svg-icon v-if="method.icon" :icon="[method.icon.prefix, method.icon.name]" />
+ <img v-if="method.image" v-bind:src="method.image" />
+ <span class="name">{{ method.name }}</span>
+ </button>
+ </div>
+ </form>
+ </div>
+ <div id="manual-payment" v-if="paymentForm == 'manual'">
+ <p v-if="wallet.currency != selectedPaymentMethod.currency">
+ Here is how it works: You specify the amount by which you want to to up your wallet in {{ wallet.currency }}.
+ We will then convert this to {{ selectedPaymentMethod.currency }}, and on the next page you will be provided with the bank-details
+ to transfer the amount in {{ selectedPaymentMethod.currency }}.
+ </p>
+ <p v-if="selectedPaymentMethod.id == 'banktransfer'">
+ Please note that a bank transfer can take several days to complete.
+ </p>
<p>Choose the amount by which you want to top up your wallet.</p>
<form id="payment-form" @submit.prevent="payment">
<div class="form-group">
@@ -76,41 +145,15 @@
</span>
</div>
</div>
+ <div v-if="wallet.currency != selectedPaymentMethod.currency" class="alert alert-warning">
+ You will be charged for {{ amount * selectedPaymentMethod.exchangeRate }} {{ selectedPaymentMethod.currency }}
+ </div>
<div class="w-100 text-center">
<button type="submit" class="btn btn-primary">
<svg-icon :icon="['far', 'credit-card']"></svg-icon> Continue
</button>
</div>
</form>
- <div class="form-separator"><hr><span>or</span></div>
- <div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending">
- <p>Add auto-payment, so you never run out.</p>
- <div v-if="mandate.id && !mandate.isValid" class="alert alert-danger">
- The setup of automatic payments failed. Restart the process to enable automatic top-ups.
- </div>
- <div class="w-100 text-center">
- <button type="button" class="btn btn-primary" @click="autoPaymentForm">Set up auto-payment</button>
- </div>
- </div>
- <div id="mandate-info" v-else>
- <p>
- Auto-payment is set to fill up your account by <b>{{ mandate.amount }} CHF</b>
- every time your account balance gets under <b>{{ mandate.balance }} CHF</b>.
- You will be charged via {{ mandate.method }}.
- </p>
- <div v-if="mandate.isPending" class="alert alert-warning">
- The setup of the automatic payment is still in progress.
- </div>
- <div v-else-if="mandate.isDisabled" class="disabled-mandate alert alert-danger">
- The configured auto-payment has been disabled. Top up your wallet or
- raise the auto-payment amount.
- </div>
- <p>You can cancel or change the auto-payment at any time.</p>
- <div class="form-group d-flex justify-content-around">
- <button type="button" class="btn btn-danger" @click="autoPaymentDelete">Cancel auto-payment</button>
- <button type="button" class="btn btn-primary" @click="autoPaymentChange">Change auto-payment</button>
- </div>
- </div>
</div>
<div id="auto-payment" v-if="paymentForm == 'auto'">
<form data-validation-prefix="mandate_">
@@ -173,22 +216,29 @@
<script>
import TransactionLog from './Widgets/TransactionLog'
+ import PaymentLog from './Widgets/PaymentLog'
export default {
components: {
- TransactionLog
+ TransactionLog,
+ PaymentLog
},
data() {
return {
amount: '',
- mandate: { amount: 10, balance: 0 },
+ mandate: { amount: 10, balance: 0, method: null },
paymentDialogTitle: null,
- paymentForm: 'init',
+ paymentForm: null,
+ nextForm: null,
receipts: [],
stripe: null,
loadTransactions: false,
+ loadPayments: false,
+ showPendingPayments: false,
wallet: {},
- walletId: null
+ walletId: null,
+ paymentMethods: [],
+ selectedPaymentMethod: null
}
},
mounted() {
@@ -220,17 +270,28 @@
})
.catch(this.$root.errorHandler)
+ this.loadMandate()
+
+ axios.get('/api/v4/payments/pending')
+ .then(response => {
+ this.showPendingPayments = (response.data.list.length > 0)
+ })
+
+ },
+ updated() {
$(this.$el).find('ul.nav-tabs a').on('click', e => {
e.preventDefault()
$(e.target).tab('show')
if ($(e.target).is('#tab-history')) {
this.loadTransactions = true
}
+ if ($(e.target).is('#tab-payments')) {
+ this.loadPayments = true
+ }
})
},
methods: {
- paymentDialog() {
- const dialog = $('#payment-dialog')
+ loadMandate() {
const mandate_form = $('#mandate-form')
this.$root.removeLoader(mandate_form)
@@ -246,14 +307,17 @@
this.$root.removeLoader(mandate_form)
})
}
+ },
+ selectPaymentMethod(method) {
+ this.formLock = false
+
+ this.selectedPaymentMethod = method
+ this.paymentForm = this.nextForm
+ this.paymentDialogTitle = 'Choose Amount'
this.formLock = false
- this.paymentForm = 'init'
- this.paymentDialogTitle = 'Top up your wallet'
- this.dialog = dialog.on('shown.bs.modal', () => {
- dialog.find('#amount').focus()
- }).modal()
+ setTimeout(() => { this.dialog.find('#mandate_amount').focus()}, 10)
},
payment() {
if (this.formLock) {
@@ -266,7 +330,7 @@
this.$root.clearFormValidation($('#payment-form'))
- axios.post('/api/v4/payments', {amount: this.amount}, { onFinish })
+ axios.post('/api/v4/payments', {amount: this.amount, methodId: this.selectedPaymentMethod.id, currency: this.selectedPaymentMethod.currency}, { onFinish })
.then(response => {
if (response.data.redirectUrl) {
location.href = response.data.redirectUrl
@@ -287,7 +351,9 @@
const method = this.mandate.id && (this.mandate.isValid || this.mandate.isPending) ? 'put' : 'post'
const post = {
amount: this.mandate.amount,
- balance: this.mandate.balance
+ balance: this.mandate.balance,
+ methodId: this.selectedPaymentMethod.id,
+ currency: this.selectedPaymentMethod.currency
}
this.$root.clearFormValidation($('#auto-payment form'))
@@ -324,10 +390,40 @@
}
})
},
+
+ paymentMethodForm(nextForm) {
+ const dialog = $('#payment-dialog')
+ this.formLock = false
+ this.paymentMethods = []
+
+ this.paymentForm = 'method'
+ this.paymentDialogTitle = 'Payment Method'
+ this.nextForm = nextForm
+
+ const methods = $('#payment-method')
+ this.$root.addLoader(methods)
+ axios.get('/api/v4/payments/methods', {params: {type: nextForm == 'manual' ? 'oneoff' : 'recurring'}})
+ .then(response => {
+ this.$root.removeLoader(methods)
+ this.paymentMethods = response.data
+ })
+ .catch(this.$root.errorHandler)
+
+ this.dialog = dialog.on('shown.bs.modal', () => {
+ dialog.find('#mandate_amount').focus()
+ }).modal()
+ },
autoPaymentForm(event, title) {
+ const dialog = $('#payment-dialog')
+
this.paymentForm = 'auto'
this.paymentDialogTitle = title || 'Add auto-payment'
this.formLock = false
+
+ this.dialog = dialog.on('shown.bs.modal', () => {
+ dialog.find('#mandate_amount').focus()
+ }).modal()
+
setTimeout(() => { this.dialog.find('#mandate_amount').focus()}, 10)
},
receiptDownload() {
diff --git a/src/resources/vue/Widgets/PaymentLog.vue b/src/resources/vue/Widgets/PaymentLog.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/PaymentLog.vue
@@ -0,0 +1,81 @@
+<template>
+ <div>
+ <table class="table table-sm m-0 payments">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col">Description</th>
+ <th scope="col" class="price">Amount</th>
+ <th scope="col">Status</th>
+ <th scope="col"></th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="payment in payments" :id="'log' + payment.id" :key="payment.id">
+ <td class="datetime">{{ payment.createdAt }}</td>
+ <td class="description">{{ payment.description }}</td>
+ <td :class="'price ' + text-success">{{ amount(payment) }}</td>
+ <td>{{ payment.status }}</td>
+ <td><a v-if="payment.checkoutUrl" v-bind:href="payment.checkoutUrl">Details</a></td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td :colspan="4">There are no payments for this account.</td>
+ </tr>
+ </tfoot>
+ </table>
+ <div class="text-center p-3" id="payments-loader" v-if="hasMore">
+ <button class="btn btn-secondary" @click="loadLog(true)">Load more</button>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ },
+ data() {
+ return {
+ payments: [],
+ hasMore: false,
+ page: 1
+ }
+ },
+ mounted() {
+ this.loadLog()
+ },
+ methods: {
+ loadLog(more) {
+ let loader = $(this.$el)
+ let param = ''
+
+ if (more) {
+ param = '?page=' + (this.page + 1)
+ loader = $('#payments-loader')
+ }
+
+ this.$root.addLoader(loader)
+ axios.get('/api/v4/payments/pending' + param)
+ .then(response => {
+ this.$root.removeLoader(loader)
+ // Note: In Vue we can't just use .concat()
+ for (let i in response.data.list) {
+ this.$set(this.payments, this.payments.length, response.data.list[i])
+ }
+ this.hasMore = response.data.hasMore
+ this.page = response.data.page || 1
+ })
+ .catch(error => {
+ this.$root.removeLoader(loader)
+ })
+ },
+ amount(payment) {
+ return this.$root.price(payment.amount)
+ }
+
+
+ }
+ }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -76,10 +76,13 @@
Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload');
Route::post('payments', 'API\V4\PaymentsController@store');
+ Route::delete('payments', 'API\V4\PaymentsController@cancel');
Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate');
Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate');
Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete');
+ Route::get('payments/methods', 'API\V4\PaymentsController@paymentMethods');
+ Route::get('payments/pending', 'API\V4\PaymentsController@payments');
Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Mar 30, 7:44 AM (3 d, 10 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18797478
Default Alt Text
D2239.1774856688.diff (38 KB)
Attached To
Mode
D2239: New Payment dialog structure
Attached
Detach File
Event Timeline