1ba25e6b4e
Закрыты 4 TODO в Webhook PoC. Job теперь полностью реализует §5.5
narrative ТЗ за исключением DuplicateDetector (Биз-19) и
SendNewLeadNotificationJob (Биз-20) — отдельные ветви.
5 новых Eloquent-моделей:
- app/app/Models/BalanceTransaction.php — списание lead_charge -1,
type-константы (TYPE_LEAD_CHARGE и т.д.)
- app/app/Models/ActivityLog.php — event=deal.created с
context.source=webhook, event-константы
- app/app/Models/RejectedDealsLog.php — zero_balance ветка вместо
Log::info (payload сохраняется для возможного восстановления)
- app/app/Models/SupplierLeadCost.php — composite PK (id, received_at),
snapshot cost_rub из suppliers, supplier_id resolves через
project_suppliers m2m (первый активный по sort_order)
- app/app/Models/Supplier.php — минимальная для FK target
Job-структура реструктурирована: handle() оркестрирует, делегирует в
logRejection() / chargeNewLead() / resolveSupplierId() / upsertDeal().
Все INSERT'ы в одной DB::transaction — атомарность Ю-2 (deal +
balance_transaction + supplier_lead_cost появляются вместе).
Graceful skip SupplierLeadCost если у проекта нет активного supplier
через project_suppliers + Log::warning. TODO для production: SystemSetting
fallback.
6 новых Pest-тестов в ProcessWebhookJobTest:
- BalanceTransaction lead_charge -1 для новой сделки
- Дубль vid НЕ создаёт BalanceTransaction
- ActivityLog event=deal.created с context.source=webhook
- RejectedDealsLog reason=zero_balance при balance_leads=0
- SupplierLeadCost snapshot cost_rub (helper seedSupplierForProject)
- SupplierLeadCost graceful skip без активного supplier
Pest 37/37 зелёные за 3.9 сек. Pint + Larastan чисто (ide-helper:models
регенерирован для 5 новых моделей).
CLAUDE.md v1.12 → v1.13. Реестр Открытые_вопросы v1.21 → v1.22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.1 KiB
PHP
82 lines
2.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
||
/**
|
||
* Транзакция баланса тенанта (списание/пополнение лидов и/или рублей).
|
||
*
|
||
* Tenant-aware с RLS. log_hash — append-only audit chain (OPEN-И-15),
|
||
* заполняется триггером `audit_chain_hash()` BEFORE INSERT.
|
||
*
|
||
* Источник: db/schema.sql v8.7 §7.6, table `balance_transactions`.
|
||
*
|
||
* @mixin IdeHelperBalanceTransaction
|
||
*/
|
||
class BalanceTransaction extends Model
|
||
{
|
||
public const TYPE_TRIAL_BONUS = 'trial_bonus';
|
||
|
||
public const TYPE_TOPUP = 'topup';
|
||
|
||
public const TYPE_LEAD_CHARGE = 'lead_charge';
|
||
|
||
public const TYPE_REFUND = 'refund';
|
||
|
||
public const TYPE_MANUAL_ADJUSTMENT = 'manual_adjustment';
|
||
|
||
public const TYPE_HISTORICAL_IMPORT = 'historical_import';
|
||
|
||
public const TYPE_CHARGEBACK_WRITEDOWN = 'chargeback_writedown';
|
||
|
||
public const TYPE_CHARGEBACK_REPAYMENT = 'chargeback_repayment';
|
||
|
||
public $timestamps = false;
|
||
|
||
protected $fillable = [
|
||
'tenant_id',
|
||
'type',
|
||
'amount_rub',
|
||
'amount_leads',
|
||
'balance_rub_after',
|
||
'balance_leads_after',
|
||
'description',
|
||
'related_type',
|
||
'related_id',
|
||
'user_id',
|
||
'admin_user_id',
|
||
'created_at',
|
||
];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'tenant_id' => 'integer',
|
||
'amount_rub' => 'decimal:2',
|
||
'amount_leads' => 'integer',
|
||
'balance_rub_after' => 'decimal:2',
|
||
'balance_leads_after' => 'integer',
|
||
'related_id' => 'integer',
|
||
'user_id' => 'integer',
|
||
'admin_user_id' => 'integer',
|
||
'created_at' => 'datetime',
|
||
];
|
||
}
|
||
|
||
/** @return BelongsTo<Tenant, $this> */
|
||
public function tenant(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Tenant::class);
|
||
}
|
||
|
||
/** @return BelongsTo<User, $this> */
|
||
public function user(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class);
|
||
}
|
||
}
|