Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117853824
D4076.1775315265.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
54 KB
Referenced Files
None
Subscribers
None
D4076.1775315265.diff
View Options
diff --git a/src/app/Console/Commands/Data/Import/VatRatesCommand.php b/src/app/Console/Commands/Data/Import/VatRatesCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/VatRatesCommand.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\VatRate;
+use App\Console\Command;
+
+class VatRatesCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:vat-rates {file} {date}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Loads VAT rates from a file';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $file = $this->argument('file');
+ $date = $this->argument('date');
+
+ if (!preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $date)) {
+ $this->error("Invalid start date");
+ return 1;
+ }
+
+ if (!file_exists($file)) {
+ $this->error("Invalid file location");
+ return 1;
+ }
+
+ $rates = json_decode(file_get_contents($file), true);
+
+ if (!is_array($rates) || empty($rates)) {
+ $this->error("Invalid or empty input data format");
+ return 1;
+ }
+
+ $date .= ' 00:00:00';
+
+ foreach ($rates as $country => $rate) {
+ if (is_string($country) && strlen($country)) {
+ if (strlen($country) != 2) {
+ $this->info("Invalid country code: {$country}");
+ continue;
+ }
+
+ if (!is_numeric($rate) || $rate < 0 || $rate > 100) {
+ $this->info("Invalid VAT rate for {$country}: {$rate}");
+ continue;
+ }
+
+ $existing = VatRate::where('country', $country)
+ ->where('start', '<=', $date)
+ ->limit(1)
+ ->first();
+
+ if (!$existing || $existing->rate != $rate) {
+ VatRate::create([
+ 'start' => $date,
+ 'rate' => $rate,
+ 'country' => strtoupper($country),
+ ]);
+
+ $this->info("Added {$country}:{$rate}");
+ continue;
+ }
+ }
+
+ $this->info("Skipped {$country}:{$rate}");
+ }
+ }
+}
diff --git a/src/app/Console/Commands/Scalpel/VatRate/CreateCommand.php b/src/app/Console/Commands/Scalpel/VatRate/CreateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Scalpel/VatRate/CreateCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Console\Commands\Scalpel\VatRate;
+
+use App\Console\ObjectCreateCommand;
+
+class CreateCommand extends ObjectCreateCommand
+{
+ protected $hidden = true;
+
+ protected $commandPrefix = 'scalpel';
+ protected $objectClass = \App\VatRate::class;
+ protected $objectName = 'vat-rate';
+}
diff --git a/src/app/Console/Commands/Scalpel/VatRate/ReadCommand.php b/src/app/Console/Commands/Scalpel/VatRate/ReadCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Scalpel/VatRate/ReadCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Console\Commands\Scalpel\VatRate;
+
+use App\Console\ObjectReadCommand;
+
+class ReadCommand extends ObjectReadCommand
+{
+ protected $hidden = true;
+
+ protected $commandPrefix = 'scalpel';
+ protected $objectClass = \App\VatRate::class;
+ protected $objectName = 'vat-rate';
+}
diff --git a/src/app/Console/Commands/Scalpel/VatRate/UpdateCommand.php b/src/app/Console/Commands/Scalpel/VatRate/UpdateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Scalpel/VatRate/UpdateCommand.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Console\Commands\Scalpel\VatRate;
+
+use App\Console\ObjectUpdateCommand;
+
+class UpdateCommand extends ObjectUpdateCommand
+{
+ protected $hidden = true;
+
+ protected $commandPrefix = 'scalpel';
+ protected $objectClass = \App\VatRate::class;
+ protected $objectName = 'vat-rate';
+}
diff --git a/src/app/Console/Commands/VatRatesCommand.php b/src/app/Console/Commands/VatRatesCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/VatRatesCommand.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Console\ObjectListCommand;
+
+class VatRatesCommand extends ObjectListCommand
+{
+ protected $objectClass = \App\VatRate::class;
+ protected $objectName = 'vat-rate';
+}
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
@@ -128,7 +128,6 @@
$company = $this->companyData();
if (self::$fakeMode) {
- $country = 'CH';
$customer = [
'id' => $this->wallet->owner->id,
'wallet_id' => $this->wallet->id,
@@ -153,10 +152,15 @@
'updated_at' => $start->copy()->next()->next()->next(),
],
]);
+
+ $items = $items->map(function ($payment) {
+ $payment->vatRate = new \App\VatRate();
+ $payment->vatRate->rate = 7.7;
+ $payment->credit_amount = $payment->amount + round($payment->amount * $payment->vatRate->rate / 100);
+ return $payment;
+ });
} else {
$customer = $this->customerData();
- $country = $this->wallet->owner->getSetting('country');
-
$items = $this->wallet->payments()
->where('status', PaymentProvider::STATUS_PAID)
->where('updated_at', '>=', $start)
@@ -166,22 +170,18 @@
->get();
}
- $vatRate = \config('app.vat.rate');
- $vatCountries = explode(',', \config('app.vat.countries'));
- $vatCountries = array_map('strtoupper', array_map('trim', $vatCountries));
-
- if (!$country || !in_array(strtoupper($country), $vatCountries)) {
- $vatRate = 0;
- }
-
+ $vatRate = 0;
$totalVat = 0;
- $total = 0;
- $items = $items->map(function ($item) use (&$total, &$totalVat, $appName, $vatRate) {
+ $total = 0; // excluding VAT
+
+ $items = $items->map(function ($item) use (&$total, &$totalVat, &$vatRate, $appName) {
$amount = $item->amount;
- if ($vatRate > 0) {
- $amount = round($amount * ((100 - $vatRate) / 100));
- $totalVat += $item->amount - $amount;
+ if ($item->vatRate && $item->vatRate->rate > 0) {
+ $vat = round($item->credit_amount * $item->vatRate->rate / 100);
+ $amount -= $vat;
+ $totalVat += $vat;
+ $vatRate = $item->vatRate->rate; // TODO: Multiple rates
}
$total += $amount;
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -80,29 +80,17 @@
}
$amount = (int) ($request->amount * 100);
- $type = $amount > 0 ? Transaction::WALLET_AWARD : Transaction::WALLET_PENALTY;
+ $method = $amount > 0 ? 'award' : 'penalty';
DB::beginTransaction();
- $wallet->balance += $amount;
- $wallet->save();
-
- Transaction::create(
- [
- 'user_email' => \App\Utils::userEmailOrNull(),
- 'object_id' => $wallet->id,
- 'object_type' => Wallet::class,
- 'type' => $type,
- 'amount' => $amount,
- 'description' => $request->description
- ]
- );
+ $wallet->{$method}(abs($amount), $request->description);
if ($user->role == 'reseller') {
if ($user->tenant && ($tenant_wallet = $user->tenant->wallet())) {
$desc = ($amount > 0 ? 'Awarded' : 'Penalized') . " user {$wallet->owner->email}";
- $method = $amount > 0 ? 'debit' : 'credit';
- $tenant_wallet->{$method}(abs($amount), $desc);
+ $tenant_method = $amount > 0 ? 'debit' : 'credit';
+ $tenant_wallet->{$tenant_method}(abs($amount), $desc);
}
}
@@ -110,7 +98,7 @@
$response = [
'status' => 'success',
- 'message' => \trans("app.wallet-{$type}-success"),
+ 'message' => \trans("app.wallet-{$method}-success"),
'balance' => $wallet->balance
];
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
@@ -227,6 +227,8 @@
'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Payment',
];
+ self::addTax($wallet, $request);
+
$provider = PaymentProvider::factory($wallet, $currency);
$result = $provider->payment($wallet, $request);
@@ -340,6 +342,8 @@
'description' => Tenant::getConfig($wallet->owner->tenant_id, 'app.name') . ' Recurring Payment',
];
+ self::addTax($wallet, $request);
+
$result = $provider->payment($wallet, $request);
return !empty($result);
@@ -484,4 +488,30 @@
'page' => $page,
]);
}
+
+ /**
+ * Calculates tax for the payment, fills the request with additional properties
+ */
+ protected static function addTax(Wallet $wallet, array &$request): void
+ {
+ $request['vat_rate_id'] = null;
+ $request['credit_amount'] = $request['amount'];
+
+ if ($rate = $wallet->vatRate()) {
+ $request['vat_rate_id'] = $rate->id;
+
+ switch (\config('app.vat.mode')) {
+ case 1:
+ // In this mode tax is added on top of the payment. The amount
+ // to pay grows, but we keep wallet balance without tax.
+ $request['amount'] = $request['amount'] + round($request['amount'] * $rate->rate / 100);
+ break;
+
+ default:
+ // In this mode tax is "swallowed" by the vendor. The payment
+ // amount does not change
+ break;
+ }
+ }
+ }
}
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -106,87 +106,6 @@
*/
public function deleting(Entitlement $entitlement)
{
- if ($entitlement->trashed()) {
- return;
- }
-
- // Start calculating the costs for the consumption of this entitlement if the
- // existing consumption spans >= 14 days.
- //
- // Effect is that anything's free for the first 14 days
- if ($entitlement->created_at >= Carbon::now()->subDays(14)) {
- return;
- }
-
- $owner = $entitlement->wallet->owner;
-
- if ($owner->isDegraded()) {
- return;
- }
-
- $now = Carbon::now();
-
- // Determine if we're still within the trial period
- $trial = $entitlement->wallet->trialInfo();
- if (
- !empty($trial)
- && $entitlement->updated_at < $trial['end']
- && in_array($entitlement->sku_id, $trial['skus'])
- ) {
- if ($trial['end'] >= $now) {
- return;
- }
-
- $entitlement->updated_at = $trial['end'];
- }
-
- // get the discount rate applied to the wallet.
- $discount = $entitlement->wallet->getDiscountRate();
-
- // just in case this had not been billed yet, ever
- $diffInMonths = $entitlement->updated_at->diffInMonths($now);
- $cost = (int) ($entitlement->cost * $discount * $diffInMonths);
- $fee = (int) ($entitlement->fee * $diffInMonths);
-
- // this moves the hypothetical updated at forward to however many months past the original
- $updatedAt = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diffInMonths);
-
- // now we have the diff in days since the last "billed" period end.
- // This may be an entitlement paid up until February 28th, 2020, with today being March
- // 12th 2020. Calculating the costs for the entitlement is based on the daily price
-
- // the price per day is based on the number of days in the last month
- // or the current month if the period does not overlap with the previous month
- // FIXME: This really should be simplified to $daysInMonth=30
-
- $diffInDays = $updatedAt->diffInDays($now);
-
- if ($now->day >= $diffInDays) {
- $daysInMonth = $now->daysInMonth;
- } else {
- $daysInMonth = \App\Utils::daysInLastMonth();
- }
-
- $pricePerDay = $entitlement->cost / $daysInMonth;
- $feePerDay = $entitlement->fee / $daysInMonth;
-
- $cost += (int) (round($pricePerDay * $discount * $diffInDays, 0));
- $fee += (int) (round($feePerDay * $diffInDays, 0));
-
- $profit = $cost - $fee;
-
- if ($profit != 0 && $owner->tenant && ($wallet = $owner->tenant->wallet())) {
- $desc = "Charged user {$owner->email}";
- $method = $profit > 0 ? 'credit' : 'debit';
- $wallet->{$method}(abs($profit), $desc);
- }
-
- if ($cost == 0) {
- return;
- }
-
- // FIXME: Shouldn't we create per-entitlement transaction record?
-
- $entitlement->wallet->debit($cost);
+ $entitlement->wallet->chargeEntitlement($entitlement);
}
}
diff --git a/src/app/Package.php b/src/app/Package.php
--- a/src/app/Package.php
+++ b/src/app/Package.php
@@ -37,6 +37,7 @@
use HasTranslations;
use UuidStrKeyTrait;
+ /** @var bool Indicates if the model should be timestamped. */
public $timestamps = false;
/** @var array<int, string> The attributes that are mass assignable */
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -2,27 +2,36 @@
namespace App;
+use Dyrynda\Database\Support\NullableFields;
use Illuminate\Database\Eloquent\Model;
/**
* A payment operation on a wallet.
*
- * @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
- * @property string $wallet_id The ID of the wallet
- * @property string $currency Currency of this payment
+ * @property int $amount Amount of money in cents of system currency (payment provider)
+ * @property int $credit_amount Amount of money in cents of system currency (wallet balance)
+ * @property string $description Payment description
+ * @property string $id Mollie's Payment ID
+ * @property ?string $vat_rate_id VAT rate identifier
+ * @property \App\Wallet $wallet The wallet
+ * @property string $wallet_id The ID of the wallet
+ * @property string $currency Currency of this payment
* @property int $currency_amount Amount of money in cents of $currency
*/
class Payment extends Model
{
+ use NullableFields;
+
+ /** @var bool Indicates that the model should be timestamped or not */
public $incrementing = false;
+
+ /** @var string The "type" of the auto-incrementing ID */
protected $keyType = 'string';
/** @var array<string, string> The attributes that should be cast */
protected $casts = [
- 'amount' => 'integer'
+ 'amount' => 'integer',
+ 'credit_amount' => 'integer',
];
/** @var array<int,string> The attributes that are mass assignable */
@@ -30,14 +39,21 @@
'id',
'wallet_id',
'amount',
+ 'credit_amount',
'description',
'provider',
'status',
+ 'vat_rate_id',
'type',
'currency',
'currency_amount',
];
+ /** @var array<int, string> The attributes that can be not set */
+ protected $nullable = [
+ 'vat_rate_id',
+ ];
+
/**
* Ensure the currency is appropriately cased.
@@ -56,4 +72,14 @@
{
return $this->belongsTo(Wallet::class, 'wallet_id', 'id');
}
+
+ /**
+ * The VAT rate assigned to this payment.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function vatRate()
+ {
+ return $this->belongsTo(VatRate::class, 'vat_rate_id', 'id');
+ }
}
diff --git a/src/app/Providers/Payment/Coinbase.php b/src/app/Providers/Payment/Coinbase.php
--- a/src/app/Providers/Payment/Coinbase.php
+++ b/src/app/Providers/Payment/Coinbase.php
@@ -352,7 +352,7 @@
$description = 'Payment';
$description .= " transaction {$payment->id} using Coinbase";
- $payment->wallet->credit($payment->amount, $description);
+ $payment->wallet->credit($payment, $description);
}
/**
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
@@ -517,7 +517,7 @@
$description = $payment->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment';
$description .= " transaction {$payment->id} using {$method}";
- $payment->wallet->credit($payment->amount, $description);
+ $payment->wallet->credit($payment, $description);
// Unlock the disabled auto-payment mandate
if ($payment->wallet->balance >= 0) {
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
@@ -454,7 +454,7 @@
$description = $payment->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment';
$description .= " transaction {$payment->id} using {$method}";
- $payment->wallet->credit($payment->amount, $description);
+ $payment->wallet->credit($payment, $description);
// Unlock the disabled auto-payment mandate
if ($payment->wallet->balance >= 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
@@ -101,6 +101,8 @@
* @param \App\Wallet $wallet The wallet
* @param array $payment Payment data:
* - amount: Value in cents
+ * - credit_amount: Balance'able base amount in cents
+ * - vat_rate_id: VAT rate id
* - currency: The operation currency
* - description: Operation desc.
* - methodId: Payment method
@@ -156,6 +158,8 @@
* @param \App\Wallet $wallet The wallet
* @param array $payment Payment data:
* - amount: Value in cents
+ * - credit_amount: Balance'able base amount in cents
+ * - vat_rate_id: Vat rate id
* - currency: The operation currency
* - type: first/oneoff/recurring
* - description: Operation description
@@ -189,6 +193,8 @@
$db_payment->description = $payment['description'] ?? '';
$db_payment->status = $payment['status'] ?? self::STATUS_OPEN;
$db_payment->amount = $payment['amount'] ?? 0;
+ $db_payment->credit_amount = $payment['credit_amount'] ?? ($payment['amount'] ?? 0);
+ $db_payment->vat_rate_id = $payment['vat_rate_id'] ?? null;
$db_payment->type = $payment['type'];
$db_payment->wallet_id = $wallet_id;
$db_payment->provider = $this->name();
@@ -235,22 +241,11 @@
// TODO We should possibly be using the same exchange rate as for the original payment?
$amount = $this->exchange($refund['amount'], $refund['currency'], $wallet->currency);
- $wallet->balance -= $amount;
- $wallet->save();
+ // TODO: Set vat_rate_id and credit_amount
- if ($refund['type'] == self::TYPE_CHARGEBACK) {
- $transaction_type = Transaction::WALLET_CHARGEBACK;
- } else {
- $transaction_type = Transaction::WALLET_REFUND;
- }
+ $method = $refund['type'] == self::TYPE_CHARGEBACK ? 'chargeback' : 'refund';
- Transaction::create([
- 'object_id' => $wallet->id,
- 'object_type' => Wallet::class,
- 'type' => $transaction_type,
- 'amount' => $amount * -1,
- 'description' => $refund['description'] ?? '',
- ]);
+ $wallet->{$method}($amount, $refund['description'] ?? '');
$refund['status'] = self::STATUS_PAID;
$refund['amount'] = -1 * $amount;
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -787,7 +787,7 @@
// Check 2FA - Companion App
if (!$error && $user->mfaEnabled()) {
- $attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP);
+ $attempt = AuthAttempt::recordAuthAttempt($user, $clientIP);
if (!$attempt->waitFor2FA()) {
$error = AuthAttempt::REASON_2FA;
}
@@ -796,7 +796,7 @@
if ($error) {
if ($user && empty($attempt)) {
- $attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP);
+ $attempt = AuthAttempt::recordAuthAttempt($user, $clientIP);
if (!$attempt->isAccepted()) {
$attempt->deny($error);
$attempt->save();
diff --git a/src/app/VatRate.php b/src/app/VatRate.php
new file mode 100644
--- /dev/null
+++ b/src/app/VatRate.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a Vat Rate.
+ *
+ * @property string $id Rate identifier (uuid)
+ * @property string $country Two-letter country code
+ * @property float $rate Tax rate
+ * @property string $start Start date of the rate
+ */
+class VatRate extends Model
+{
+ use UuidStrKeyTrait;
+
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'start' => 'datetime:Y-m-d H:i:s',
+ 'rate' => 'float'
+ ];
+
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'country',
+ 'rate',
+ 'start',
+ ];
+
+ /** @var bool Indicates if the model should be timestamped. */
+ public $timestamps = false;
+}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -66,6 +66,109 @@
}
}
+ /**
+ * Add an award to this wallet's balance.
+ *
+ * @param int|\App\Payment $amount The amount of award (in cents) or Payment object
+ * @param string $description The transaction description
+ *
+ * @return Wallet Self
+ */
+ public function award(int|Payment $amount, string $description = ''): Wallet
+ {
+ return $this->balanceUpdate(Transaction::WALLET_AWARD, $amount, $description);
+ }
+
+ /**
+ * Charge a specific entitlement (for use on entitlement delete).
+ *
+ * @param \App\Entitlement $entitlement The entitlement.
+ */
+ public function chargeEntitlement(Entitlement $entitlement): void
+ {
+ // Sanity checks
+ if ($entitlement->trashed() || $entitlement->wallet->id != $this->id || !$this->owner) {
+ return;
+ }
+
+ // Start calculating the costs for the consumption of this entitlement if the
+ // existing consumption spans >= 14 days.
+ //
+ // Effect is that anything's free for the first 14 days
+ if ($entitlement->created_at >= Carbon::now()->subDays(14)) {
+ return;
+ }
+
+ if ($this->owner->isDegraded()) {
+ return;
+ }
+
+ $now = Carbon::now();
+
+ // Determine if we're still within the trial period
+ $trial = $this->trialInfo();
+ if (
+ !empty($trial)
+ && $entitlement->updated_at < $trial['end']
+ && in_array($entitlement->sku_id, $trial['skus'])
+ ) {
+ if ($trial['end'] >= $now) {
+ return;
+ }
+
+ $entitlement->updated_at = $trial['end'];
+ }
+
+ // get the discount rate applied to the wallet.
+ $discount = $this->getDiscountRate();
+
+ // just in case this had not been billed yet, ever
+ $diffInMonths = $entitlement->updated_at->diffInMonths($now);
+ $cost = (int) ($entitlement->cost * $discount * $diffInMonths);
+ $fee = (int) ($entitlement->fee * $diffInMonths);
+
+ // this moves the hypothetical updated at forward to however many months past the original
+ $updatedAt = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diffInMonths);
+
+ // now we have the diff in days since the last "billed" period end.
+ // This may be an entitlement paid up until February 28th, 2020, with today being March
+ // 12th 2020. Calculating the costs for the entitlement is based on the daily price
+
+ // the price per day is based on the number of days in the last month
+ // or the current month if the period does not overlap with the previous month
+ // FIXME: This really should be simplified to $daysInMonth=30
+
+ $diffInDays = $updatedAt->diffInDays($now);
+
+ if ($now->day >= $diffInDays) {
+ $daysInMonth = $now->daysInMonth;
+ } else {
+ $daysInMonth = \App\Utils::daysInLastMonth();
+ }
+
+ $pricePerDay = $entitlement->cost / $daysInMonth;
+ $feePerDay = $entitlement->fee / $daysInMonth;
+
+ $cost += (int) (round($pricePerDay * $discount * $diffInDays, 0));
+ $fee += (int) (round($feePerDay * $diffInDays, 0));
+
+ $profit = $cost - $fee;
+
+ if ($profit != 0 && $this->owner->tenant && ($wallet = $this->owner->tenant->wallet())) {
+ $desc = "Charged user {$this->owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
+
+ if ($cost == 0) {
+ return;
+ }
+
+ // TODO: Create per-entitlement transaction record?
+
+ $this->debit($cost);
+ }
+
/**
* Charge entitlements in the wallet
*
@@ -229,6 +332,19 @@
return $until;
}
+ /**
+ * Chargeback an amount of pecunia from this wallet's balance.
+ *
+ * @param int|\App\Payment $amount The amount of pecunia to charge back (in cents) or Payment object
+ * @param string $description The transaction description
+ *
+ * @return Wallet Self
+ */
+ public function chargeback(int|Payment $amount, string $description = ''): Wallet
+ {
+ return $this->balanceUpdate(Transaction::WALLET_CHARGEBACK, $amount, $description);
+ }
+
/**
* Controllers of this wallet.
*
@@ -247,64 +363,28 @@
/**
* Add an amount of pecunia to this wallet's balance.
*
- * @param int $amount The amount of pecunia to add (in cents).
- * @param string $description The transaction description
+ * @param int|\App\Payment $amount The amount of pecunia to add (in cents) or Payment object
+ * @param string $description The transaction description
*
* @return Wallet Self
*/
- public function credit(int $amount, string $description = ''): Wallet
+ public function credit(int|Payment $amount, string $description = ''): Wallet
{
- $this->balance += $amount;
-
- $this->save();
-
- Transaction::create(
- [
- 'object_id' => $this->id,
- 'object_type' => Wallet::class,
- 'type' => Transaction::WALLET_CREDIT,
- 'amount' => $amount,
- 'description' => $description
- ]
- );
-
- return $this;
+ return $this->balanceUpdate(Transaction::WALLET_CREDIT, $amount, $description);
}
/**
* Deduct an amount of pecunia from this wallet's balance.
*
- * @param int $amount The amount of pecunia to deduct (in cents).
- * @param string $description The transaction description
- * @param array $eTIDs List of transaction IDs for the individual entitlements
- * that make up this debit record, if any.
+ * @param int|\App\Payment $amount The amount of pecunia to deduct (in cents) or Payment object
+ * @param string $description The transaction description
+ * @param array $eTIDs List of transaction IDs for the individual entitlements
+ * that make up this debit record, if any.
* @return Wallet Self
*/
- public function debit(int $amount, string $description = '', array $eTIDs = []): Wallet
+ public function debit(int|Payment $amount, string $description = '', array $eTIDs = []): Wallet
{
- if ($amount == 0) {
- return $this;
- }
-
- $this->balance -= $amount;
-
- $this->save();
-
- $transaction = Transaction::create(
- [
- 'object_id' => $this->id,
- 'object_type' => Wallet::class,
- 'type' => Transaction::WALLET_DEBIT,
- 'amount' => $amount * -1,
- 'description' => $description
- ]
- );
-
- if (!empty($eTIDs)) {
- Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
- }
-
- return $this;
+ return $this->balanceUpdate(Transaction::WALLET_DEBIT, $amount, $description, $eTIDs);
}
/**
@@ -410,6 +490,19 @@
return $this->hasMany(Payment::class);
}
+ /**
+ * Add a penalty to this wallet's balance.
+ *
+ * @param int|\App\Payment $amount The amount of penalty (in cents) or Payment object
+ * @param string $description The transaction description
+ *
+ * @return Wallet Self
+ */
+ public function penalty(int|Payment $amount, string $description = ''): Wallet
+ {
+ return $this->balanceUpdate(Transaction::WALLET_PENALTY, $amount, $description);
+ }
+
/**
* Plan of the wallet.
*
@@ -436,6 +529,49 @@
}
}
+ /**
+ * Refund an amount of pecunia from this wallet's balance.
+ *
+ * @param int|\App\Payment $amount The amount of pecunia to refund (in cents) or Payment object
+ * @param string $description The transaction description
+ *
+ * @return Wallet Self
+ */
+ public function refund($amount, string $description = ''): Wallet
+ {
+ return $this->balanceUpdate(Transaction::WALLET_REFUND, $amount, $description);
+ }
+
+ /**
+ * Get the VAT rate for the wallet owner country.
+ *
+ * @param ?\DateTime $start Get the rate valid for the specified date-time,
+ * without it the current rate will be returned (if exists).
+ *
+ * @return ?\App\VatRate VAT rate
+ */
+ public function vatRate(\DateTime $start = null): ?VatRate
+ {
+ $owner = $this->owner;
+
+ // Make it working with deleted accounts too
+ if (!$owner) {
+ $owner = $this->owner()->withTrashed()->first();
+ }
+
+ $country = $owner->getSetting('country');
+
+ if (!$country) {
+ return null;
+ }
+
+ return VatRate::where('country', $country)
+ ->where('start', '<=', ($start ?: now())->format('Y-m-d h:i:s'))
+ ->orderByDesc('start')
+ ->limit(1)
+ ->first();
+ }
+
/**
* Retrieve the transactions against this wallet.
*
@@ -556,4 +692,42 @@
return $charges;
}
+
+ /**
+ * Update the wallet balance, and create a transaction record
+ */
+ protected function balanceUpdate(string $type, int|Payment $amount, $description = null, array $eTIDs = [])
+ {
+ if ($amount instanceof Payment) {
+ $amount = $amount->credit_amount;
+ }
+
+ if ($amount === 0) {
+ return $this;
+ }
+
+ if (in_array($type, [Transaction::WALLET_CREDIT, Transaction::WALLET_AWARD])) {
+ $amount = abs($amount);
+ } else {
+ $amount = abs($amount) * -1;
+ }
+
+ $this->balance += $amount;
+ $this->save();
+
+ $transaction = Transaction::create([
+ 'user_email' => \App\Utils::userEmailOrNull(),
+ 'object_id' => $this->id,
+ 'object_type' => Wallet::class,
+ 'type' => $type,
+ 'amount' => $amount,
+ 'description' => $description,
+ ]);
+
+ if (!empty($eTIDs)) {
+ Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]);
+ }
+
+ return $this;
+ }
}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -247,8 +247,7 @@
],
'vat' => [
- 'countries' => env('VAT_COUNTRIES'),
- 'rate' => (float) env('VAT_RATE'),
+ 'mode' => (int) env('VAT_MODE', 0),
],
'password_policy' => env('PASSWORD_POLICY') ?: 'min:6,max:255',
diff --git a/src/database/migrations/2023_02_17_100000_vat_rates_table.php b/src/database/migrations/2023_02_17_100000_vat_rates_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2023_02_17_100000_vat_rates_table.php
@@ -0,0 +1,88 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('vat_rates', function (Blueprint $table) {
+ $table->string('id', 36)->primary();
+ $table->string('country', 2);
+ $table->timestamp('start')->useCurrent();
+ $table->double('rate', 5, 2);
+
+ $table->unique(['country', 'start']);
+ });
+
+ Schema::table(
+ 'payments',
+ function (Blueprint $table) {
+ $table->string('vat_rate_id', 36)->nullable();
+ $table->integer('credit_amount')->nullable(); // temporarily allow null
+
+ $table->foreign('vat_rate_id')->references('id')->on('vat_rates')->onUpdate('cascade');
+ }
+ );
+
+ DB::table('payments')->update(['credit_amount' => DB::raw("`amount`")]);
+
+ Schema::table(
+ 'payments',
+ function (Blueprint $table) {
+ $table->integer('credit_amount')->nullable(false)->change(); // remove nullable
+ }
+ );
+
+ // Migrate old tax rates (and existing payments)
+ if (($countries = \env('VAT_COUNTRIES')) && ($rate = \env('VAT_RATE'))) {
+ $countries = explode(',', strtoupper(trim($countries)));
+
+ foreach ($countries as $country) {
+ $vatRate = \App\VatRate::create([
+ 'start' => new DateTime('2010-01-01 00:00:00'),
+ 'rate' => $rate,
+ 'country' => $country,
+ ]);
+
+ DB::table('payments')->whereIn('wallet_id', function ($query) use ($country) {
+ $query->select('id')
+ ->from('wallets')
+ ->whereIn('user_id', function ($query) use ($country) {
+ $query->select('user_id')
+ ->from('user_settings')
+ ->where('key', 'country')
+ ->where('value', $country);
+ });
+ })
+ ->update(['vat_rate_id' => $vatRate->id]);
+ }
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table(
+ 'payments',
+ function (Blueprint $table) {
+ $table->dropColumn('vat_rate_id');
+ $table->dropColumn('credit_amount');
+ }
+ );
+
+ Schema::dropIfExists('vat_rates');
+ }
+};
diff --git a/src/tests/Browser/Reseller/WalletTest.php b/src/tests/Browser/Reseller/WalletTest.php
--- a/src/tests/Browser/Reseller/WalletTest.php
+++ b/src/tests/Browser/Reseller/WalletTest.php
@@ -138,6 +138,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
]);
@@ -154,6 +155,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
]);
diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php
--- a/src/tests/Browser/WalletTest.php
+++ b/src/tests/Browser/WalletTest.php
@@ -131,6 +131,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
]);
@@ -147,6 +148,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
]);
diff --git a/src/tests/Feature/Console/Data/Stats/CollectorTest.php b/src/tests/Feature/Console/Data/Stats/CollectorTest.php
--- a/src/tests/Feature/Console/Data/Stats/CollectorTest.php
+++ b/src/tests/Feature/Console/Data/Stats/CollectorTest.php
@@ -53,6 +53,7 @@
'description' => '',
'status' => PaymentProvider::STATUS_PAID,
'amount' => 1000,
+ 'credit_amount' => 1000,
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $wallet->id,
'provider' => 'mollie',
diff --git a/src/tests/Feature/Controller/Admin/StatsTest.php b/src/tests/Feature/Controller/Admin/StatsTest.php
--- a/src/tests/Feature/Controller/Admin/StatsTest.php
+++ b/src/tests/Feature/Controller/Admin/StatsTest.php
@@ -18,7 +18,7 @@
parent::setUp();
self::useAdminUrl();
- Payment::truncate();
+ Payment::query()->delete();
DB::table('wallets')->update(['discount_id' => null]);
$this->deleteTestUser('test-stats@' . \config('app.domain'));
@@ -29,7 +29,7 @@
*/
public function tearDown(): void
{
- Payment::truncate();
+ Payment::query()->delete();
DB::table('wallets')->update(['discount_id' => null]);
$this->deleteTestUser('test-stats@' . \config('app.domain'));
@@ -133,7 +133,8 @@
'id' => 'test1',
'description' => '',
'status' => PaymentProvider::STATUS_PAID,
- 'amount' => 1000, // EUR
+ 'amount' => 1000,
+ 'credit_amount' => 1000,
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $wallet->id,
'provider' => 'mollie',
@@ -144,7 +145,8 @@
'id' => 'test2',
'description' => '',
'status' => PaymentProvider::STATUS_PAID,
- 'amount' => 2000, // EUR
+ 'amount' => 2000,
+ 'credit_amount' => 2000,
'type' => PaymentProvider::TYPE_RECURRING,
'wallet_id' => $wallet->id,
'provider' => 'mollie',
@@ -155,7 +157,8 @@
'id' => 'test3',
'description' => '',
'status' => PaymentProvider::STATUS_PAID,
- 'amount' => 3000, // CHF
+ 'amount' => 3000,
+ 'credit_amount' => 3000,
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $johns_wallet->id,
'provider' => 'mollie',
@@ -166,7 +169,8 @@
'id' => 'test4',
'description' => '',
'status' => PaymentProvider::STATUS_PAID,
- 'amount' => 4000, // CHF
+ 'amount' => 4000,
+ 'credit_amount' => 4000,
'type' => PaymentProvider::TYPE_RECURRING,
'wallet_id' => $johns_wallet->id,
'provider' => 'mollie',
@@ -177,7 +181,8 @@
'id' => 'test5',
'description' => '',
'status' => PaymentProvider::STATUS_OPEN,
- 'amount' => 5000, // CHF
+ 'amount' => 5000,
+ 'credit_amount' => 5000,
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $johns_wallet->id,
'provider' => 'mollie',
@@ -188,7 +193,8 @@
'id' => 'test6',
'description' => '',
'status' => PaymentProvider::STATUS_FAILED,
- 'amount' => 6000, // CHF
+ 'amount' => 6000,
+ 'credit_amount' => 6000,
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $johns_wallet->id,
'provider' => 'mollie',
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
--- a/src/tests/Feature/Controller/Admin/UsersTest.php
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -24,6 +24,9 @@
$jack = $this->getTestUser('jack@kolab.org');
$jack->setSetting('external_email', null);
+
+ \App\SharedFolderAlias::truncate();
+ \App\Payment::query()->delete();
}
/**
@@ -39,6 +42,7 @@
$jack->setSetting('external_email', null);
\App\SharedFolderAlias::truncate();
+ \App\Payment::query()->delete();
parent::tearDown();
}
@@ -236,6 +240,7 @@
'wallet_id' => $wallet->id,
'status' => 'paid',
'amount' => 1337,
+ 'credit_amount' => 1337,
'description' => 'nonsense transaction for testing',
'provider' => 'self',
'type' => 'oneoff',
diff --git a/src/tests/Feature/Controller/PaymentsMollieEuroTest.php b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
@@ -657,6 +657,7 @@
'id' => 'tr_123456',
'status' => PaymentProvider::STATUS_PAID,
'amount' => 123,
+ 'credit_amount' => 123,
'currency_amount' => 123,
'currency' => 'EUR',
'type' => PaymentProvider::TYPE_ONEOFF,
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
@@ -716,6 +716,7 @@
'id' => 'tr_123456',
'status' => PaymentProvider::STATUS_PAID,
'amount' => 123,
+ 'credit_amount' => 123,
'currency_amount' => 123,
'currency' => 'CHF',
'type' => PaymentProvider::TYPE_ONEOFF,
@@ -874,6 +875,7 @@
'id' => 'tr_123456',
'status' => PaymentProvider::STATUS_PAID,
'amount' => 1234,
+ 'credit_amount' => 1234,
'currency_amount' => 1117,
'currency' => 'EUR',
'type' => PaymentProvider::TYPE_ONEOFF,
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
@@ -180,6 +180,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency' => 'CHF',
'currency_amount' => 1111,
]);
diff --git a/src/tests/Feature/Documents/ReceiptTest.php b/src/tests/Feature/Documents/ReceiptTest.php
--- a/src/tests/Feature/Documents/ReceiptTest.php
+++ b/src/tests/Feature/Documents/ReceiptTest.php
@@ -7,19 +7,19 @@
use App\Providers\PaymentProvider;
use App\User;
use App\Wallet;
+use App\VatRate;
use Carbon\Carbon;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class ReceiptTest extends TestCase
{
- private $paymentIDs = ['AAA1', 'AAA2', 'AAA3', 'AAA4', 'AAA5', 'AAA6', 'AAA7'];
-
public function setUp(): void
{
parent::setUp();
- Payment::whereIn('id', $this->paymentIDs)->delete();
+ Payment::query()->delete();
+ VatRate::query()->delete();
}
/**
@@ -29,7 +29,8 @@
{
$this->deleteTestUser('receipt-test@kolabnow.com');
- Payment::whereIn('id', $this->paymentIDs)->delete();
+ Payment::query()->delete();
+ VatRate::query()->delete();
parent::tearDown();
}
@@ -121,9 +122,6 @@
*/
public function testHtmlOutputVat(): void
{
- \config(['app.vat.rate' => 7.7]);
- \config(['app.vat.countries' => 'ch']);
-
$appName = \config('app.name');
$wallet = $this->getTestData('CH');
$receipt = new Receipt($wallet, 2020, 5);
@@ -214,6 +212,15 @@
$wallet = $user->wallets()->first();
+ $vat = null;
+ if ($country) {
+ $vat = VatRate::create([
+ 'country' => $country,
+ 'rate' => 7.7,
+ 'start' => now(),
+ ])->id;
+ }
+
// Create two payments out of the 2020-05 period
// and three in it, plus one in the period but unpaid,
// and one with amount 0, and an extra refund and chanrgeback
@@ -226,6 +233,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 1111,
]);
@@ -240,6 +249,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 2222,
+ 'credit_amount' => 2222,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 2222,
]);
@@ -254,6 +265,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 0,
+ 'credit_amount' => 0,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 0,
]);
@@ -268,6 +281,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 990,
+ 'credit_amount' => 990,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 990,
]);
@@ -283,6 +298,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1234,
+ 'credit_amount' => 1234,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 1234,
]);
@@ -297,6 +314,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1,
+ 'credit_amount' => 1,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 1,
]);
@@ -311,6 +330,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 100,
+ 'credit_amount' => 100,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => 100,
]);
@@ -325,6 +346,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => -100,
+ 'credit_amount' => -100,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => -100,
]);
@@ -339,6 +362,8 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => -10,
+ 'credit_amount' => -10,
+ 'vat_rate_id' => $vat,
'currency' => 'CHF',
'currency_amount' => -10,
]);
diff --git a/src/tests/Feature/Jobs/PaymentEmailTest.php b/src/tests/Feature/Jobs/PaymentEmailTest.php
--- a/src/tests/Feature/Jobs/PaymentEmailTest.php
+++ b/src/tests/Feature/Jobs/PaymentEmailTest.php
@@ -53,6 +53,7 @@
$payment->id = 'test-payment';
$payment->wallet_id = $wallet->id;
$payment->amount = 100;
+ $payment->credit_amount = 100;
$payment->currency_amount = 100;
$payment->currency = 'CHF';
$payment->status = PaymentProvider::STATUS_PAID;
diff --git a/src/tests/Feature/Stories/RateLimitTest.php b/src/tests/Feature/Stories/RateLimitTest.php
--- a/src/tests/Feature/Stories/RateLimitTest.php
+++ b/src/tests/Feature/Stories/RateLimitTest.php
@@ -19,10 +19,14 @@
$this->setUpTest();
$this->useServicesUrl();
+
+ \App\Payment::query()->delete();
}
public function tearDown(): void
{
+ \App\Payment::query()->delete();
+
parent::tearDown();
}
@@ -169,6 +173,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
];
@@ -256,6 +261,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
];
@@ -399,6 +405,7 @@
'wallet_id' => $wallet->id,
'provider' => 'stripe',
'amount' => 1111,
+ 'credit_amount' => 1111,
'currency_amount' => 1111,
'currency' => 'CHF',
];
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -2,12 +2,14 @@
namespace Tests\Feature;
+use App\Payment;
use App\Package;
use App\Plan;
use App\User;
use App\Sku;
use App\Transaction;
use App\Wallet;
+use App\VatRate;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
@@ -41,6 +43,8 @@
}
Sku::select()->update(['fee' => 0]);
+ Payment::query()->delete();
+ VatRate::query()->delete();
}
/**
@@ -53,6 +57,8 @@
}
Sku::select()->update(['fee' => 0]);
+ Payment::query()->delete();
+ VatRate::query()->delete();
parent::tearDown();
}
@@ -557,6 +563,30 @@
*/
}
+ /**
+ * Tests for award() and penalty()
+ */
+ public function testAwardAndPenalty(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Tests for chargeback() and refund()
+ */
+ public function testChargebackAndRefund(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Tests for chargeEntitlement()
+ */
+ public function testChargeEntitlement(): void
+ {
+ $this->markTestIncomplete();
+ }
+
/**
* Tests for updateEntitlements()
*/
@@ -564,4 +594,47 @@
{
$this->markTestIncomplete();
}
+
+ /**
+ * Tests for vatRate()
+ */
+ public function testVatRate(): void
+ {
+ $rate1 = VatRate::create([
+ 'start' => now()->subDay(),
+ 'country' => 'US',
+ 'rate' => 7.5,
+ ]);
+ $rate2 = VatRate::create([
+ 'start' => now()->subDay(),
+ 'country' => 'DE',
+ 'rate' => 10.0,
+ ]);
+
+ $user = $this->getTestUser('UserWallet1@UserWallet.com');
+ $wallet = $user->wallets()->first();
+
+ $user->setSetting('country', null);
+ $this->assertSame(null, $wallet->vatRate());
+
+ $user->setSetting('country', 'PL');
+ $this->assertSame(null, $wallet->vatRate());
+
+ $user->setSetting('country', 'US');
+ $this->assertSame($rate1->id, $wallet->vatRate()->id); // @phpstan-ignore-line
+
+ $user->setSetting('country', 'DE');
+ $this->assertSame($rate2->id, $wallet->vatRate()->id); // @phpstan-ignore-line
+
+ // Test $start argument
+ $rate3 = VatRate::create([
+ 'start' => now()->subYear(),
+ 'country' => 'DE',
+ 'rate' => 5.0,
+ ]);
+
+ $this->assertSame($rate2->id, $wallet->vatRate()->id); // @phpstan-ignore-line
+ $this->assertSame($rate3->id, $wallet->vatRate(now()->subMonth())->id);
+ $this->assertSame(null, $wallet->vatRate(now()->subYears(2)));
+ }
}
diff --git a/src/tests/Unit/Backends/DAV/VcardTest.php b/src/tests/Unit/Backends/DAV/VcardTest.php
--- a/src/tests/Unit/Backends/DAV/VcardTest.php
+++ b/src/tests/Unit/Backends/DAV/VcardTest.php
@@ -27,7 +27,7 @@
N:;;;;
UID:$uid
END:VCARD
-]]></c:calendar-data>
+]]></c:address-data>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 3:07 PM (7 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830097
Default Alt Text
D4076.1775315265.diff (54 KB)
Attached To
Mode
D4076: VAT modes
Attached
Detach File
Event Timeline