Files
wordpress-theme-aksrt/src/Ledger/LedgerCommand.php
T

139 lines
4.6 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Ledger;
defined('ABSPATH') || exit;
final class LedgerCommand {
/**
* @param array<int,array{user_id:int,asset:string,delta_minor:int,entry_type:string,counterparty_user_id?:int,remark?:string}> $entries
* @param array<string,mixed> $metadata
*/
public function __construct(
public readonly string $operation_key,
public readonly string $reason,
public readonly array $entries,
public readonly string $ref_type = '',
public readonly string $ref_key = '',
public readonly int $actor_user_id = 0,
public readonly array $metadata = []
) {}
/**
* @param array<string,mixed> $metadata
*/
public static function transfer(
string $operation_key,
string $reason,
int $sender_id,
int $recipient_id,
AssetType|string $asset,
int $gross_minor,
int $fee_minor,
string $ref_type = '',
string $ref_key = '',
string $sender_remark = '',
string $recipient_remark = '',
array $metadata = []
): self {
$asset = AssetType::from_value($asset);
if ($sender_id <= 0 || $recipient_id <= 0 || $sender_id === $recipient_id || $gross_minor <= 0 || $fee_minor < 0 || $fee_minor >= $gross_minor) {
throw new \InvalidArgumentException('ledger_transfer_invalid');
}
$net_minor = $gross_minor - $fee_minor;
$entries = [[
'user_id' => $sender_id,
'asset' => $asset->value,
'delta_minor' => -$net_minor,
'entry_type' => 'transfer_out',
'counterparty_user_id' => $recipient_id,
'remark' => $sender_remark,
]];
if ($fee_minor > 0) {
$entries[] = [
'user_id' => $sender_id,
'asset' => $asset->value,
'delta_minor' => -$fee_minor,
'entry_type' => 'fee',
'counterparty_user_id' => $recipient_id,
'remark' => __('转账手续费', 'aksrt'),
];
}
$entries[] = [
'user_id' => $recipient_id,
'asset' => $asset->value,
'delta_minor' => $net_minor,
'entry_type' => 'transfer_in',
'counterparty_user_id' => $sender_id,
'remark' => $recipient_remark,
];
$metadata['gross_minor'] = $gross_minor;
$metadata['fee_minor'] = $fee_minor;
$metadata['net_minor'] = $net_minor;
return new self($operation_key, $reason, $entries, $ref_type, $ref_key, $sender_id, $metadata);
}
public function validate() {
if ($this->operation_key === '' || strlen($this->operation_key) > 191 || !preg_match('/^[A-Za-z0-9:._-]+$/', $this->operation_key)) {
return new \WP_Error('aksrt_ledger_operation_key', __('账务操作标识无效', 'aksrt'), ['status' => 400]);
}
if ($this->reason === '' || strlen($this->reason) > 64 || sanitize_key($this->reason) !== $this->reason) {
return new \WP_Error('aksrt_ledger_reason', __('账务原因无效', 'aksrt'), ['status' => 400]);
}
if ($this->entries === []) {
return new \WP_Error('aksrt_ledger_entries', __('账务明细不能为空', 'aksrt'), ['status' => 400]);
}
if ($this->actor_user_id < 0) {
return new \WP_Error('aksrt_ledger_actor', __('账务操作者无效', 'aksrt'), ['status' => 400]);
}
foreach ($this->entries as $entry) {
try {
AssetType::from((string) ($entry['asset'] ?? ''));
} catch (\ValueError $e) {
return new \WP_Error('aksrt_ledger_asset', __('资产类型无效', 'aksrt'), ['status' => 400]);
}
$user_id = $entry['user_id'] ?? null;
$delta_minor = $entry['delta_minor'] ?? null;
$entry_type = $entry['entry_type'] ?? null;
if (!is_int($user_id) || $user_id <= 0
|| !is_int($delta_minor) || $delta_minor === 0 || $delta_minor === PHP_INT_MIN
|| !is_string($entry_type) || $entry_type === '' || strlen($entry_type) > 32 || sanitize_key($entry_type) !== $entry_type
) {
return new \WP_Error('aksrt_ledger_entry', __('账务明细无效', 'aksrt'), ['status' => 400]);
}
if (isset($entry['counterparty_user_id']) && (!is_int($entry['counterparty_user_id']) || $entry['counterparty_user_id'] <= 0)) {
return new \WP_Error('aksrt_ledger_entry', __('账务明细无效', 'aksrt'), ['status' => 400]);
}
}
return true;
}
public function payload_hash(): string {
$payload = [
'reason' => $this->reason,
'entries' => $this->entries,
'ref_type' => $this->ref_type,
'ref_key' => $this->ref_key,
'actor_user_id' => $this->actor_user_id,
'metadata' => $this->metadata,
];
self::sort_recursive($payload);
return hash('sha256', (string) wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
private static function sort_recursive(array &$value): void {
foreach ($value as &$item) {
if (is_array($item)) {
self::sort_recursive($item);
}
}
unset($item);
if (!array_is_list($value)) {
ksort($value);
}
}
}