Files
portal/app/app/Console/Commands/BillingMigrateLeadsToRubCommand.php
T

96 lines
3.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\BalanceTransaction;
use App\Models\PricingTier;
use App\Models\Tenant;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* Идемпотентная одноразовая миграция: balance_leads → balance_rub по цене ступени 1.
*
* Запускается ОДИН РАЗ в проде после деплоя Billing v2 Spec A Phase A. Повторный
* запуск — no-op (тенанты с balance_leads=0 уже не обрабатываются).
*
* Per-tenant атомарность: lockForUpdate(Tenant) внутри DB::transaction.
*
* Spec: docs/superpowers/specs/2026-05-23-billing-v2-spec-a-balance-rub-design.md §4.4
* Plan: docs/superpowers/plans/2026-05-23-billing-v2-spec-a-balance-rub-plan.md Task A.11
*/
final class BillingMigrateLeadsToRubCommand extends Command
{
protected $signature = 'billing:migrate-leads-to-rub';
protected $description = 'Convert legacy balance_leads to balance_rub at tier 1 price (idempotent, run once in prod)';
public function handle(): int
{
$tier1 = PricingTier::query()
->where('is_active', true)
->where('tier_no', 1)
->where('effective_from', '<=', Carbon::now('Europe/Moscow')->toDateString())
->orderBy('effective_from', 'desc')
->first();
if ($tier1 === null) {
$this->error('No active tier 1 found. Aborting.');
return self::FAILURE;
}
$count = 0;
Tenant::query()
->where('balance_leads', '>', 0)
->chunkById(100, function ($tenants) use ($tier1, &$count): void {
foreach ($tenants as $tenant) {
DB::transaction(function () use ($tenant, $tier1, &$count): void {
/** @var Tenant|null $locked */
$locked = Tenant::query()
->whereKey($tenant->id)
->lockForUpdate()
->first();
if ($locked === null || (int) $locked->balance_leads <= 0) {
return; // idempotency — already migrated or zero
}
$migratedLeads = (int) $locked->balance_leads;
$migratedKopecks = bcmul((string) $migratedLeads, (string) $tier1->price_per_lead_kopecks, 0);
$migratedRub = bcdiv((string) $migratedKopecks, '100', 2);
$newBalanceRub = bcadd((string) $locked->balance_rub, $migratedRub, 2);
DB::table('tenants')
->where('id', $locked->id)
->update([
'balance_rub' => $newBalanceRub,
'balance_leads' => 0,
]);
BalanceTransaction::create([
'tenant_id' => $locked->id,
'type' => BalanceTransaction::TYPE_MIGRATION,
'amount_leads' => -$migratedLeads,
'amount_rub' => $migratedRub,
'balance_leads_after' => 0,
'balance_rub_after' => $newBalanceRub,
'description' => 'Конвертация предоплаченных лидов в ₽ (миграция модели биллинга Spec A)',
'created_at' => now(),
]);
$count++;
});
}
});
$this->info("Migrated {$count} tenant(s).");
return self::SUCCESS;
}
}