在我的Laravel应用程序中,我有一个页面,用户必须支付150英镑的会员费。为了处理这笔付款,我选择了Stripe。
我将所有费用以及用户的ID都存储在付款表中。
付款表
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.");
}
}
使用代金券的用户没有付款,因此弄清楚何时自动开具账单是很棘手的。