This commit is contained in:
ct
2025-07-02 01:08:11 +08:00
parent 209c022f1d
commit 68a47ec916
19 changed files with 503 additions and 75 deletions

View File

@@ -37,6 +37,32 @@ public static function getPricingPageOneTime()
return $one_time;
}
public static function getSubscriptionPlanByStripePriceID($stripe_price_id)
{
$plans = self::getSubscriptions('subscription_plans', true, true);
foreach ($plans as $plan) {
if ($plan['id'] == 'free') {
continue;
}
$stripe_price_ids = self::getPlanSystemProperty($plan, 'stripe.stripe_price_ids');
if (is_array($stripe_price_ids)) {
if (isset($stripe_price_ids['month']) && in_array($stripe_price_id, $stripe_price_ids['month'])) {
return $plan;
}
if (isset($stripe_price_ids['year']) && in_array($stripe_price_id, $stripe_price_ids['year'])) {
return $plan;
}
}
}
return null;
}
public static function getPlanSystemProperty($plan, $property)
{
// Get the environment (test or prod)
@@ -50,10 +76,10 @@ public static function getPlanSystemProperty($plan, $property)
// Inject environment into the path
// stripe.product_id.month becomes system.stripe.product_id.{env}.month
array_splice($propertyParts, 2, 0, $environment);
$fullPath = 'system.'.implode('.', $propertyParts);
$fullPath = 'system.' . implode('.', $propertyParts);
} else {
// For non-stripe properties, just prepend 'system.'
$fullPath = 'system.'.$property;
$fullPath = 'system.' . $property;
}
return data_get($plan, $fullPath);

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Helpers\FirstParty\Purchase;
use App\Helpers\FirstParty\Stripe\StripeHelper;
use App\Models\Plan;
use App\Models\UserPlan;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Events\WebhookReceived;
class SubscriptionHelper
{
public static function handleSubscriptionWebhookEvents(WebhookReceived $event)
{
switch ($event->payload['type']) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
self::handleSubscriptionUpsert($event);
break;
case 'customer.subscription.deleted':
self::handleSubscriptionDelete($event);
break;
}
}
private static function handleSubscriptionUpsert(WebhookReceived $event)
{
$object = $event->payload['data']['object'];
//dump($object);
$ignore_statuses = ['incomplete_expired', 'incomplete'];
if (in_array($object['status'], $ignore_statuses)) {
return;
}
$user = StripeHelper::getUserByStripeID($object['customer']);
if ($user) {
foreach ($object['items']['data'] as $line_item) {
//dump($line_item);
$stripe_price_id = $line_item['plan']['id'];
$current_period_end = $line_item['current_period_end'];
$cancel_at = null;
$canceled_at = null;
if ($line_item['subscription'] == $object['id']) {
$cancel_at = $object['cancel_at'];
$canceled_at = $object['canceled_at'];
}
$subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id);
$plan = Plan::where('tier', $subscription_config['id'])->first();
if ($plan) {
$user_plan = UserPlan::where('user_id', $user->id)->first();
if ($user_plan) {
$user_plan->update([
'plan_id' => $plan->id,
'current_period_end' => $current_period_end,
'cancel_at' => $cancel_at,
'canceled_at' => $canceled_at,
]);
} else {
$user_plan = UserPlan::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'current_period_end' => $current_period_end,
'cancel_at' => $cancel_at,
'canceled_at' => $canceled_at,
]);
}
}
}
}
}
private static function handleSubscriptionDelete(WebhookReceived $event)
{
// /
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Helpers\FirstParty\Purchase;
use App\Helpers\FirstParty\Stripe\StripeHelper;
use App\Models\UserUsage;
use Illuminate\Support\Facades\App;
use Laravel\Cashier\Events\WebhookReceived;
class WatermarkUsageHelper
{
public static function handleWatermarkUsageWebhookEvents(WebhookReceived $event)
{
switch ($event->payload['type']) {
case 'invoice.paid':
self::handleInvoicePaid($event);
break;
}
}
private static function handleInvoicePaid(WebhookReceived $event)
{
$object = $event->payload['data']['object'];
//dump($object);
$accept_statuses = ['paid', 'partially_paid'];
if (!in_array($object['status'], $accept_statuses)) {
return;
}
$user = StripeHelper::getUserByStripeID($object['customer']);
if ($user) {
foreach ($object['lines']['data'] as $line_item) {
$stripe_price_id = $line_item['pricing']['price_details']['price'];
//dd($stripe_price_id);
$subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id);
if ($subscription_config) {
if ($subscription_config['type'] == 'subscription_plans') {
$user_usage = UserUsage::where('user_id', $user->id)->first();
if ($user_usage) {
$user_usage->update([
'non_watermark_videos_left' => $subscription_config['system']['non_watermark_videos'],
]);
} else {
$user_usage = UserUsage::create([
'user_id' => $user->id,
'non_watermark_videos_left' => $subscription_config['system']['non_watermark_videos'],
]);
}
}
//dd($subscription_config);
}
}
}
}
}

