Files
lotteryLaravel/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileItemIndexController.php
kang 1e7b2b31ee feat: enhance reconciliation job processing and reporting features
- Updated ReconcileItemIndexController to include admin user validation and improved transfer order handling.
- Refactored ReconcileJobStoreController to ensure date range handling is more precise with start and end of day adjustments.
- Enhanced various report controllers to include currency code in responses for better financial clarity.
- Introduced new methods in AdminReconcileJobService for wallet transfer job creation and improved item scanning logic.
- Added wallet lookup idempotent path configuration for integration services to enhance API interactions.
2026-06-16 13:50:55 +08:00

144 lines
5.4 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1\Admin\Reconcile;
use App\Models\AdminUser;
use App\Models\TransferOrder;
use App\Models\ReconcileJob;
use Illuminate\Http\Request;
use App\Models\ReconcileItem;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminTransferOrderCapabilities;
use App\Services\Wallet\LotteryTransferService;
use App\Services\Wallet\HttpMainSiteWalletIdempotentProbeClient;
use App\Services\Wallet\MainSiteWalletIdempotentProbeResult;
/** GET /api/v1/admin/reconcile-jobs/{reconcile_job}/items */
final class ReconcileItemIndexController extends Controller
{
public function __construct(
private readonly LotteryTransferService $transferService,
private readonly HttpMainSiteWalletIdempotentProbeClient $mainSiteProbeClient,
) {}
public function __invoke(Request $request, ReconcileJob $reconcile_job): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$p = AdminApiList::readPaging($request, 50, 200);
$paginator = $reconcile_job->items()
->orderBy('id')
->paginate($p['perPage'], ['*'], 'page', $p['page']);
$transferNos = collect($paginator->items())
->map(fn (ReconcileItem $item) => $item->side_a_ref)
->filter(fn ($value) => is_string($value) && $value !== '')
->values()
->all();
$transferOrders = $transferNos === []
? collect()
: TransferOrder::query()
->with(['player:id,site_code,site_player_id'])
->whereIn('transfer_no', $transferNos)
->get()
->keyBy('transfer_no');
return AdminApiList::jsonWith($paginator, function (ReconcileItem $r) use ($transferOrders, $admin): array {
$transferNo = (string) ($r->side_a_ref ?? '');
/** @var TransferOrder|null $order */
$order = $transferNo !== '' ? $transferOrders->get($transferNo) : null;
$resolvedStatuses = ['success', 'reversed', 'manually_processed'];
$currentStatus = $order?->status;
$isResolved = $r->resolved_at !== null
|| in_array($currentStatus, $resolvedStatuses, true);
$capabilities = $order instanceof TransferOrder
? AdminTransferOrderCapabilities::forOrder(
$order,
$admin instanceof AdminUser ? $admin : null,
$this->transferService,
)
: [
'can_reverse' => false,
'can_complete_credit' => false,
'can_manually_process' => false,
];
$mainSiteCheck = $this->resolveMainSiteCheck($order);
return [
'id' => (int) $r->id,
'side_a_ref' => $r->side_a_ref,
'side_b_ref' => $r->side_b_ref,
'external_ref_no' => $order?->external_ref_no,
'difference_amount' => (int) $r->difference_amount,
'status' => $r->status,
'resolved_at' => $r->resolved_at?->toIso8601String(),
'is_resolved' => $isResolved,
'current_transfer_status' => $currentStatus,
'transfer_direction' => $order?->direction,
'transfer_fail_reason' => $order?->fail_reason,
'main_site_check' => $mainSiteCheck['status'],
'main_site_check_message' => $mainSiteCheck['message'],
'main_site_external_ref_no' => $mainSiteCheck['external_ref_no'],
...$capabilities,
'created_at' => $r->created_at?->toIso8601String(),
];
}, [
'job_id' => (int) $reconcile_job->id,
'job_no' => $reconcile_job->job_no,
]);
}
/**
* @return array{status: string, message: ?string, external_ref_no: ?string}
*/
private function resolveMainSiteCheck(?TransferOrder $order): array
{
if ($order === null || $order->player === null) {
return [
'status' => MainSiteWalletIdempotentProbeResult::STATUS_SKIPPED,
'message' => null,
'external_ref_no' => null,
];
}
if (trim((string) $order->idempotent_key) === '') {
return [
'status' => MainSiteWalletIdempotentProbeResult::STATUS_SKIPPED,
'message' => null,
'external_ref_no' => null,
];
}
$shouldProbe = trim((string) $order->external_ref_no) !== ''
|| $order->status === 'success'
|| (
$order->direction === 'in'
&& $order->status === 'pending_reconcile'
&& $order->fail_reason === 'lottery_credit_failed'
);
if (! $shouldProbe) {
return [
'status' => MainSiteWalletIdempotentProbeResult::STATUS_SKIPPED,
'message' => null,
'external_ref_no' => null,
];
}
$probe = $this->mainSiteProbeClient->probe($order->player, (string) $order->idempotent_key);
return [
'status' => $probe->status,
'message' => $probe->message,
'external_ref_no' => $probe->externalRefNo ?? $order->external_ref_no,
];
}
}