Transaction State Projection
Issue #1015 needs per-transaction state (balance_before, balance_after, state_hash) in one atomic batch.
Package gives extension points, not hardcoded business projection. State-aware is opt-in.
How it works (opt-in)
- By default, package inserts regular
TransactionDto— no extra state calculation. - If you want state tracking, create custom Assembler that uses
TransactionStateService. - Register your custom Assembler in config.
No overhead for users who don't need state projection.
1) Add columns
Add custom columns to transactions table in your app migration:
php
$table->string('balance_before')->nullable();
$table->string('balance_after')->nullable();
$table->string('state_hash', 64)->nullable();2) Register custom assembler
php
// config/wallet.php
'assemblers' => [
'transaction' => \App\Wallet\StateAwareTransactionAssembler::class,
],3) Implement custom assembler
Your assembler computes balance before/after and pushes to TransactionStateService:
php
use Bavix\Wallet\Enums\TransactionType;
use Bavix\Wallet\Internal\Assembler\TransactionDtoAssembler;
use Bavix\Wallet\Internal\Assembler\TransactionDtoAssemblerInterface;
use Bavix\Wallet\Internal\Dto\TransactionDtoInterface;
use Bavix\Wallet\Internal\Service\MathServiceInterface;
use Bavix\Wallet\Internal\Service\TransactionStateService;
use Bavix\Wallet\Services\RegulatorServiceInterface;
use Illuminate\Database\Eloquent\Model;
final readonly class StateAwareTransactionAssembler implements TransactionDtoAssemblerInterface
{
public function __construct(
private TransactionDtoAssembler $base,
private TransactionStateService $stateService, // Your service instance
private RegulatorServiceInterface $regulator,
private MathServiceInterface $mathService,
) {}
public function create(
Model $payable,
int $walletId,
TransactionType $type,
float|int|string $amount,
bool $confirmed,
?array $meta,
?string $uuid
): TransactionDtoInterface {
$dto = $this->base->create($payable, $walletId, $type, $amount, $confirmed, $meta, $uuid);
$before = $this->regulator->amount($payable);
$after = $before;
if ($confirmed) {
$after = $type === TransactionType::Deposit
? $this->mathService->add($before, $amount)
: $this->mathService->sub($before, $amount);
}
// Push state to your service
$this->stateService->push($dto->getUuid(), $walletId, [
'balance' => $before,
], [
'balance' => $after,
]);
return $dto;
}
}4) Use transformer to persist state
php
use Bavix\Wallet\Internal\Dto\TransactionDtoInterface;
use Bavix\Wallet\Internal\Service\TransactionStateService;
use Bavix\Wallet\Internal\Transform\TransactionDtoTransformer;
use Bavix\Wallet\Internal\Transform\TransactionDtoTransformerInterface;
final readonly class TransactionStateDtoTransformer implements TransactionDtoTransformerInterface
{
public function __construct(
private TransactionDtoTransformer $base,
private TransactionStateService $stateService,
) {}
public function extract(TransactionDtoInterface $dto): array
{
$result = $this->base->extract($dto);
if (!$this->stateService->has($dto->getUuid())) {
return $result;
}
$before = $this->stateService->before($dto->getUuid());
$after = $this->stateService->after($dto->getUuid());
$result['balance_before'] = $before['balance'];
$result['balance_after'] = $after['balance'];
$result['state_hash'] = hash('sha256', $dto->getUuid().':'.$dto->getAmount().':'.$before['balance'].':'.$after['balance']);
return $result;
}
}Key points
- No default
state_awareconfig flag — opt-in only via custom Assembler TransactionStateServiceis your utility class, not auto-registered in core- Compute balance before/after in your Assembler (not '0','0' placeholders)
balance_beforeandbalance_afterare sequentially correct inside single atomic operation, including mixed wallets in same batch
See wallet-side columns: Wallet State Projection.