Harden ABDM bridge auth flow and sync handling

This commit is contained in:
DevSoftTech Server 2026-07-28 00:14:49 +05:30
parent bf6090cfd5
commit 90831d4b39
6 changed files with 343 additions and 37 deletions

View File

@ -1206,10 +1206,11 @@ class AbdmGateway extends BaseController
}
$patientName = '';
$patientRow = [];
if ($this->db->tableExists('patient_master')) {
$pmFields = $this->db->getFieldNames('patient_master') ?? [];
$pmSelect = ['id'];
foreach (['p_fname', 'p_lname', 'abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
foreach (['p_fname', 'p_lname', 'dob', 'p_dob', 'birth_date', 'date_of_birth', 'birth_year', 'year_of_birth', 'age', 'p_age', 'abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
if (in_array($f, $pmFields, true)) {
$pmSelect[] = $f;
}
@ -1221,10 +1222,7 @@ class AbdmGateway extends BaseController
->get(1)
->getRowArray() ?? [];
$patientName = trim(
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
trim((string) ($patientRow['p_lname'] ?? ''))
);
$patientName = $this->patientDisplayName($patientRow);
if ($abhaAddress === '') {
foreach (['abha_address', 'abha', 'abha_id', 'abha_no'] as $field) {
@ -1248,6 +1246,8 @@ class AbdmGateway extends BaseController
}
}
$patientBirthYear = $this->resolvePatientBirthYear($patientRow, $abhaAddress, $abhaNumber);
if ($abhaAddress === '' && $abhaNumber === '') {
$logBridge('error', ['ok' => 0, 'error_text' => 'ABHA address or ABHA number is required for record push.'], 'ABHA address or ABHA number is required for record push.', 'abdm.opd.prescription.share.validation');
return $this->response->setJSON([
@ -1442,6 +1442,7 @@ class AbdmGateway extends BaseController
'patient_name' => $patientName !== '' ? $patientName : ('PATIENT-' . $patientId),
'abha_id' => $abhaNumber,
'abha_address' => $abhaAddress,
'year_of_birth' => $patientBirthYear,
'hi_type' => $hiType,
'record_type' => $hiType,
'visit_date' => $visitDate,
@ -1560,6 +1561,7 @@ class AbdmGateway extends BaseController
$patName = (string) ($fhirPayload['patient_name'] ?? '');
$doctorName = (string) ($fhirPayload['doctor_name'] ?? '');
$visitDate = (string) ($fhirPayload['visit_date'] ?? date('Y-m-d'));
$birthYear = (int) ($fhirPayload['year_of_birth'] ?? 0);
$ccRef = (string) ($fhirPayload['care_context_reference'] ?? ('IPD-' . $ipdId . '-' . $visitDate));
$ccDisplay = (string) ($fhirPayload['care_context_display'] ?? ('Discharge Summary ' . $visitDate));
$bundle = (array) ($fhirPayload['bundle'] ?? []);
@ -1613,8 +1615,9 @@ class AbdmGateway extends BaseController
try {
$result = $this->connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patName,
'patient_name' => $patName !== '' ? $patName : ('PATIENT-' . $patientId),
'abha_id' => $abhaId,
'year_of_birth' => $birthYear,
'hi_type' => 'DischargeSummaryRecord',
'record_type' => 'DischargeSummaryRecord',
'visit_date' => $visitDate,
@ -1708,10 +1711,11 @@ class AbdmGateway extends BaseController
if ($this->db->tableExists('patient_master')) {
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
}
$patName = trim(
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
trim((string) ($patientRow['p_lname'] ?? ''))
) ?: trim((string) ($labReq->patient_name ?? ''));
$patName = $this->patientDisplayName($patientRow);
if ($patName === '') {
$patName = trim((string) ($labReq->patient_name ?? ''));
}
$patientBirthYear = $this->resolvePatientBirthYear($patientRow, str_contains($abhaId, '@') ? $abhaId : '', str_contains($abhaId, '@') ? '' : $abhaId);
// ── Load test / charge name ────────────────────────────────────────────
$testTitle = '';
@ -1856,6 +1860,7 @@ class AbdmGateway extends BaseController
'patient_id' => (string) $patientId,
'patient_name' => $patName,
'abha_id' => $abhaId,
'year_of_birth' => $patientBirthYear,
'hi_type' => 'DiagnosticReportRecord',
'record_type' => 'DiagnosticReportRecord',
'visit_date' => $visitDate,
@ -1939,10 +1944,10 @@ class AbdmGateway extends BaseController
if ($this->db->tableExists('patient_master')) {
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
}
$patName = trim(
trim((string) ($patientRow['p_fname'] ?? '')) . ' ' .
trim((string) ($patientRow['p_lname'] ?? ''))
) ?: trim((string) ($labReq->patient_name ?? ''));
$patName = $this->patientDisplayName($patientRow);
if ($patName === '') {
$patName = trim((string) ($labReq->patient_name ?? ''));
}
$testTitle = '';
$chargeId = (int) ($labReq->charge_id ?? 0);
@ -2162,6 +2167,7 @@ class AbdmGateway extends BaseController
'patient_name' => $patientName,
'abha_id' => preg_match('/^\d{14}$/', $abhaId) === 1 ? $abhaId : '',
'abha_address' => str_contains($abhaId, '@') ? $abhaId : '',
'year_of_birth' => (int) ($payload['year_of_birth'] ?? 0),
'hi_type' => 'ImmunizationRecord',
'record_type' => 'ImmunizationRecord',
'visit_date' => $visitDate,
@ -2342,7 +2348,7 @@ class AbdmGateway extends BaseController
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
}
$patientName = trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? '')));
$patientName = $this->patientDisplayName($patientRow);
if ($patientName === '') {
$patientName = trim((string) ($ipdRow['P_name'] ?? ''));
}
@ -2673,7 +2679,7 @@ class AbdmGateway extends BaseController
return null;
}
$patientName = trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? '')));
$patientName = $this->patientDisplayName($patientRow);
$rawAbha = trim($preferredAbhaId);
if ($rawAbha === '') {
foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $field) {
@ -2751,6 +2757,7 @@ class AbdmGateway extends BaseController
'care_context_reference' => $ccRef,
'care_context_display' => 'Immunization Record - ' . $visitDate,
'patient_name' => $patient['name'],
'year_of_birth' => $this->resolvePatientBirthYear($patientRow, str_contains($rawAbha, '@') ? $rawAbha : '', str_contains($rawAbha, '@') ? '' : $rawAbha),
'visit_date' => $visitDate,
'records' => $records,
];
@ -3345,12 +3352,24 @@ class AbdmGateway extends BaseController
$queueId = null;
$connectorErr = null;
try {
$patientRowForPush = $this->loadPatientRow($patientId);
$sanitizedPatientName = trim((string) $patientName);
if ($sanitizedPatientName === '' || preg_match('/\s0$/', $sanitizedPatientName) === 1) {
$sanitizedPatientName = $this->patientDisplayName($patientRowForPush);
}
$patientBirthYear = $this->resolvePatientBirthYear(
$patientRowForPush,
str_contains($abhaId, '@') ? $abhaId : '',
str_contains($abhaId, '@') ? '' : $abhaId
);
// Use the new store-and-link flow (POST /v3/records/push) for re-push:
// No consent needed — bridge stores record and serves it when ABDM requests data.
$result = $this->connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patientName,
'patient_name' => $sanitizedPatientName !== '' ? $sanitizedPatientName : ('PATIENT-' . $patientId),
'abha_id' => $abhaId,
'year_of_birth' => $patientBirthYear,
'hi_type' => $hiType,
'record_type' => $this->mapHiTypeToRecordType($hiType),
'visit_date' => $visitDate !== '' ? $visitDate : date('Y-m-d'),
@ -5559,11 +5578,18 @@ class AbdmGateway extends BaseController
$bridgeRecordId = 0;
$connectorError = null;
try {
$patientRowForPush = $this->loadPatientRow($patientId);
$patientBirthYear = $this->resolvePatientBirthYear(
$patientRowForPush,
str_contains($abhaId, '@') ? $abhaId : '',
str_contains($abhaId, '@') ? '' : $abhaId
);
$result = $this->connector->pushRecord([
'patient_id' => (string) $patientId,
'patient_name' => $patientName,
'abha_id' => preg_match('/^\d{14}$/', $abhaId) === 1 ? $abhaId : '',
'abha_address' => str_contains($abhaId, '@') ? $abhaId : '',
'year_of_birth' => $patientBirthYear,
'hi_type' => $hiType,
'record_type' => $hiType,
'visit_date' => $visitDate,
@ -5755,7 +5781,66 @@ class AbdmGateway extends BaseController
private function patientDisplayName(array $patientRow): string
{
return trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? ''))) ?: 'Patient';
$first = trim((string) ($patientRow['p_fname'] ?? ''));
$last = trim((string) ($patientRow['p_lname'] ?? ''));
if (in_array(strtolower($last), ['0', '00', 'na', 'n/a', 'null', 'nil', '-'], true)) {
$last = '';
}
$full = trim($first . ' ' . $last);
return $full !== '' ? $full : 'Patient';
}
/**
* Resolve patient birth year required by ABDM link-token flow.
*/
private function resolvePatientBirthYear(array $patientRow, string $abhaAddress = '', string $abhaNumber = ''): int
{
$year = 0;
foreach (['birth_year', 'year_of_birth'] as $field) {
$candidate = preg_replace('/\D/', '', (string) ($patientRow[$field] ?? ''));
$candidate = is_string($candidate) ? (int) $candidate : 0;
if ($candidate >= 1900 && $candidate <= 2200) {
return $candidate;
}
}
foreach (['dob', 'p_dob', 'birth_date', 'date_of_birth'] as $field) {
$raw = trim((string) ($patientRow[$field] ?? ''));
if ($raw === '') {
continue;
}
$ts = strtotime($raw);
if ($ts !== false) {
$candidate = (int) date('Y', $ts);
if ($candidate >= 1900 && $candidate <= 2200) {
return $candidate;
}
}
}
foreach (['age', 'p_age', 'patient_age'] as $field) {
$ageDigits = preg_replace('/\D/', '', (string) ($patientRow[$field] ?? ''));
$age = is_string($ageDigits) ? (int) $ageDigits : 0;
if ($age > 0 && $age < 130) {
$candidate = (int) date('Y') - $age;
if ($candidate >= 1900 && $candidate <= 2200) {
return $candidate;
}
}
}
$source = trim($abhaAddress !== '' ? $abhaAddress : $abhaNumber);
if ($source !== '' && preg_match('/(19\d{2}|20\d{2}|21\d{2}|2200)/', $source, $m) === 1) {
$year = (int) ($m[1] ?? 0);
if ($year >= 1900 && $year <= 2200) {
return $year;
}
}
return 0;
}
private function buildAbdmPatientResource(array $patientRow, int $patientId, string $abhaId): array

View File

@ -1039,13 +1039,80 @@ class Abha extends BaseController
}
if (empty($result['ok']) || $result['ok'] != 1) {
$errorText = '';
foreach ([
$result['error_text'] ?? null,
$result['message'] ?? null,
$result['description'] ?? null,
$result['error'] ?? null,
$result['error']['message'] ?? null,
$result['error']['description'] ?? null,
$result['data']['message'] ?? null,
$result['data']['description'] ?? null,
$result['data']['error'] ?? null,
$result['data']['error']['message'] ?? null,
$result['data']['error']['description'] ?? null,
$result['data']['raw_response'] ?? null,
] as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
$errorText = trim($candidate);
break;
}
}
$requestId = '';
foreach ([
$result['request_id'] ?? null,
$result['data']['request_id'] ?? null,
$result['result']['request_id'] ?? null,
] as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
$requestId = trim($candidate);
break;
}
}
if ($errorText === '') {
$errorText = 'ABHA validation failed';
}
if ($requestId !== '' && stripos($errorText, $requestId) === false) {
$errorText .= ' (Ref: ' . $requestId . ')';
}
if (stripos($errorText, 'Please make a valid request.') !== false) {
$errorText = 'Bridge validation rejected this request. Please contact bridge support with the reference shown. '
. $errorText;
}
return $this->response->setJSON([
'ok' => 0,
'error_text' => $result['error_text'] ?? $result['message']
?? $result['data']['message'] ?? 'ABHA validation failed',
'error_text' => $errorText,
]);
}
return $this->response->setJSON(['ok' => 1, 'status' => (string) ($result['data']['status'] ?? 'UNKNOWN')]);
$statusRaw = '';
foreach ([
$result['status'] ?? null,
$result['validation_status'] ?? null,
$result['data']['status'] ?? null,
$result['data']['validation_status'] ?? null,
$result['result']['status'] ?? null,
$result['data']['result']['status'] ?? null,
] as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
$statusRaw = strtoupper(trim($candidate));
break;
}
}
// Bridge adapters may return ACTIVE/VERIFIED/EXISTS for successful ABHA checks.
$validStates = ['VALID', 'ACTIVE', 'VERIFIED', 'EXISTS', 'FOUND', 'SUCCESS'];
$status = in_array($statusRaw, $validStates, true) || $statusRaw === '' ? 'VALID' : $statusRaw;
return $this->response->setJSON([
'ok' => 1,
'status' => $status,
'raw_status' => $statusRaw,
]);
}
}