View File

@@ -2,31 +2,13 @@
namespace App\Helpers\FirstParty\Stripe;
use App\Models\User;
use Laravel\Cashier\Events\WebhookReceived;
class StripeHelper
{
public static function handleSubscriptionWebhookEvents(WebhookReceived $event)
public static function getUserByStripeID($customer_id)
{
switch ($event->payload['type']) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
self::handleSubscriptionUpsert($event);
break;
case 'customer.subscription.deleted':
self::handleSubscriptionDelete($event);
break;
}
}
private static function handleSubscriptionUpsert(WebhookReceived $event)
{
///
}
private static function handleSubscriptionDelete(WebhookReceived $event)
{
///
return User::where('stripe_id', $customer_id)->first();
}
}

View File

@@ -31,7 +31,7 @@ public function create(): Response
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => 'required|string|lowercase|email|max:255|unique:' . User::class,
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);

View File

@@ -15,7 +15,7 @@ class VerifyEmailController extends Controller
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false) . '?verified=1');
return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
@@ -25,6 +25,6 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
event(new Verified($user));
}
return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false) . '?verified=1');
return redirect()->intended(route(config('platform.general.authed_route_redirect'), absolute: false).'?verified=1');
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App;
use App\Models\Plan;
use App\Models\User;
use App\Models\UserPlan;
use Illuminate\Support\Facades\Auth;
@@ -64,7 +65,7 @@ public function handleGoogleCallback()
return redirect()->intended(route('home'))->with('success', "You're now logged in!");
} catch (\Exception $e) {
throw $e;
//throw $e;
$error_message = 'Google login failed. Please try again.';
if (config('app.debug')) {
$error_message = $e->getMessage();
@@ -78,10 +79,10 @@ private function setupUser($user)
{
$user_plan = UserPlan::where('user_id', $user->id)->first();
if (!$user_plan) {
if (! $user_plan) {
$user_plan = UserPlan::create([
'user_id' => $user->id,
'plan_id' => 'free',
'plan_id' => Plan::where('tier', 'free')->first()->id,
]);
}
}

View File

@@ -32,8 +32,8 @@ public function subscribe(Request $request)
$payload = [
'mode' => 'subscription',
'success_url' => route('subscribe.success') . '?' . 'session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('subscribe.cancelled') . '?' . 'session_id={CHECKOUT_SESSION_ID}',
'success_url' => route('subscribe.success').'?'.'session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('subscribe.cancelled').'?'.'session_id={CHECKOUT_SESSION_ID}',
'line_items' => [[
'price' => $price_id,
'quantity' => 1,
@@ -81,8 +81,8 @@ public function purchase(Request $request)
$price_id = $request->input('price_id');
$payload = [
'success_url' => route('subscribe.success') . '?' . 'session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('subscribe.cancelled') . '?' . 'session_id={CHECKOUT_SESSION_ID}',
'success_url' => route('subscribe.success').'?'.'session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('subscribe.cancelled').'?'.'session_id={CHECKOUT_SESSION_ID}',
];
$checkout_session = Auth::user()->checkout([$price_id => 1], $payload);
@@ -107,7 +107,7 @@ public function purchaseSuccess(Request $request)
Session::forget('checkout_session_id');
return redirect()->route('home')->with('success', "Thank you for purchasing! Your purchase should be active momentarily. Please refresh the page if you do not see your plan.");
return redirect()->route('home')->with('success', 'Thank you for purchasing! Your purchase should be active momentarily. Please refresh the page if you do not see your plan.');
}
public function purchaseCancelled(Request $request)

View File

@@ -47,15 +47,15 @@ public function share(Request $request): array
'user' => $request->user(),
'user_is_admin' => user_is_master_admin($request->user()),
],
'ziggy' => fn(): array => [
'ziggy' => fn (): array => [
...(new Ziggy)->toArray(),
'location' => $request->url(),
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
'flash' => [
'message' => fn() => $request->session()->get('message'),
'error' => fn() => $request->session()->get('error'),
'success' => fn() => $request->session()->get('success'),
'message' => fn () => $request->session()->get('message'),
'error' => fn () => $request->session()->get('error'),
'success' => fn () => $request->session()->get('success'),
],
];
}

View File

@@ -2,10 +2,8 @@
namespace App\Listeners;
use App\Helpers\FirstParty\Stripe\StripeHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Helpers\FirstParty\Purchase\SubscriptionHelper;
use App\Helpers\FirstParty\Purchase\WatermarkUsageHelper;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
@@ -23,6 +21,7 @@ public function __construct()
*/
public function handle(WebhookReceived $event): void
{
StripeHelper::handleSubscriptionWebhookEvents($event);
SubscriptionHelper::handleSubscriptionWebhookEvents($event);
WatermarkUsageHelper::handleWatermarkUsageWebhookEvents($event);
}
}

View File

@@ -11,21 +11,19 @@
/**
* Class Plan
*
*
* @property int $id
* @property string $name
* @property string $tier
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @package App\Models
*/
class Plan extends Model
{
protected $table = 'plans';
protected $table = 'plans';
protected $fillable = [
'name',
'tier'
];
protected $fillable = [
'name',
'tier',
];
}

View File

@@ -7,13 +7,14 @@
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable;
use Laravel\Sanctum\HasApiTokens;
use Str;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
use HasApiTokens, HasFactory, Notifiable, SoftDeletes, Billable;
/**
* The attributes that are mass assignable.

View File

@@ -11,7 +11,7 @@
/**
* Class UserPlan
*
*
* @property int $id
* @property int $user_id
* @property int $plan_id
@@ -20,26 +20,24 @@
* @property Carbon|null $canceled_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @package App\Models
*/
class UserPlan extends Model
{
protected $table = 'user_plans';
protected $table = 'user_plans';
protected $casts = [
'user_id' => 'int',
'plan_id' => 'int',
'current_period_end' => 'datetime',
'cancel_at' => 'datetime',
'canceled_at' => 'datetime'
];
protected $casts = [
'user_id' => 'int',
'plan_id' => 'int',
'current_period_end' => 'datetime',
'cancel_at' => 'datetime',
'canceled_at' => 'datetime',
];
protected $fillable = [
'user_id',
'plan_id',
'current_period_end',
'cancel_at',
'canceled_at'
];
protected $fillable = [
'user_id',
'plan_id',
'current_period_end',
'cancel_at',
'canceled_at',
];
}

36
app/Models/UserUsage.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class UserUsage
*
* @property int $id
* @property int $user_id
* @property int $non_watermark_videos_left
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
*
* @package App\Models
*/
class UserUsage extends Model
{
protected $table = 'user_usages';
protected $casts = [
'user_id' => 'int',
'non_watermark_videos_left' => 'int'
];
protected $fillable = [
'user_id',
'non_watermark_videos_left'
];
}