100% скидка на единовременную оплату с Stripe [Laravel]

В моем приложении Laravel у меня есть страница, на которой пользователи должны заплатить 150 фунтов стерлингов за членский взнос. Для обработки этого платежа я выбрал Stripe.

Я храню все платежи в таблице платежей вместе с идентификатором пользователя.

Таблица платежей

Schema::create('payments', function (Blueprint $table) {
    $table->increments('id');
    $table->uuid('user_id');
    $table->string('transaction_id');
    $table->string('description');
    $table->string('amount');
    $table->string('currency');
    $table->datetime('date_recorded');
    $table->string('card_brand');
    $table->string('card_last_4', 4);
    $table->string('status');
    $table->timestamps();
});

Я также внедрил собственную систему ваучеров, поскольку я не использую подписки.

Таблица ваучеров

Schema::create('vouchers', function (Blueprint $table) {
    $table->increments('id');
    $table->string('code');
    $table->integer('discount_percent');
    $table->dateTime('expires_on');
    $table->timestamps();
});

Платежный контроллер

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Carbon\Carbon;
use App\User;
use App\Payment;
use App\Voucher;
use App\Mail\User\PaymentReceipt;
use App\Mail\Admin\UserMembershipPaid;
use Log;
use Mail;
use Validator;
use Stripe;
use Stripe\Error\Card;

