Напиши на vue3 компонент для использования этого сервиса -  Вот полный рабочий код, реализованный на Laravel, который включает модели, миграции, сервис для биллинга и команду для ежедневного списания.

1. Миграции и модели

Создадим необходимые таблицы и модели: Client, Subscription и Transaction.

Миграция и модель для клиентов (Client):


// database/migrations/XXXX_XX_XX_create_clients_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateClientsTable extends Migration
{
    public function up()
    {
        Schema::create('clients', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id'); // Ссылка на пользователя
            $table->string('name');
            $table->decimal('balance', 15, 2)->default(0);
            $table->date('trial_ends_at')->nullable();
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    public function down()
    {
        Schema::dropIfExists('clients');
    }
}

// app/Models/Client.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Client extends Model
{
    protected $fillable = ['user_id', 'name', 'balance', 'trial_ends_at'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function subscriptions()
    {
        return $this->hasMany(Subscription::class);
    }

    public function currentSubscription()
    {
        return $this->hasOne(Subscription::class)->where('status', 'active');
    }

    public function transactions()
    {
        return $this->hasMany(Transaction::class);
    }
}
Миграция и модель для подписок (Subscription):


// database/migrations/XXXX_XX_XX_create_subscriptions_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateSubscriptionsTable extends Migration
{
    public function up()
    {
        Schema::create('subscriptions', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('client_id');
            $table->date('start_date');
            $table->date('end_date')->nullable();
            $table->integer('number_of_users')->default(1);
            $table->integer('disk_space')->default(10);
            $table->decimal('base_price', 15, 2);
            $table->decimal('extra_users_price', 15, 2)->default(0);
            $table->decimal('extra_disk_space_price', 15, 2)->default(0);
            $table->decimal('discount_percentage', 5,2)->default(0);
            $table->decimal('total_price', 15,2);
            $table->string('status')->default('active');
            $table->timestamps();

            $table->foreign('client_id')->references('id')->on('clients');
        });
    }

    public function down()
    {
        Schema::dropIfExists('subscriptions');
    }
}

// app/Models/Subscription.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Subscription extends Model
{
    protected $fillable = [
        'client_id', 'start_date', 'end_date', 'number_of_users', 'disk_space',
        'base_price', 'extra_users_price', 'extra_disk_space_price', 'discount_percentage',
        'total_price', 'status'
    ];

    public function client()
    {
        return $this->belongsTo(Client::class);
    }
}
Миграция и модель для транзакций (Transaction):


// database/migrations/XXXX_XX_XX_create_transactions_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTransactionsTable extends Migration
{
    public function up()
    {
        Schema::create('transactions', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('client_id');
            $table->decimal('amount', 15,2);
            $table->enum('transaction_type', ['payment', 'deduction']);
            $table->text('description')->nullable();
            $table->decimal('balance_after_transaction', 15,2)->default(0);
            $table->timestamps();

            $table->foreign('client_id')->references('id')->on('clients');
        });
    }

    public function down()
    {
        Schema::dropIfExists('transactions');
    }
}

// app/Models/Transaction.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
    protected $fillable = ['client_id', 'amount', 'transaction_type', 'description', 'balance_after_transaction'];

    public function client()
    {
        return $this->belongsTo(Client::class);
    }
}
2. Сервис для биллинга

Создадим BillingService, который будет обрабатывать всю логику расчётов.


// app/Services/BillingService.php

namespace App\Services;

use App\Models\Client;
use App\Models\Subscription;
use App\Models\Transaction;
use Carbon\Carbon;

class BillingService
{
    const BASE_PRICE = 29500;
    const BASE_USERS = 1;
    const BASE_DISK_SPACE = 10; // GB
    const EXTRA_USER_PRICE = 1000;
    const EXTRA_DISK_SPACE_PRICE = 150;

    protected $discounts = [
        3 => 0.02,
        6 => 0.05,
        12 => 0.07,
    ];

    public function createSubscription(Client $client, $numberOfMonths = 1)
    {
        $startDate = Carbon::now();
        $endDate = $startDate->copy()->addMonths($numberOfMonths);

        $subscription = Subscription::create([
            'client_id' => $client->id,
            'start_date' => $startDate->toDateString(),
            'end_date' => $endDate->toDateString(),
            'number_of_users' => self::BASE_USERS,
            'disk_space' => self::BASE_DISK_SPACE,
            'base_price' => self::BASE_PRICE,
            'extra_users_price' => 0,
            'extra_disk_space_price' => 0,
            'discount_percentage' => $this->getDiscountPercentage($numberOfMonths),
            'total_price' => 0,
            'status' => 'active',
        ]);

        $this->calculateTotalPrice($subscription);

        return $subscription;
    }

    protected function getDiscountPercentage($numberOfMonths)
    {
        return isset($this->discounts[$numberOfMonths]) ? $this->discounts[$numberOfMonths] : 0;
    }

