139 lines
4.6 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|