new app store assets ready for relese
This commit is contained in:
@@ -1,52 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\LicenseService;
|
||||
use App\Notifications\AccountUpdate;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\LicenseService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
class PaddleController extends Controller {
|
||||
public function checkout(Request $r): JsonResponse {
|
||||
$config=config('tarnax.paddle');abort_unless($config['api_key'] && $config['price_id'] && $config['client_token'],503,'Checkout is awaiting merchant configuration.');
|
||||
$user=$r->user();if(!$user->account_token){$user->forceFill(['account_token'=>(string)Str::uuid()])->save();}
|
||||
$base=$config['environment']==='production'?'https://api.paddle.com':'https://sandbox-api.paddle.com';
|
||||
$response=Http::withToken($config['api_key'])->timeout(15)->post($base.'/transactions',['items'=>[['price_id'=>$config['price_id'],'quantity'=>1]],'collection_mode'=>'automatic','custom_data'=>['account_token'=>$user->account_token]]);
|
||||
abort_unless($response->successful(),502,'Payment provider unavailable.');
|
||||
return response()->json(['transaction_id'=>$response->json('data.id')]);
|
||||
}
|
||||
public function webhook(Request $r,LicenseService $licenses): JsonResponse {
|
||||
$secret=config('tarnax.paddle.webhook_secret');abort_unless($secret,503);
|
||||
$parts=[];foreach(explode(';',$r->header('Paddle-Signature','')) as $part){$pair=explode('=',trim($part),2);if(count($pair)===2){$parts[$pair[0]][]=$pair[1];}}
|
||||
$ts=$parts['ts'][0]??'';abort_unless(ctype_digit($ts) && abs(time()-(int)$ts)<=300,401);
|
||||
$expected=hash_hmac('sha256',$ts.':'.$r->getContent(),$secret);$valid=false;foreach($parts['h1']??[] as $signature){$valid=$valid||hash_equals($expected,$signature);}abort_unless($valid,401);
|
||||
$event=$r->validate(['event_id'=>'required|string|max:100','event_type'=>'required|string|max:100','occurred_at'=>'required|date','data'=>'required|array']);
|
||||
DB::transaction(function()use($event,$licenses){
|
||||
$inserted=DB::table('webhook_events')->insertOrIgnore(['id'=>$event['event_id'],'type'=>$event['event_type'],'processed_at'=>now()]);if(!$inserted){return;}
|
||||
$d=$event['data'];$type=$event['event_type'];$at=Carbon::parse($event['occurred_at']);
|
||||
if($type==='transaction.completed'){
|
||||
abort_unless(($d['status']??'')==='completed',422);
|
||||
$matching=collect($d['items']??[])->contains(fn($i)=>($i['price']['id']??null)===config('tarnax.paddle.price_id') && ($i['quantity']??0)>=1);if(!$matching){return;}
|
||||
$user=User::where('account_token',$d['custom_data']['account_token']??'')->first();abort_unless($user,422,'Unknown customer.');
|
||||
$purchase=DB::table('purchases')->where('transaction_id',$d['id'])->lockForUpdate()->first();
|
||||
if($purchase && ($purchase->status==='refunded' || Carbon::parse($purchase->provider_updated_at)->gte($at))){return;}
|
||||
$id=$purchase?->id??DB::table('purchases')->insertGetId(['user_id'=>$user->id,'provider'=>'paddle','transaction_id'=>$d['id'],'subscription_id'=>$d['subscription_id']??null,'status'=>'completed','total'=>(int)($d['details']['totals']['grand_total']??0),'currency'=>$d['currency_code']??'USD','provider_updated_at'=>$at,'created_at'=>now(),'updated_at'=>now()]);
|
||||
$licenses->issue($user->id,$id);
|
||||
$user->notify(new AccountUpdate('Your Tarnax license is ready','Your purchase has been confirmed. Your license and downloads are in your account.',url('/account')));
|
||||
}
|
||||
if($type==='adjustment.updated' || $type==='adjustment.created'){
|
||||
if(($d['status']??'')!=='approved' || !in_array($d['action']??'',['refund','chargeback','chargeback_warning'])){return;}
|
||||
$purchase=DB::table('purchases')->where('transaction_id',$d['transaction_id']??'')->lockForUpdate()->first();
|
||||
if($purchase){DB::table('purchases')->where('id',$purchase->id)->update(['status'=>'refunded','provider_updated_at'=>$at,'updated_at'=>now()]);DB::table('licenses')->where('purchase_id',$purchase->id)->update(['status'=>'revoked','updated_at'=>now()]);}
|
||||
}
|
||||
if(in_array($type,['subscription.canceled','subscription.paused','subscription.past_due'])){
|
||||
$purchases=DB::table('purchases')->where('subscription_id',$d['id']??'')->where('provider_updated_at','<',$at)->get();
|
||||
foreach($purchases as $purchase){DB::table('licenses')->where('purchase_id',$purchase->id)->update(['status'=>'revoked','updated_at'=>now()]);DB::table('purchases')->where('id',$purchase->id)->update(['status'=>$d['status'],'provider_updated_at'=>$at]);}
|
||||
}
|
||||
},3);
|
||||
return response()->json(['received'=>true]);
|
||||
}
|
||||
|
||||
class PaddleController extends Controller
|
||||
{
|
||||
public function checkout(Request $r): JsonResponse
|
||||
{
|
||||
$config = config('tarnax.paddle');
|
||||
abort_unless($config['api_key'] && $config['price_id'] && $config['client_token'], 503, 'Checkout is awaiting merchant configuration.');
|
||||
$user = $r->user();
|
||||
if (! $user->account_token) {
|
||||
$user->forceFill(['account_token' => (string) Str::uuid()])->save();
|
||||
}
|
||||
$base = $config['environment'] === 'production' ? 'https://api.paddle.com' : 'https://sandbox-api.paddle.com';
|
||||
$response = Http::withToken($config['api_key'])->timeout(15)->post($base.'/transactions', ['items' => [['price_id' => $config['price_id'], 'quantity' => 1]], 'collection_mode' => 'automatic', 'custom_data' => ['account_token' => $user->account_token]]);
|
||||
abort_unless($response->successful(), 502, 'Payment provider unavailable.');
|
||||
|
||||
return response()->json(['transaction_id' => $response->json('data.id')]);
|
||||
}
|
||||
|
||||
public function webhook(Request $r, LicenseService $licenses): JsonResponse
|
||||
{
|
||||
$secret = config('tarnax.paddle.webhook_secret');
|
||||
abort_unless($secret, 503);
|
||||
$parts = [];
|
||||
foreach (explode(';', $r->header('Paddle-Signature', '')) as $part) {
|
||||
$pair = explode('=', trim($part), 2);
|
||||
if (count($pair) === 2) {
|
||||
$parts[$pair[0]][] = $pair[1];
|
||||
}
|
||||
}
|
||||
$ts = $parts['ts'][0] ?? '';
|
||||
abort_unless(ctype_digit($ts) && abs(time() - (int) $ts) <= 300, 401);
|
||||
$expected = hash_hmac('sha256', $ts.':'.$r->getContent(), $secret);
|
||||
$valid = false;
|
||||
foreach ($parts['h1'] ?? [] as $signature) {
|
||||
$valid = $valid || hash_equals($expected, $signature);
|
||||
}abort_unless($valid, 401);
|
||||
$event = $r->validate(['event_id' => 'required|string|max:100', 'event_type' => 'required|string|max:100', 'occurred_at' => 'required|date', 'data' => 'required|array']);
|
||||
DB::transaction(function () use ($event, $licenses) {
|
||||
$inserted = DB::table('webhook_events')->insertOrIgnore(['id' => $event['event_id'], 'type' => $event['event_type'], 'processed_at' => now()]);
|
||||
if (! $inserted) {
|
||||
return;
|
||||
}
|
||||
$d = $event['data'];
|
||||
$type = $event['event_type'];
|
||||
$at = Carbon::parse($event['occurred_at']);
|
||||
if ($type === 'transaction.completed') {
|
||||
abort_unless(($d['status'] ?? '') === 'completed' && ! empty($d['id']), 422);
|
||||
DB::table('payment_locks')->insertOrIgnore(['id' => $d['id']]);
|
||||
DB::table('payment_locks')->where('id', $d['id'])->lockForUpdate()->first();
|
||||
abort_unless(config('tarnax.paddle.price_id'), 503);
|
||||
$matching = collect($d['items'] ?? [])->contains(fn ($i) => ($i['price']['id'] ?? null) === config('tarnax.paddle.price_id') && ($i['quantity'] ?? 0) >= 1);
|
||||
if (! $matching) {
|
||||
return;
|
||||
}
|
||||
if (DB::table('refund_blocks')->where('transaction_id', $d['id'])->exists()) {
|
||||
return;
|
||||
}
|
||||
$user = User::where('account_token', $d['custom_data']['account_token'] ?? '')->first();
|
||||
abort_unless($user, 422, 'Unknown customer.');
|
||||
$purchase = DB::table('purchases')->where('transaction_id', $d['id'])->lockForUpdate()->first();
|
||||
if ($purchase && ($purchase->status === 'refunded' || Carbon::parse($purchase->provider_updated_at)->gte($at))) {
|
||||
return;
|
||||
}
|
||||
$id = $purchase?->id ?? DB::table('purchases')->insertGetId(['user_id' => $user->id, 'provider' => 'paddle', 'transaction_id' => $d['id'], 'subscription_id' => $d['subscription_id'] ?? null, 'status' => 'completed', 'total' => (int) ($d['details']['totals']['grand_total'] ?? 0), 'currency' => $d['currency_code'] ?? 'USD', 'provider_updated_at' => $at, 'created_at' => now(), 'updated_at' => now()]);
|
||||
$licenses->issue($user->id, $id);
|
||||
if (! empty($d['subscription_id'])) {
|
||||
$state = DB::table('subscription_states')->where('id', $d['subscription_id'])->first();
|
||||
if ($state && ! in_array($state->status, ['active', 'trialing'])) {
|
||||
DB::table('licenses')->where('purchase_id', $id)->update(['status' => 'revoked']);
|
||||
}
|
||||
}
|
||||
$user->notify(new AccountUpdate('Your Tarnax license is ready', 'Your purchase has been confirmed. Your license and downloads are in your account.', url('/account'), 'purchase-confirmed'));
|
||||
}
|
||||
if ($type === 'adjustment.updated' || $type === 'adjustment.created') {
|
||||
if (($d['status'] ?? '') !== 'approved' || ! in_array($d['action'] ?? '', ['refund', 'chargeback', 'chargeback_warning'])) {
|
||||
return;
|
||||
}
|
||||
abort_unless(! empty($d['transaction_id']), 422);
|
||||
DB::table('payment_locks')->insertOrIgnore(['id' => $d['transaction_id']]);
|
||||
DB::table('payment_locks')->where('id', $d['transaction_id'])->lockForUpdate()->first();
|
||||
DB::table('refund_blocks')->updateOrInsert(['transaction_id' => $d['transaction_id']], ['occurred_at' => $at]);
|
||||
$purchase = DB::table('purchases')->where('transaction_id', $d['transaction_id'])->lockForUpdate()->first();
|
||||
if ($purchase) {
|
||||
DB::table('purchases')->where('id', $purchase->id)->update(['status' => 'refunded', 'provider_updated_at' => $at, 'updated_at' => now()]);
|
||||
DB::table('licenses')->where('purchase_id', $purchase->id)->update(['status' => 'revoked', 'updated_at' => now()]);
|
||||
}
|
||||
}
|
||||
if (str_starts_with($type, 'subscription.') && ! empty($d['id']) && in_array($d['status'] ?? '', ['active', 'trialing', 'canceled', 'paused', 'past_due'])) {
|
||||
DB::table('subscription_states')->insertOrIgnore(['id' => $d['id'], 'status' => $d['status'], 'occurred_at' => $at]);
|
||||
$state = DB::table('subscription_states')->where('id', $d['id'])->lockForUpdate()->first();
|
||||
if (Carbon::parse($state->occurred_at)->gt($at)) {
|
||||
return;
|
||||
}
|
||||
DB::table('subscription_states')->where('id', $d['id'])->update(['status' => $d['status'], 'occurred_at' => $at]);
|
||||
$purchases = DB::table('purchases')->where('subscription_id', $d['id'])->where('status', '!=', 'refunded')->lockForUpdate()->get();
|
||||
foreach ($purchases as $purchase) {
|
||||
DB::table('licenses')->where('purchase_id', $purchase->id)->update(['status' => in_array($d['status'], ['active', 'trialing']) ? 'active' : 'revoked', 'updated_at' => now()]);
|
||||
DB::table('purchases')->where('id', $purchase->id)->update(['status' => $d['status'], 'provider_updated_at' => $at, 'updated_at' => now()]);
|
||||
}
|
||||
}
|
||||
}, 3);
|
||||
|
||||
return response()->json(['received' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user