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]); } }