presseportale/app/Services/Billing/AdminGrantService.php
Kevin Adametz 3f4679fbb2 admin: Credits und freie PMs ohne Zahlung gutschreiben
Neues Panel "Guthaben & Kontingent" auf der Benutzer-Detailseite
(users:manage): Credits gehen als Grant-Buchung ins Wallet-Ledger,
freie Pressemitteilungen als bezahlte 0-€-Einzelkäufe, die das
Buchungs-Gate erfüllen und beim Veröffentlichen regulär eingelöst
werden — Stripe bleibt unberührt. Jede Gutschrift landet mit Vergeber
und Grund als neuer Typ "Gutschrift" im Aktivitäts-Verlauf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:06:01 +00:00

89 lines
3.2 KiB
PHP

<?php
namespace App\Services\Billing;
use App\Enums\ActivityType;
use App\Enums\CreditTransactionType;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
use App\Models\CreditTransaction;
use App\Models\User;
use App\Services\ActivityLog;
use Illuminate\Support\Facades\DB;
/**
* Admin-Gutschriften ohne Zahlung (Admin ▸ Benutzer ▸ Guthaben & Kontingent):
* Credits als Grant-Buchung ins Wallet-Ledger, freie Pressemitteilungen als
* bezahlte 0-€-Einzelkäufe — beides greift in die reguläre Buchungs-/
* Kontingent-Mechanik, ohne Stripe zu berühren, und wird im
* Aktivitäts-Verlauf protokolliert.
*/
class AdminGrantService
{
public function __construct(
private readonly CreditWalletService $wallet,
private readonly ActivityLog $activityLog,
) {}
public function grantCredits(User $user, int $credits, User $grantedBy, ?string $note = null): CreditTransaction
{
$description = 'Admin-Gutschrift durch '.$grantedBy->name.($note !== null && $note !== '' ? ': '.$note : '');
$transaction = $this->wallet->credit($user, $credits, CreditTransactionType::Grant, $description);
$this->activityLog->record(
ActivityType::AdminGrant,
sprintf(
'%s hat %s (%s) %d Credits gutgeschrieben — ohne Zahlung.%s',
$grantedBy->name,
$user->name,
$user->email,
$credits,
$note !== null && $note !== '' ? ' Grund: '.$note : '',
),
user: $user,
subject: $transaction,
meta: ['credits' => $credits, 'granted_by' => $grantedBy->id, 'note' => $note],
);
return $transaction;
}
/**
* Schreibt dem User freie Pressemitteilungen gut: bezahlte Einzel-PM-Käufe
* zu 0 € ({@see SinglePurchase::scopeGrantingSubmission()}), die das
* Submit-Gate erfüllen und beim Veröffentlichen regulär eingelöst werden.
*/
public function grantFreePressReleases(User $user, int $count, User $grantedBy, ?string $note = null): void
{
if ($count < 1) {
throw new \InvalidArgumentException('Anzahl muss positiv sein.');
}
DB::transaction(function () use ($user, $count): void {
for ($i = 0; $i < $count; $i++) {
$user->singlePurchases()->create([
'type' => SinglePurchaseType::SinglePm,
'status' => SinglePurchaseStatus::Paid,
'price_cents' => 0,
'currency' => config('billing.default_currency'),
'paid_at' => now(),
]);
}
});
$this->activityLog->record(
ActivityType::AdminGrant,
sprintf(
'%s hat %s (%s) %d freie Pressemitteilung(en) gutgeschrieben — ohne Zahlung.%s',
$grantedBy->name,
$user->name,
$user->email,
$count,
$note !== null && $note !== '' ? ' Grund: '.$note : '',
),
user: $user,
meta: ['free_press_releases' => $count, 'granted_by' => $grantedBy->id, 'note' => $note],
);
}
}