Page MenuHomePhorge

D4076.1775315265.diff
No OneTemporary

Authored By
Unknown
Size
54 KB
Referenced Files
None
Subscribers
None

D4076.1775315265.diff

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

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)

Event Timeline