    public function calculateTotalPrice(Subscription $subscription)
    {
        $extraUsers = max(0, $subscription->number_of_users - self::BASE_USERS);
        $extraUsersPrice = $extraUsers * self::EXTRA_USER_PRICE;

        $extraDiskSpace = max(0, $subscription->disk_space - self::BASE_DISK_SPACE);
        $extraDiskSpacePrice = $extraDiskSpace * self::EXTRA_DISK_SPACE_PRICE;

        $totalPrice = $subscription->base_price + $extraUsersPrice + $extraDiskSpacePrice;

        $discountPercentage = $subscription->discount_percentage;
        if ($discountPercentage > 0) {
            $totalPrice = $totalPrice * (1 - $discountPercentage);
        }

        $subscription->extra_users_price = $extraUsersPrice;
        $subscription->extra_disk_space_price = $extraDiskSpacePrice;
        $subscription->total_price = $totalPrice;
        $subscription->save();
    }

    public function processDailyBilling()
    {
        $clients = Client::all();

        foreach ($clients as $client) {
            if ($this->isTrial($client)) {
                continue;
            }

            $subscription = $client->currentSubscription;
            if (!$subscription) {
                continue;
            }

            $dailyRate = $this->calculateDailyRate($subscription);

            $client->balance -= $dailyRate;
            $client->save();

            Transaction::create([
                'client_id' => $client->id,
                'amount' => -$dailyRate,
                'transaction_type' => 'deduction',
                'description' => 'Ежедневное списание за подписку',
                'balance_after_transaction' => $client->balance,
            ]);
        }
    }

    protected function calculateDailyRate(Subscription $subscription)
    {
        $dailyRate = $subscription->total_price / 31;
        return round($dailyRate, 2);
    }

    protected function isTrial(Client $client)
    {
        if ($client->trial_ends_at) {
            $now = Carbon::now();
            return $now->lessThanOrEqualTo(Carbon::parse($client->trial_ends_at));
        }
        return false;
    }

    public function addUsers(Subscription $subscription, $additionalUsers)
    {
        $subscription->number_of_users += $additionalUsers;
        $this->calculateTotalPrice($subscription);
    }

    public function addDiskSpace(Subscription $subscription, $additionalDiskSpace)
    {
        $subscription->disk_space += $additionalDiskSpace;
        $this->calculateTotalPrice($subscription);
    }

    public function changeTariff(Subscription $subscription, $newNumberOfUsers, $newDiskSpace)
    {
        $today = Carbon::now();
        $endDate = Carbon::parse($subscription->end_date);

        $daysRemaining = $today->diffInDays($endDate) + 1;

        if ($daysRemaining <= 0) {
            return false;
        }

        $oldDailyRate = $this->calculateDailyRate($subscription);

        $subscription->number_of_users = $newNumberOfUsers;
        $subscription->disk_space = $newDiskSpace;

        $this->calculateTotalPrice($subscription);

        $newDailyRate = $this->calculateDailyRate($subscription);
        $differenceInDailyRate = $newDailyRate - $oldDailyRate;

        $additionalAmountDue = $differenceInDailyRate * $daysRemaining;

        $client = $subscription->client;
        $client->balance -= $additionalAmountDue;
        $client->save();

        Transaction::create([
            'client_id' => $client->id,
            'amount' => -$additionalAmountDue,
            'transaction_type' => 'deduction',
            'description' => 'Корректировка при изменении тарифа',
            'balance_after_transaction' => $client->balance,
        ]);
    }

    public function processPayment(Client $client, $amount)
    {
        $client->balance += $amount;
        $client->save();

        Transaction::create([
            'client_id' => $client->id,
            'amount' => $amount,
            'transaction_type' => 'payment',
            'description' => 'Получен платеж',
            'balance_after_transaction' => $client->balance,
        ]);
    }
}
3. Команда для ежедневного списания

Создадим Artisan-команду, которая будет запускаться ежедневно и производить списания.


// app/Console/Commands/ProcessDailyBilling.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Services\BillingService;

class ProcessDailyBilling extends Command
{
    protected $signature = 'billing:daily';
    protected $description = 'Ежедневное списание средств с клиентов';

    public function handle()
    {
        $billingService = new BillingService();
        $billingService->processDailyBilling();

        $this->info('Ежедневное списание успешно выполнено.');
    }
}
В app/Console/Kernel.php добавим команду в расписание:


protected function schedule(Schedule $schedule)
{
    $schedule->command('billing:daily')->dailyAt('00:00');
}
4. Учет скидок при оплате нескольких месяцев

В BillingService при создании подписки учитывается скидка в методе getDiscountPercentage и применяется в методе calculateTotalPrice.


protected function getDiscountPercentage($numberOfMonths)
{
    return isset($this->discounts[$numberOfMonths]) ? $this->discounts[$numberOfMonths] : 0;
}
5. Обработка изменения тарифа и добавления пользователей

Методы addUsers, addDiskSpace и changeTariff позволяют изменять параметры подписки в любое время.

6. Учёт всех условий договора

Ежедневное списание средств с учётом количества дней в месяце (31 день).
Учет пробного периода, когда плата не взимается.
Возможность оплаты за несколько месяцев вперед со скидками.
Возможность изменения тарифа в любое время.
Заключение

Этот функционал обеспечивает полный контроль над процессом списания и зачисления оплаты в соответствии с условиями договора. Код реализует все необходимые требования и позволяет гибко управлять подписками клиентов.