entries, 'delta_minor')); self::assertSame(['transfer_out', 'fee', 'transfer_in'], array_column($command->entries, 'entry_type')); self::assertSame(1000, $command->metadata['gross_minor']); self::assertSame(125, $command->metadata['fee_minor']); self::assertSame(875, $command->metadata['net_minor']); self::assertSame(-125, array_sum(array_column($command->entries, 'delta_minor'))); } public function test_transfer_rejects_net_below_one_minor_unit(): void { $this->expectException(InvalidArgumentException::class); LedgerCommand::transfer('transfer:key', 'transfer', 1, 2, AssetType::POINTS, 10, 10); } public function test_transfer_rejects_negative_fee_before_arithmetic(): void { $this->expectException(InvalidArgumentException::class); LedgerCommand::transfer('transfer:key', 'transfer', 1, 2, AssetType::POINTS, PHP_INT_MAX, -1); } public function test_validation_returns_400_for_bad_operation_key(): void { $command = new LedgerCommand('bad key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => 1, 'entry_type' => 'credit', ]]); $error = $command->validate(); self::assertInstanceOf(WP_Error::class, $error); self::assertSame('aksrt_ledger_operation_key', $error->get_error_code()); self::assertSame(400, $error->get_error_data()['status']); } public function test_payload_hash_is_stable_for_metadata_key_order(): void { $entries = [['user_id' => 1, 'asset' => 'points', 'delta_minor' => 2, 'entry_type' => 'credit']]; $first = new LedgerCommand('stable:key', 'credit', $entries, '', '', 0, ['b' => 2, 'a' => 1]); $second = new LedgerCommand('stable:key', 'credit', $entries, '', '', 0, ['a' => 1, 'b' => 2]); self::assertSame($first->payload_hash(), $second->payload_hash()); } public function test_payload_hash_changes_with_business_payload(): void { $first = new LedgerCommand('stable:key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => 2, 'entry_type' => 'credit', ]]); $second = new LedgerCommand('stable:key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => 3, 'entry_type' => 'credit', ]]); self::assertNotSame($first->payload_hash(), $second->payload_hash()); } public function test_validation_rejects_empty_entries_and_invalid_asset(): void { $empty = new LedgerCommand('empty:key', 'credit', []); self::assertSame('aksrt_ledger_entries', $empty->validate()->get_error_code()); $invalid = new LedgerCommand('asset:key', 'credit', [[ 'user_id' => 1, 'asset' => 'cash', 'delta_minor' => 1, 'entry_type' => 'credit', ]]); self::assertSame('aksrt_ledger_asset', $invalid->validate()->get_error_code()); } public function test_validation_requires_integer_non_zero_entry_values(): void { foreach (['1', 0, PHP_INT_MIN] as $delta) { $command = new LedgerCommand('entry:key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => $delta, 'entry_type' => 'credit', ]]); self::assertSame('aksrt_ledger_entry', $command->validate()->get_error_code()); } } public function test_validation_rejects_negative_actor_and_invalid_entry_type(): void { $actor = new LedgerCommand('actor:key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => 1, 'entry_type' => 'credit', ]], '', '', -1); self::assertSame('aksrt_ledger_actor', $actor->validate()->get_error_code()); $type = new LedgerCommand('type:key', 'credit', [[ 'user_id' => 1, 'asset' => 'points', 'delta_minor' => 1, 'entry_type' => 'Bad Type', ]]); self::assertSame('aksrt_ledger_entry', $type->validate()->get_error_code()); } }