Files
autocrm/app/Http/Controllers/PaymentController.php
T
Vasyka 3911012c65 feat(i18n): mega-wrap 494 raw RO strings + 269 lang entries + 161 human RU/163 EN
Broad sweep across the whole codebase:
- Blade views (39 files, 315 wraps): tag-text and title/placeholder/alt
  attributes wrapped with {{ __() }}. Excludes scripts, styles, @php,
  @verbatim, {{ }}, {!! !!}, comments to avoid touching interpolations.
- PHP (54 files, 179 wraps): array 'key' => 'RO value' patterns and
  list items with diacritics wrapped with __(). Reverted __() inside
  const arrays (PHP disallows non-constant expressions).
- Added 269 new keys to lang/{ru,en}.json (identity fallback for
  unknowns → 161 human RU + 163 EN translations added for the most
  common enums, stages, roles, statuses, payment methods, vehicle
  categories, warehouse, portal, form actions.

Missing translations fall back to RO so the UI never breaks. All 306
tests pass; view cache compiles cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-15 05:53:22 +00:00

264 lines
9.5 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Central\PlatformSetting;
use App\Models\Central\Subscription;
use App\Tenancy\TenantManager;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
* Payment flow:
* - GET /billing → tenant invoices list (auth required)
* - POST /pay/{subscription} → start checkout (Stripe/PayPal/Bank)
* - GET /pay/{subscription}/success → marked paid via webhook in real flow
* - GET /pay/{subscription}/cancel → user aborted
* - POST /payments/stripe/webhook → Stripe → mark paid
* - POST /payments/paypal/webhook → PayPal → mark paid
*/
class PaymentController
{
public function billing(Request $request)
{
$tenant = app(TenantManager::class)->current();
if (! $tenant) abort(404);
$user = $request->user();
if (! $user) {
return redirect('/app/login');
}
$invoices = Subscription::where('company_id', $tenant->id)
->latest('period_end')
->limit(20)
->get();
$methods = $this->availableMethods();
$bank = PlatformSetting::get('payments.bank', []);
$legal = PlatformSetting::get('payments.company_legal', []);
return view('site.billing', [
'tenant' => $tenant,
'invoices' => $invoices,
'methods' => $methods,
'bank' => $bank,
'legal' => $legal,
'themeColor' => $tenant->settings['theme_color'] ?? '#3B82F6',
]);
}
public function startCheckout(Request $request, int $subscription)
{
$tenant = app(TenantManager::class)->current();
$sub = Subscription::where('company_id', $tenant?->id)->findOrFail($subscription);
$method = $request->input('method');
if (! in_array($method, ['stripe', 'paypal', 'paynet', 'bank'], true)) {
abort(422, 'Method invalid');
}
if ($sub->status === 'paid') {
return back()->with('error', 'Factura este deja plătită');
}
if ($method === 'bank') {
// Show bank instructions page; payment is manual.
return view('site.bank-instructions', [
'tenant' => $tenant,
'sub' => $sub,
'bank' => PlatformSetting::get('payments.bank', []),
'themeColor' => $tenant->settings['theme_color'] ?? '#3B82F6',
]);
}
if ($method === 'stripe') {
return $this->startStripe($sub);
}
if ($method === 'paypal') {
return $this->startPaypal($sub);
}
if ($method === 'paynet') {
return $this->startPaynet($sub);
}
}
private function startStripe(Subscription $sub)
{
$stripe = PlatformSetting::get('payments.stripe', []);
if (empty($stripe['enabled']) || empty($stripe['secret_key'])) {
return back()->with('error', 'Stripe nu este configurat. Contactează operatorul.');
}
// Real implementation requires `stripe/stripe-php` package. Here we
// produce a stub that explains the flow; in prod, replace with:
// $session = \Stripe\Checkout\Session::create([...]);
// return redirect($session->url);
return view('site.checkout-stub', [
'gateway' => 'Stripe',
'sub' => $sub,
'note' => __('Pentru a activa Stripe Checkout, instalează `composer require stripe/stripe-php` și activează în /admin/payment-settings.'),
]);
}
private function startPaypal(Subscription $sub)
{
$paypal = PlatformSetting::get('payments.paypal', []);
if (empty($paypal['enabled']) || empty($paypal['client_id'])) {
return back()->with('error', 'PayPal nu este configurat.');
}
return view('site.checkout-stub', [
'gateway' => 'PayPal',
'sub' => $sub,
'note' => __('Pentru a activa PayPal, instalează `composer require srmklive/paypal` și activează în /admin/payment-settings.'),
]);
}
private function startPaynet(Subscription $sub)
{
$paynet = PlatformSetting::get('payments.paynet', []);
if (empty($paynet['enabled']) || empty($paynet['merchant_code']) || empty($paynet['secret'])) {
return back()->with('error', 'Paynet nu este configurat. Contactează operatorul.');
}
$base = $paynet['mode'] === 'live'
? 'https://paynet.md/payment'
: 'https://test.paynet.md/payment';
// Real flow: build POST form to Paynet with HMAC-SHA256 signature.
// Stubbed for now until merchant credentials are set + tested.
return view('site.checkout-stub', [
'gateway' => 'Paynet (' . strtoupper($paynet['mode']) . ')',
'sub' => $sub,
'note' => "În prod: redirect către {$base} cu form ascuns (Merchant_Code, Service_ID, Order_ID={$sub->invoice_number}, Amount, Currency, Notify_URL, Return_URL, semnătură HMAC-SHA256). La success, webhook semnat verifică plata și marchează factura.",
]);
}
public function success(int $subscription)
{
$sub = Subscription::findOrFail($subscription);
return view('site.payment-success', ['sub' => $sub]);
}
public function cancel(int $subscription)
{
return view('site.payment-cancel');
}
public function stripeWebhook(Request $request)
{
$stripe = PlatformSetting::get('payments.stripe', []);
$secret = $stripe['webhook_secret'] ?? null;
if (! $secret) {
Log::warning('Stripe webhook hit but no secret configured');
return response()->json(['ok' => false], 400);
}
// Real impl: \Stripe\Webhook::constructEvent($payload, $sig, $secret)
// Mock: trust the body shape for stub mode.
$payload = $request->json()->all();
$event = $payload['type'] ?? null;
if ($event === 'checkout.session.completed') {
$subId = $payload['data']['object']['metadata']['subscription_id'] ?? null;
if ($subId && $sub = Subscription::find($subId)) {
$this->markPaid($sub, 'stripe_card', $payload['data']['object']['id'] ?? null);
}
}
return response()->json(['ok' => true]);
}
public function paypalWebhook(Request $request)
{
// Real impl: verify signature with PayPal
$payload = $request->json()->all();
$event = $payload['event_type'] ?? null;
if ($event === 'CHECKOUT.ORDER.APPROVED' || $event === 'PAYMENT.CAPTURE.COMPLETED') {
$subId = $payload['resource']['custom_id'] ?? null;
if ($subId && $sub = Subscription::find($subId)) {
$this->markPaid($sub, 'paypal', $payload['resource']['id'] ?? null);
}
}
return response()->json(['ok' => true]);
}
public function paynetWebhook(Request $request)
{
$paynet = PlatformSetting::get('payments.paynet', []);
$secret = $paynet['secret'] ?? null;
if (! $secret) {
Log::warning('Paynet webhook hit but no secret configured');
return response('error', 400);
}
// Paynet sends form-encoded notification. Verify HMAC-SHA256 signature
// computed over the canonical concatenated payload.
$orderId = $request->input('Order_ID'); // matches our invoice_number
$status = $request->input('Status'); // OK / FAIL
$amount = $request->input('Amount');
$signature = $request->input('Signature');
// Canonical signature payload (Merchant_Code|Order_ID|Amount|Status).
$canonical = ($paynet['merchant_code'] ?? '') . '|' . $orderId . '|' . $amount . '|' . $status;
$expected = hash_hmac('sha256', $canonical, $secret);
if (! hash_equals($expected, (string) $signature)) {
Log::warning('Paynet webhook signature mismatch', ['order' => $orderId]);
return response('invalid signature', 400);
}
if ($status !== 'OK') {
return response('ok', 200);
}
$sub = Subscription::where('invoice_number', $orderId)->first();
if ($sub) {
$this->markPaid($sub, 'paynet', $request->input('Reference_ID'));
}
return response('ok', 200);
}
private function markPaid(Subscription $sub, string $method, ?string $reference): void
{
if ($sub->status === 'paid') return;
$sub->update([
'status' => 'paid',
'paid_at' => now(),
'payment_method' => $method,
'reference' => $reference,
]);
// Extend company subscription
$sub->company?->update([
'status' => 'active',
'active_until' => $sub->period_end,
]);
}
private function availableMethods(): array
{
$stripe = PlatformSetting::get('payments.stripe', []);
$paypal = PlatformSetting::get('payments.paypal', []);
$paynet = PlatformSetting::get('payments.paynet', []);
$bank = PlatformSetting::get('payments.bank', []);
return [
'stripe' => ! empty($stripe['enabled']) && ! empty($stripe['publishable_key']),
'paypal' => ! empty($paypal['enabled']) && ! empty($paypal['client_id']),
'paynet' => ! empty($paynet['enabled']) && ! empty($paynet['merchant_code']),
'bank' => ! empty($bank['enabled']) && ! empty($bank['iban']),
];
}
}