diff --git a/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php b/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php --- a/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php +++ b/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php @@ -27,23 +27,20 @@ */ public function handle() { - $sourceCurrency = 'CHF'; + foreach (['CHF', 'EUR'] as $sourceCurrency) { + $rates = \App\Backends\OpenExchangeRates::retrieveRates($sourceCurrency); - $rates = \App\Backends\OpenExchangeRates::retrieveRates($sourceCurrency); + $file = resource_path("exchangerates-$sourceCurrency.php"); - // - // export - // - $file = resource_path("exchangerates-$sourceCurrency.php"); + $out = " $rate) { + $out .= sprintf(" '%s' => '%s',\n", $countryCode, $rate); + } - foreach ($rates as $countryCode => $rate) { - $out .= sprintf(" '%s' => '%s',\n", $countryCode, $rate); - } - - $out .= "];\n"; + $out .= "];\n"; - file_put_contents($file, $out); + file_put_contents($file, $out); + } } } diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php --- a/src/app/Documents/Receipt.php +++ b/src/app/Documents/Receipt.php @@ -50,7 +50,7 @@ { $wallet = new Wallet(); $wallet->id = \App\Utils::uuidStr(); - $wallet->owner = new User(['id' => 123456789]); // @phpstan-ignore-line + $wallet->owner = new User(['id' => 123456789]); $receipt = new self($wallet, date('Y'), date('n')); 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 @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Providers\PaymentProvider; +use App\Tenant; use App\Wallet; use App\Payment; use Illuminate\Http\Request; @@ -53,8 +54,8 @@ ]); $mandate = [ - 'currency' => 'CHF', - 'description' => \config('app.name') . ' Auto-Payment Setup', + 'currency' => $wallet->currency, + 'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup', 'methodId' => $request->methodId ]; @@ -173,7 +174,7 @@ } if ($amount < PaymentProvider::MIN_AMOUNT) { - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; + $min = $wallet->money(PaymentProvider::MIN_AMOUNT); return ['amount' => \trans('validation.minamount', ['amount' => $min])]; } @@ -211,7 +212,7 @@ // Validate the minimum value if ($amount < PaymentProvider::MIN_AMOUNT) { - $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF'; + $min = $wallet->money(PaymentProvider::MIN_AMOUNT); $errors = ['amount' => \trans('validation.minamount', ['amount' => $min])]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } @@ -221,7 +222,7 @@ 'currency' => $request->currency, 'amount' => $amount, 'methodId' => $request->methodId, - 'description' => \config('app.name') . ' Payment', + 'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Payment', ]; $provider = PaymentProvider::factory($wallet); @@ -327,10 +328,10 @@ $request = [ 'type' => PaymentProvider::TYPE_RECURRING, - 'currency' => 'CHF', + 'currency' => $wallet->currency, 'amount' => $amount, 'methodId' => PaymentProvider::METHOD_CREDITCARD, - 'description' => \config('app.name') . ' Recurring Payment', + 'description' => Tenant::getConfig($wallet->owner->tenant_id, 'app.name') . ' Recurring Payment', ]; $result = $provider->payment($wallet, $request); @@ -449,7 +450,7 @@ $hasMore = true; } - $result = $result->map(function ($item) { + $result = $result->map(function ($item) use ($wallet) { $provider = PaymentProvider::factory($item->provider); $payment = $provider->getPayment($item->id); $entry = [ @@ -458,6 +459,8 @@ 'type' => $item->type, 'description' => $item->description, 'amount' => $item->amount, + 'currency' => $wallet->currency, + // note: $item->currency/$item->currency_amount might be different 'status' => $item->status, 'isCancelable' => $payment['isCancelable'], 'checkoutUrl' => $payment['checkoutUrl'] diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -256,13 +256,14 @@ } } - $result = $result->map(function ($item) use ($isAdmin) { + $result = $result->map(function ($item) use ($isAdmin, $wallet) { $entry = [ 'id' => $item->id, 'createdAt' => $item->created_at->format('Y-m-d H:i'), 'type' => $item->type, 'description' => $item->shortDescription(), 'amount' => $item->amount, + 'currency' => $wallet->currency, 'hasDetails' => !empty($item->cnt), ]; diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -56,7 +56,7 @@ { $settings = [ 'country' => \App\Utils::countryForRequest(), - 'currency' => 'CHF', + 'currency' => \config('app.currency'), /* 'first_name' => '', 'last_name' => '', 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 @@ -25,6 +25,8 @@ break; } } + + $wallet->currency = \config('app.currency'); } /** diff --git a/src/app/Payment.php b/src/app/Payment.php --- a/src/app/Payment.php +++ b/src/app/Payment.php @@ -7,7 +7,7 @@ /** * A payment operation on a wallet. * - * @property int $amount Amount of money in cents of CHF + * @property int $amount Amount of money in cents of system currency * @property string $description Payment description * @property string $id Mollie's Payment ID * @property \App\Wallet $wallet The wallet 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 @@ -547,6 +547,7 @@ * List supported payment methods. * * @param string $type The payment type for which we require a method (oneoff/recurring). + * @param string $currency Currency code * * @return array Array of array with available payment methods: * - id: id of the method @@ -556,30 +557,34 @@ * - exchangeRate: The projected exchange rate (actual rate is determined during payment) * - icon: An icon (icon name) representing the method */ - public function providerPaymentMethods($type): array + public function providerPaymentMethods(string $type, string $currency): array { - $providerMethods = array_merge( - // Fallback to EUR methods (later provider methods will override earlier ones) - (array) mollie()->methods()->allActive( - [ - 'sequenceType' => $type, - 'amount' => [ - 'value' => '1.00', - 'currency' => 'EUR' - ] + // Prefer methods in the system currency + $providerMethods = (array) mollie()->methods()->allActive( + [ + 'sequenceType' => $type, + 'amount' => [ + 'value' => '1.00', + 'currency' => $currency ] - ), - // Prefer CHF methods - (array) mollie()->methods()->allActive( + ] + ); + + // Get EUR methods (e.g. bank transfers are in EUR only) + if ($currency != 'EUR') { + $eurMethods = (array) mollie()->methods()->allActive( [ 'sequenceType' => $type, 'amount' => [ 'value' => '1.00', - 'currency' => 'CHF' + 'currency' => 'EUR' ] ] - ) - ); + ); + + // Later provider methods will override earlier ones + $providerMethods = array_merge($eurMethods, $providerMethods); + } $availableMethods = []; @@ -589,7 +594,7 @@ 'name' => $method->description, 'minimumAmount' => round(floatval($method->minimumAmount->value) * 100), // Converted to cents 'currency' => $method->minimumAmount->currency, - 'exchangeRate' => \App\Utils::exchangeRate('CHF', $method->minimumAmount->currency) + 'exchangeRate' => \App\Utils::exchangeRate($currency, $method->minimumAmount->currency) ]; } 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 @@ -483,7 +483,8 @@ /** * List supported payment methods. * - * @param string $type The payment type for which we require a method (oneoff/recurring). + * @param string $type The payment type for which we require a method (oneoff/recurring). + * @param string $currency Currency code * * @return array Array of array with available payment methods: * - id: id of the method @@ -493,7 +494,7 @@ * - exchangeRate: The projected exchange rate (actual rate is determined during payment) * - icon: An icon (icon name) representing the method */ - public function providerPaymentMethods($type): array + public function providerPaymentMethods(string $type, string $currency): array { //TODO get this from the stripe API? $availableMethods = []; @@ -504,14 +505,14 @@ 'id' => self::METHOD_CREDITCARD, 'name' => "Credit Card", 'minimumAmount' => self::MIN_AMOUNT, - 'currency' => 'CHF', + 'currency' => $currency, 'exchangeRate' => 1.0 ], self::METHOD_PAYPAL => [ 'id' => self::METHOD_PAYPAL, 'name' => "PayPal", 'minimumAmount' => self::MIN_AMOUNT, - 'currency' => 'CHF', + 'currency' => $currency, 'exchangeRate' => 1.0 ] ]; @@ -522,7 +523,7 @@ 'id' => self::METHOD_CREDITCARD, 'name' => "Credit Card", 'minimumAmount' => self::MIN_AMOUNT, // Converted to cents, - 'currency' => 'CHF', + 'currency' => $currency, 'exchangeRate' => 1.0 ] ]; 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 @@ -254,7 +254,8 @@ /** * List supported payment methods from this provider * - * @param string $type The payment type for which we require a method (oneoff/recurring). + * @param string $type The payment type for which we require a method (oneoff/recurring). + * @param string $currency Currency code * * @return array Array of array with available payment methods: * - id: id of the method @@ -264,7 +265,7 @@ * - exchangeRate: The projected exchange rate (actual rate is determined during payment) * - icon: An icon (icon name) representing the method */ - abstract public function providerPaymentMethods($type): array; + abstract public function providerPaymentMethods(string $type, string $currency): array; /** * Get a payment. @@ -345,7 +346,7 @@ { $providerName = self::providerName($wallet); - $cacheKey = "methods-" . $providerName . '-' . $type; + $cacheKey = "methods-{$providerName}-{$type}-{$wallet->currency}"; if ($methods = Cache::get($cacheKey)) { \Log::debug("Using payment method cache" . var_export($methods, true)); @@ -353,7 +354,8 @@ } $provider = PaymentProvider::factory($providerName); - $methods = self::applyMethodWhitelist($type, $provider->providerPaymentMethods($type)); + $methods = $provider->providerPaymentMethods($type, $wallet->currency); + $methods = self::applyMethodWhitelist($type, $methods); \Log::debug("Loaded payment methods" . var_export($methods, true)); diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -14,7 +14,12 @@ * * A wallet is owned by an {@link \App\User}. * - * @property integer $balance + * @property int $balance Current balance in cents + * @property string $currency Currency code + * @property ?string $description Description + * @property string $id Unique identifier + * @property ?\App\User $owner Owner (can be null when owner is deleted) + * @property int $user_id Owner's identifier */ class Wallet extends Model { @@ -26,19 +31,39 @@ public $timestamps = false; + /** + * The attributes' default values. + * + * @var array + */ protected $attributes = [ 'balance' => 0, - 'currency' => 'CHF' ]; + /** + * The attributes that are mass assignable. + * + * @var array + */ protected $fillable = [ - 'currency' + 'currency', + 'description' ]; + /** + * The attributes that can be not set. + * + * @var array + */ protected $nullable = [ 'description', ]; + /** + * The types of attributes to which its values will be cast + * + * @var array + */ protected $casts = [ 'balance' => 'integer', ]; @@ -345,15 +370,10 @@ { $amount = round($amount / 100, 2); - // Prefer intl extension's number formatter - if (class_exists('NumberFormatter')) { - $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); - $result = $nf->formatCurrency($amount, $this->currency); - // Replace non-breaking space - return str_replace("\xC2\xA0", " ", $result); - } - - return sprintf('%.2f %s', $amount, $this->currency); + $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); + $result = $nf->formatCurrency($amount, $this->currency); + // Replace non-breaking space + return str_replace("\xC2\xA0", " ", $result); } /** diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -69,6 +69,8 @@ 'tenant_id' => env('APP_TENANT_ID', null), + 'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')), + /* |-------------------------------------------------------------------------- | Application Domain diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -331,7 +331,7 @@ 'add-penalty' => "Add penalty", 'add-penalty-title' => "Add a penalty to the wallet", 'auto-payment' => "Auto-payment", - 'auto-payment-text' => "Fill up by {amount} CHF when under {balance} CHF using {method}", + 'auto-payment-text' => "Fill up by {amount} when under {balance} using {method}", 'country' => "Country", 'create' => "Create user", 'custno' => "Customer No.", @@ -390,7 +390,7 @@ . " You can cancel or change the auto-payment option at any time.", 'auto-payment-setup' => "Set up auto-payment", 'auto-payment-disabled' => "The configured auto-payment has been disabled. Top up your wallet or raise the auto-payment amount.", - 'auto-payment-info' => "Auto-payment is set to fill up your account by {amount} CHF every time your account balance gets under {balance} CHF.", + 'auto-payment-info' => "Auto-payment is set to fill up your account by {amount} every time your account balance gets under {balance}.", 'auto-payment-inprogress' => "The setup of the automatic payment is still in progress.", 'auto-payment-next' => "Next, you will be redirected to the checkout page, where you can provide your credit card details.", 'auto-payment-disabled-next' => "The auto-payment is disabled. Immediately after you submit new settings we'll enable it and attempt to top up your wallet.", diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -128,7 +128,7 @@
{{ $t('wallet.payment-method', { method: mandate.method }) }}