View File

@ -492,11 +492,107 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
public function validateAbha(string $abhaId, array $fullPayload = []): array
{
$body = $fullPayload !== [] ? $fullPayload : ['abha_id' => $abhaId];
if ($this->hfrId !== '' && empty($body['hfr_id'])) {
$inputValue = '';
$inputType = 'abha-number';
if ($fullPayload !== []) {
$abhaAddress = trim((string) ($fullPayload['abha_address'] ?? ''));
$abhaNumber = trim((string) ($fullPayload['abha_id'] ?? $abhaId));
if ($abhaAddress !== '') {
$inputType = 'abha-address';
$inputValue = $abhaAddress;
} else {
$digits = preg_replace('/\D/', '', $abhaNumber);
$inputValue = $digits !== '' ? $digits : $abhaNumber;
}
} else {
$abhaRaw = trim((string) $abhaId);
if (str_contains($abhaRaw, '@')) {
$inputType = 'abha-address';
$inputValue = $abhaRaw;
} else {
$digits = preg_replace('/\D/', '', $abhaRaw);
$inputValue = $digits !== '' ? $digits : $abhaRaw;
}
}
$body = [
'type' => $inputType,
'value' => $inputValue,
];
if ($this->hfrId !== '') {
$body['hfr_id'] = $this->hfrId;
}
return $this->post('/v3/abha/validate', $body);
$result = $this->post('/v3/abha/login/search', $body);
// Backward-compat: if a bridge does not yet expose login/search, retry legacy validate.
if (empty($result['ok']) || (int) ($result['ok'] ?? 0) !== 1) {
$httpCode = (int) ($result['http_code'] ?? 0);
if (in_array($httpCode, [404, 405], true)) {
$legacyBody = $inputType === 'abha-address'
? ['abha_address' => $inputValue]
: ['abha_id' => $inputValue];
if ($this->hfrId !== '') {
$legacyBody['hfr_id'] = $this->hfrId;
}
$result = $this->post('/v3/abha/validate', $legacyBody);
}
}
if (empty($result['ok']) || (int) ($result['ok'] ?? 0) !== 1) {
return $result;
}
$data = is_array($result['data'] ?? null) ? $result['data'] : [];
$accounts = is_array($data['accounts'] ?? null) ? $data['accounts'] : [];
$firstAccount = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : [];
$status = '';
foreach ([
$result['status'] ?? null,
$result['account_status'] ?? null,
$data['status'] ?? null,
$data['account_status'] ?? null,
$data['accountStatus'] ?? null,
$data['abhaStatus'] ?? null,
$firstAccount['status'] ?? null,
$firstAccount['accountStatus'] ?? null,
$firstAccount['abhaStatus'] ?? null,
] as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
$status = strtoupper(trim($candidate));
break;
}
}
$authMethods = [];
foreach ([
$data['authMethods'] ?? null,
$data['auth_methods'] ?? null,
$data['availableAuthMethods'] ?? null,
$firstAccount['authMethods'] ?? null,
$firstAccount['auth_methods'] ?? null,
$firstAccount['availableAuthMethods'] ?? null,
] as $candidate) {
if (is_array($candidate) && ! empty($candidate)) {
foreach ($candidate as $method) {
$m = strtoupper(trim((string) $method));
if ($m !== '' && ! in_array($m, $authMethods, true)) {
$authMethods[] = $m;
}
}
}
}
if (! empty($authMethods)) {
$result['auth_methods'] = $authMethods;
}
// HMS callers expect a status-like field; treat successful search as VALID when absent.
$result['status'] = $status !== '' ? $status : 'VALID';
return $result;
}
// -------------------------------------------------------------------------

