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>
89 lines
3.2 KiB
PHP
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],
|
|
);
|
|
}
|
|
}
|