class PaymentController extends Controller
{
    /**
     * Set an initial amount to be used by the controller
     *
     * @var float
     */
    private $amount = 150.00;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('verified');
        $this->middleware('is_investor');
        $this->middleware('is_passive_member');
    }

    /**
     * Display a form allowing a user to make a payment
     *
     * @return void
     */
    public function showPaymentForm()
    {
        return view('user.payment');
    }

    /**
     * Handle an entered voucher code by the user
     * Either calculate a discount or skip the payment form 
     *
     * @param [type] $request
     * @return void
     */
    public function processVoucher(Request $request)
    {
        $rules = [
            'code' => 'required|exists:vouchers',
        ];

        $messages = [
            'code.required' => 'You submitted a blank field',
            'code.exists' => 'This voucher code is not valid'
        ];

        Validator::make($request->all(), $rules, $messages)->validate();

        $entered_voucher_code = $request->get('code');

        $voucher = Voucher::where('code', $entered_voucher_code)->where('expires_on', '>', Carbon::now())->first();

        // If the voucher exists
        if ($voucher) {
            $discount_percent = $voucher->discount_percent;
            $new_amount = $this->amount - ($discount_percent / 100 * $this->amount);

            // As Stripe won't handle charges of 0, we need some extra logic
            if ($new_amount <= 0.05) {
                $this->upgradeAccount(auth()->user());

                Log::info(auth()->user()->log_reference . " used voucher code {$voucher->code} to get a 100% discount on their Active membership");

                return redirect()->route('user.dashboard')->withSuccess("Your membership has been upgraded free of charge.");
            }
            // Apply the discount to this session 
            else {
                Log::info(auth()->user()->log_reference . " used voucher code {$voucher->code} to get a {$voucher->discount_percent}% discount on their Active membership");

                // Store some data in the session and redirect
                session(['voucher_discount' => $voucher->discount_percent]);
                session(['new_price' => $this->amount - ($voucher->discount_percent / 100) * $this->amount]);

                return redirect()->back()->withSuccess([
                    'voucher' => [
                        'message' => 'Voucher code ' . $voucher->code . ' has been applied. Please fill in the payment form',
                        'new_price' => $new_amount
                    ]
                ]);
            }
        }
        // Voucher has expired
        else {
            return redirect()->back()->withError('This voucher code has expired.');
        }
    }

    /**
     * Handle a Stripe payment attempt from the Stripe Elements form
     * Takes into account voucher codes if they are less than 100%
     *
     * @param Request $request
     * @return void
     */
    public function handleStripePayment(Request $request)
    {
        // Retreive the currently authenticated user
        $user = auth()->user();

        // Get the Stripe token from the request
        $token = $request->get('stripeToken');

        // Set the currency for your country
        $currency = 'GBP';

        // Set an initial amount for Stripe to use with the charge
        $amount = $this->amount;

        // A description for this payment
        $description = "Newable Private Investing Portal - Active Membership fee";

        // Initialize Stripe with given public key
        $stripe = Stripe::make(config('services.stripe.secret'));

        // Attempt a charge via Stripe
        try {
            Log::info("{$user->log_reference} attempted to upgrade their membership to Active");

            // Check that token was sent across, if it wasn't, stop
            if (empty($token)) {
                return redirect()->back()->withErrors([
                    'error' => "Token error, do you have JavaScript disabled?"
                ]);
            }

            // Check whether a discount should be applied to this charge
            if (session()->has('voucher_discount')) {
                $discount_percentage = session()->pull('voucher_discount');

                $discount = ($discount_percentage / 100) * $amount;

                $amount = $amount - $discount;

                session()->forget('new_price');
            }

            // Create a charge with an idempotent id to prevent duplicate charges
            $charge = $stripe->idempotent(session()->getId())->charges()->create([
                'amount' => $amount,
                'currency' => $currency,
                'card' => $token,
                'description' => $description,
                'statement_descriptor' => 'Newable Ventures',
                'receipt_email' => $user->email
            ]);

            //If the payment is successful, store the payment, send some emails and upgrade this user
            if ($charge['status'] == 'succeeded') {
                $this->storePayment($charge);

                Mail::send(new PaymentReceipt($user));
                Mail::send(new UserMembershipPaid($user));

                $this->upgradeAccount($user);

                return redirect()->route('user.dashboard')->withSuccess("Your payment was successful, you will soon recieve an email receipt.");
            // If the payment was unsuccessful
            } else {
                $this->storePayment($charge);

                Log::error("Stripe charge failed for {$user->log_reference}");

                return redirect()->back()->withErrors([
                    'error' => "Unfortunately, your payment was unsuccessful."
                ]);
            }
        } catch (Exception $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - Exception - error details {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\MissingParameterException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - MissingParameterException - error details {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\CardErrorException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - CardErrorException - error details {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\ApiLimitExceededException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - ApiLimitExceededException - error details {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\BadRequestException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - BadRequestException -  error details {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\ServerErrorException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - ServerErrorException - error details: {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        } catch (\Cartalyst\Stripe\Exception\UnauthorizedException $e) {
            Log::error("Error attempting Stripe Charge for {$user->log_reference} - UnauthorizedException - error details: {$e->getMessage()}");

            return redirect()->back()->withErrors([
                'error' => $e->getMessage()
            ]);
        }
    }

    /**
     * Store a Stripe chargee in our database so we can reference it later if necessary
     * Charges stored against users for cross referencing and easy refunds
     *
     * @return void
     */
    private function storePayment(array $charge)
    {
        $payment = new Payment();

        $payment->transaction_id = $charge['id'];
        $payment->description = $charge['description'];
        $payment->amount = $charge['amount'];
        $payment->currency = $charge['currency'];
        $payment->date_recorded = Carbon::createFromTimestamp($charge['created']);
        $payment->card_brand = $charge['source']['brand'];
        $payment->card_last_4 = $charge['source']['last4'];
        $payment->status = $charge['status'];

        auth()->user()->payments()->save($payment);

        if ($payment->status === "succeeded") {
            Log::info("Successful Stripe Charge recorded for {$user->log_reference} with Stripe reference {$payment->transaction_id} using card ending {$payment->card_last_4}");
        } else {
            Log::info("Failed Stripe Charge recorded for {$user->log_reference} with Stripe reference {$payment->transaction_id} using card ending {$payment->card_last_4}");
        }
    }

    /**
     * Handle a user account upgrade from whatever to Active
     *
     * @param User $user
     * @return void
     */
    private function upgradeAccount(User $user)
    {
        $current_membership_type = $user->member_type;

        $user->member_type = "Active";

        $user->save();

        Log::info("{$user->log_reference} has been upgraded from a {$current_membership_type} member to an Active Member.");
    }
}

processVoucher() берет строку, введенную пользователем, проверяет, существует ли она в таблице vouchers, а затем применяет процент скидки к комиссии 150.00.

Затем он добавляет новое значение в сеанс, и я использую его в Stripe Charge.

Проблема

Проблема в том, что минимальная оплачиваемая сумма Stripe составляет 0.05, поэтому, чтобы обойти эту проблему, я только что вызвал метод, который обновляет учетную запись.

Теоретически я должен хранить бесплатные обновления в таблице charges, но в итоге я получу несколько нулевых значений.

Это ужасное решение?

В модели User у меня также есть следующие методы:

/**
 * Relationship to payments
 */
public function payments()
{
    return $this->hasMany(Payment::class, 'user_id', 'id');
}

/**
 * Relationship to payments to get most recent payment
 *
 * @return void
 */
public function latest_payment()
{
    return $this->hasOne(Payment::class, 'user_id', 'id')->latest();
} 

Они используются, чтобы я мог рассчитать, когда пользователь в последний раз совершал платеж, поскольку мне нужно было ежегодно выставлять им счета без использования подписки, поскольку пользователи также могут использовать ваучеры со скидкой 100% для обновления.

Я сделал эту консольную команду:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

use Carbon\Carbon;
use App\User;
use App\Payment;
use Log;

class ExpireMembership extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'membership:expire';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Expire user memberships after 1 year of being Active.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //Retrieve all users who are an active member with their list of payments
        $activeUsers = User::where('member_type', 'Active')->get();

        //Get current date
        $current_date = Carbon::now();

        foreach($activeUsers as $user){
            $this->info("Checking user {$user->log_reference}");

            // If a user has at least one payment recorded
            if ($user->payments()->exists()){
                //Get membership end date (latest payment + 1 year added)
                $membership_end_date = $user->payments
                    ->where('description', 'Newable Private Investing Portal - Active Membership fee')
                    ->sortByDesc('created_at')
                    ->first()->created_at->addYear();
            }
            // If the user has no payments but is an active member just check if they're older than a year
            else{
                $membership_end_date = $user->created_at->addYear();
            }
            //If the membership has gone over 1 year, expire the membership.
            if ($current_date->lessThanOrEqualTo($membership_end_date)) {
                $user->member_type = "Passive";
                $user->save();

                $this->info($user->log_reference . "membership has expired and membership status has been set to Passive.");

                Log::info($user->log_reference . "membership has expired and membership status has been set to Passive.");

            }
        }

        $this->info("Finished checking user memberships.");

    }
}

Пользователи, которые используют ваучеры, не имеют платежей, поэтому выяснить, когда их автоматически выставлять счета, сложно.

Мне непонятно, зачем вам нужно использовать разовые платежи, если вы строите систему подписки. Почему вы не можете использовать подписки Stripe? Они предлагают все, что вам нужно, основываясь на вашем описании здесь. Если бы вы могли предложить больше информации, это было бы полезно.

sam 07.02.2019 16:01

Создание системы подписки с помощью Stripe, но без выставления счетов за подписку Stripe <вставьте запутанный мем Джеки Чана>

Martin Bean 07.02.2019 16:23

Я добавлю больше ясности, но это, по сути, потому, что бизнес-сфера не может решить, хотят ли они на самом деле подписки или нет, и существует странная бизнес-логика относительно того, должны ли они платить или нет.

Jesse Orange 07.02.2019 16:28

Но да... Я знаю, это звучит глупо, но по существу я должен хранить использование ваучера в моей таблице платежей?

Jesse Orange 07.02.2019 16:29

Stripe предлагает множество различных функций, которые обеспечивают большую гибкость при создании платежных систем. Бизнес может быть не уверен, нужны ли ему подписки или разовые платежи, но это не должно определять, как вы строите систему. Вы можете использовать пробные версии и скидки в системе подписки Stripe, чтобы предоставлять бесплатные периоды и предотвращать автоматическое продление. Вы должны построить систему, используя подписки Stripe, а затем, если бизнес решит, что «на самом деле нам нужны разовые платежи», вы можете просто написать немного кода, чтобы подписки не продлевались...

sam 07.02.2019 16:40

...и если бизнес решит, что ему делать нужны подписки, тогда отлично, вы уже построили систему, которая будет делать то, что они хотят. Идея, которую вы предложили здесь, будет Работа, но это плохой подход, который вызовет у вас головную боль, и если они решат, что «на самом деле мы хотим подписки», вам придется провести рефакторинг.

sam 07.02.2019 16:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
2
6
768
0

Другие вопросы по теме