View File

@ -917,6 +917,24 @@ class M3HiuWorkflowService
$hiuId = trim((string) ($payload['hfr_id'] ?? ''));
}
$istTz = new \DateTimeZone('Asia/Kolkata');
$utcTz = new \DateTimeZone('UTC');
$nowIst = new \DateTimeImmutable('now', $istTz);
$fromUtc = $nowIst
->modify('-365 days')
->setTime(0, 0, 0)
->setTimezone($utcTz)
->format('Y-m-d\TH:i:s.000\Z');
$toUtc = $nowIst
->setTime(23, 59, 59)
->setTimezone($utcTz)
->format('Y-m-d\TH:i:s.000\Z');
$eraseAtUtc = $nowIst
->modify('+365 days')
->setTime(23, 59, 59)
->setTimezone($utcTz)
->format('Y-m-d\TH:i:s.000\Z');
$consent = [
'purpose' => [
'code' => 'CAREMGT',
@ -933,10 +951,10 @@ class M3HiuWorkflowService
'permission' => [
'accessMode' => 'VIEW',
'dateRange' => [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 days')),
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
'from' => $fromUtc,
'to' => $toUtc,
],
'dataEraseAt' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('+365 days')),
'dataEraseAt' => $eraseAtUtc,
'frequency' => [
'unit' => 'HOUR',
'value' => 1,

View File

@ -297,8 +297,14 @@ class AbdmSyncWorkerService
$name = trim((string) ($payload['patient_name'] ?? ''));
if ($name === '') {
$name = trim(trim((string) ($row['p_fname'] ?? '')) . ' ' . trim((string) ($row['p_lname'] ?? '')));
$first = trim((string) ($row['p_fname'] ?? ''));
$last = trim((string) ($row['p_lname'] ?? ''));
if (in_array(strtolower($last), ['0', '00', 'na', 'n/a', 'null', 'nil', '-'], true)) {
$last = '';
}
$name = trim($first . ' ' . $last);
}
$name = preg_replace('/\s0$/', '', $name) ?? $name;
if ($name === '') {
$name = 'PATIENT-' . $localPatientId;
}
@ -351,12 +357,15 @@ class AbdmSyncWorkerService
}
}
$birthYearRaw = trim((string) ($payload['birth_year'] ?? $payload['year_of_birth'] ?? $row['birth_year'] ?? $row['year_of_birth'] ?? ''));
$ageRaw = trim((string) ($payload['age'] ?? $payload['patient_age'] ?? $row['age'] ?? $row['p_age'] ?? ''));
return [
'patient_id' => (string) ($localPatientId > 0 ? $localPatientId : ($payload['local_patient_id'] ?? '')),
'name' => $name,
'mobile' => $mobile,
'gender' => $this->normalizeBridgeGenderCode($genderRaw),
'year_of_birth' => $this->extractBirthYear($dobRaw),
'year_of_birth' => $this->extractBirthYear($dobRaw, $birthYearRaw, $ageRaw, $abhaAddress),
'abha_address' => $abhaAddress,
'abha_number' => $abhaDigits,
];
@ -378,13 +387,38 @@ class AbdmSyncWorkerService
return 'O';
}
private function extractBirthYear(string $dob): int
private function extractBirthYear(string $dob, string $birthYear = '', string $age = '', string $abhaAddress = ''): int
{
$birthYearDigits = preg_replace('/\D/', '', $birthYear);
$birthYearInt = is_string($birthYearDigits) ? (int) $birthYearDigits : 0;
if ($birthYearInt >= 1900 && $birthYearInt <= 2200) {
return $birthYearInt;
}
$dob = trim($dob);
if ($dob !== '') {
$ts = strtotime($dob);
if ($ts !== false) {
return (int) date('Y', $ts);
$year = (int) date('Y', $ts);
if ($year >= 1900 && $year <= 2200) {
return $year;
}
}
}
$ageDigits = preg_replace('/\D/', '', $age);
$ageInt = is_string($ageDigits) ? (int) $ageDigits : 0;
if ($ageInt > 0 && $ageInt < 130) {
$year = (int) date('Y') - $ageInt;
if ($year >= 1900 && $year <= 2200) {
return $year;
}
}
if ($abhaAddress !== '' && preg_match('/(19\d{2}|20\d{2}|21\d{2}|2200)/', $abhaAddress, $match) === 1) {
$year = (int) ($match[1] ?? 0);
if ($year >= 1900 && $year <= 2200) {
return $year;
}
}

View File

@ -285,7 +285,13 @@
$bridgeRecordId = trim((string) ($r['bridge_record_id'] ?? ''));
$hasFhir = (int) ($r['has_fhir'] ?? 0) === 1;
$pushStatusRaw = strtolower(trim((string) ($r['push_status'] ?? '')));
$canSubmitGateway = $hasFhir && ! in_array($pushStatusRaw, ['queued', 'linked'], true);
$canSubmitGateway = $hasFhir;
$submitButtonLabel = in_array($pushStatusRaw, ['queued', 'linked', 'failed'], true)
? 'Submit Again'
: 'Submit to ABDM';
$submitButtonClass = in_array($pushStatusRaw, ['queued', 'linked'], true)
? 'btn-outline-secondary'
: 'btn-outline-warning';
$statusBadgeClass = 'badge bg-' . $statusTone;
if ($statusTone === 'warning') {
$statusBadgeClass .= ' text-dark';
@ -323,10 +329,10 @@
<?php if ($canSubmitGateway): ?>
<button
type="button"
class="btn btn-sm btn-outline-warning btn-opd-consult-submit-gateway ms-1"
class="btn btn-sm <?= esc($submitButtonClass) ?> btn-opd-consult-submit-gateway ms-1"
data-opd-id="<?= (int) ($r['opd_id'] ?? 0) ?>"
data-abha-id="<?= esc((string) ($r['abha_id'] ?? '')) ?>"
>Submit to ABDM</button>
><?= esc($submitButtonLabel) ?></button>
<?php endif; ?>
</td>
</tr>
@ -2441,9 +2447,9 @@
detailsCell.innerHTML = '<div>Submitted to gateway and waiting for link workflow.</div>' + queueHtml + bridgeHtml;
}
}
rowSubmitBtn.disabled = true;
rowSubmitBtn.className = 'btn btn-sm btn-success ms-1';
rowSubmitBtn.textContent = 'Submitted';
rowSubmitBtn.disabled = false;
rowSubmitBtn.className = 'btn btn-sm btn-outline-secondary btn-opd-consult-submit-gateway ms-1';
rowSubmitBtn.textContent = 'Submit Again';
}
var qInfo = res.queue_id ? ' Queue ID: ' + res.queue_id : '';