feat(ibl-registration): order save resilient terhadap kegagalan generate_location
- generate_location dipindah ke luar trans_begin/commit agar order tidak rollback - _do_generate_location_for_stations() diextract, iterasi semua station tanpa abort - deteksi NO_MAPPING (loc_id null) sebelum INSERT - location_warning ditambahkan ke response sukses (opsional untuk FE) - endpoint retry_location untuk generate ulang lokasi yang belum terbentuk - tiap kegagalan station dicatat ke error_log_order Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -455,17 +455,6 @@ class Order extends MY_Controller
|
||||
exit;
|
||||
}
|
||||
|
||||
$fn_generate_location = $this->generate_location($header_id, $userid);
|
||||
if (!$fn_generate_location['status']) {
|
||||
$this->db_smartone->trans_rollback();
|
||||
$message = $fn_generate_location['message'] ?? 'Terjadi kesalahan saat membuat lokasi';
|
||||
if ($internal)
|
||||
return ['status' => 'ERR', 'message' => $message];
|
||||
$this->sys_error($message);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$fn_generate_req = $this->generate_req($header_id, $prm['req'], $userid);
|
||||
if (!$fn_generate_req['status']) {
|
||||
$this->db_smartone->trans_rollback();
|
||||
@@ -611,6 +600,20 @@ class Order extends MY_Controller
|
||||
|
||||
$this->db_smartone->trans_commit();
|
||||
|
||||
$fn_generate_location = $this->generate_location($header_id, $userid);
|
||||
$location_warning = null;
|
||||
if ($fn_generate_location['has_failure']) {
|
||||
$failed_parts = array_map(function ($f) {
|
||||
return $f['name'] . ' (ID:' . $f['id'] . ')';
|
||||
}, $fn_generate_location['failed']);
|
||||
$failed_str = implode(', ', $failed_parts);
|
||||
$location_warning = [
|
||||
'has_error' => true,
|
||||
'message' => "Order tersimpan sebagian. Gagal membuat lokasi: {$failed_str} belum punya mapping m_location aktif. Tindakan: tambahkan mapping lokasi aktif untuk station tersebut lalu retry generate location. OrderID: {$header_id}.",
|
||||
'failed_stations' => $fn_generate_location['failed'],
|
||||
];
|
||||
}
|
||||
|
||||
$sql = "SELECT * FROM `s_menu` WHERE `S_MenuName` = 'Pre-Register' AND `S_MenuIsActive` = 'Y' LIMIT 1";
|
||||
$query_menu = $this->db_smartone->query($sql);
|
||||
if (!$query_menu) {
|
||||
@@ -783,12 +786,13 @@ class Order extends MY_Controller
|
||||
'url_preregister' => $url_preregister,
|
||||
'data' => array('id' => $header_id, 'number' => $lab_number),
|
||||
'order_delivery' => isset($prm['delivery']) ? $prm['delivery'] : $order_delivery,
|
||||
'order_detail' => $order_detail,
|
||||
'order_header' => $order_header,
|
||||
'inform_consent' => isset($order_header['inform_consent']) ? $order_header['inform_consent'] : [],
|
||||
'order_promise' => $order_promise,
|
||||
'order_details' => $order_details,
|
||||
'report_url' => $report_url
|
||||
'order_detail' => $order_detail,
|
||||
'order_header' => $order_header,
|
||||
'inform_consent' => isset($order_header['inform_consent']) ? $order_header['inform_consent'] : [],
|
||||
'order_promise' => $order_promise,
|
||||
'order_details' => $order_details,
|
||||
'report_url' => $report_url,
|
||||
'location_warning' => $location_warning,
|
||||
];
|
||||
|
||||
if ($internal)
|
||||
@@ -879,10 +883,10 @@ class Order extends MY_Controller
|
||||
GROUP BY Group_ResultID, Group_ResultName
|
||||
HAVING Group_ResultID IS NOT NULL AND Group_Concat(T_OrderDetailID) IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT Group_ResultID as Group_ResultID, T_TestName as Group_ResultName, Group_Concat(T_OrderDetailID) as T_OrderDetailIDs, T_TestID as T_TestID
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsResult = 'Y' AND
|
||||
T_TestIsActive = 'Y'
|
||||
SELECT DISTINCT Group_ResultID as Group_ResultID, T_TestName as Group_ResultName, Group_Concat(T_OrderDetailID) as T_OrderDetailIDs, T_TestID as T_TestID
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsResult = 'Y' AND
|
||||
T_TestIsActive = 'Y'
|
||||
JOIN group_resultdetail ON Group_ResultDetailT_TestID = T_TestID AND
|
||||
Group_ResultDetailIsActive = 'Y'
|
||||
JOIN group_result ON Group_ResultDetailGroup_ResultID = Group_ResultID AND
|
||||
@@ -988,8 +992,8 @@ class Order extends MY_Controller
|
||||
return $return;
|
||||
}
|
||||
|
||||
function get_order_header($order_id)
|
||||
{
|
||||
function get_order_header($order_id)
|
||||
{
|
||||
$return = [
|
||||
"status" => true,
|
||||
"message" => "",
|
||||
@@ -1056,97 +1060,97 @@ class Order extends MY_Controller
|
||||
|
||||
}
|
||||
|
||||
$data = $query->row_array();
|
||||
if (is_array($data) && count($data) > 0) {
|
||||
$data['inform_consent'] = $this->get_inform_consent_by_order($order_id);
|
||||
}
|
||||
$return['data'] = $data;
|
||||
return $return;
|
||||
}
|
||||
|
||||
function get_inform_consent_by_order($order_id)
|
||||
{
|
||||
$fallback = $this->get_inform_consent_template('umum');
|
||||
$fallback['company_type_group_name'] = '';
|
||||
$fallback['company_type_group_id'] = 0;
|
||||
|
||||
$sql_group = "SELECT
|
||||
IFNULL(c.M_CompanyM_CompanyGroupTypeID, 0) as company_type_group_id,
|
||||
IFNULL(g.M_CompanyTypeGroupName, '') as company_type_group_name
|
||||
FROM t_orderheader h
|
||||
JOIN m_company c ON h.T_OrderHeaderM_CompanyID = c.M_CompanyID
|
||||
LEFT JOIN m_companytypegroup g ON c.M_CompanyM_CompanyGroupTypeID = g.M_CompanyTypeGroupID
|
||||
WHERE h.T_OrderHeaderID = ?
|
||||
LIMIT 1";
|
||||
$query_group = $this->db_smartone->query($sql_group, [$order_id]);
|
||||
if (!$query_group) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_group = $query_group->row_array();
|
||||
if (!$dt_group) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$group_type_id = intval($dt_group['company_type_group_id']);
|
||||
$template_type = ($group_type_id === 8) ? 'cpmi' : 'umum';
|
||||
|
||||
$sql_template = "SELECT
|
||||
M_InformConsentType as type,
|
||||
M_InformConsentTitle as title,
|
||||
M_InformConsentContent as content
|
||||
FROM m_informconsent
|
||||
WHERE M_InformConsentType = ?
|
||||
AND M_InformConsentIsActive = 'Y'
|
||||
LIMIT 1";
|
||||
$query_template = $this->db_smartone->query($sql_template, [$template_type]);
|
||||
if (!$query_template) {
|
||||
$fallback = $this->get_inform_consent_template($template_type);
|
||||
$fallback['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$fallback['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template = $query_template->row_array();
|
||||
if (!$dt_template) {
|
||||
$fallback = $this->get_inform_consent_template($template_type);
|
||||
$fallback['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$fallback['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$dt_template['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $dt_template;
|
||||
}
|
||||
|
||||
function get_inform_consent_template($template_type)
|
||||
{
|
||||
$fallback = [
|
||||
'type' => $template_type,
|
||||
'title' => '',
|
||||
'content' => ''
|
||||
];
|
||||
|
||||
$sql_template = "SELECT
|
||||
M_InformConsentType as type,
|
||||
M_InformConsentTitle as title,
|
||||
M_InformConsentContent as content
|
||||
FROM m_informconsent
|
||||
WHERE M_InformConsentType = ?
|
||||
AND M_InformConsentIsActive = 'Y'
|
||||
LIMIT 1";
|
||||
$query_template = $this->db_smartone->query($sql_template, [$template_type]);
|
||||
if (!$query_template) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template = $query_template->row_array();
|
||||
if (!$dt_template) {
|
||||
return $fallback;
|
||||
}
|
||||
return $dt_template;
|
||||
}
|
||||
$data = $query->row_array();
|
||||
if (is_array($data) && count($data) > 0) {
|
||||
$data['inform_consent'] = $this->get_inform_consent_by_order($order_id);
|
||||
}
|
||||
$return['data'] = $data;
|
||||
return $return;
|
||||
}
|
||||
|
||||
function get_inform_consent_by_order($order_id)
|
||||
{
|
||||
$fallback = $this->get_inform_consent_template('umum');
|
||||
$fallback['company_type_group_name'] = '';
|
||||
$fallback['company_type_group_id'] = 0;
|
||||
|
||||
$sql_group = "SELECT
|
||||
IFNULL(c.M_CompanyM_CompanyGroupTypeID, 0) as company_type_group_id,
|
||||
IFNULL(g.M_CompanyTypeGroupName, '') as company_type_group_name
|
||||
FROM t_orderheader h
|
||||
JOIN m_company c ON h.T_OrderHeaderM_CompanyID = c.M_CompanyID
|
||||
LEFT JOIN m_companytypegroup g ON c.M_CompanyM_CompanyGroupTypeID = g.M_CompanyTypeGroupID
|
||||
WHERE h.T_OrderHeaderID = ?
|
||||
LIMIT 1";
|
||||
$query_group = $this->db_smartone->query($sql_group, [$order_id]);
|
||||
if (!$query_group) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_group = $query_group->row_array();
|
||||
if (!$dt_group) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$group_type_id = intval($dt_group['company_type_group_id']);
|
||||
$template_type = ($group_type_id === 8) ? 'cpmi' : 'umum';
|
||||
|
||||
$sql_template = "SELECT
|
||||
M_InformConsentType as type,
|
||||
M_InformConsentTitle as title,
|
||||
M_InformConsentContent as content
|
||||
FROM m_informconsent
|
||||
WHERE M_InformConsentType = ?
|
||||
AND M_InformConsentIsActive = 'Y'
|
||||
LIMIT 1";
|
||||
$query_template = $this->db_smartone->query($sql_template, [$template_type]);
|
||||
if (!$query_template) {
|
||||
$fallback = $this->get_inform_consent_template($template_type);
|
||||
$fallback['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$fallback['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template = $query_template->row_array();
|
||||
if (!$dt_template) {
|
||||
$fallback = $this->get_inform_consent_template($template_type);
|
||||
$fallback['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$fallback['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template['company_type_group_name'] = $dt_group['company_type_group_name'];
|
||||
$dt_template['company_type_group_id'] = intval($dt_group['company_type_group_id']);
|
||||
return $dt_template;
|
||||
}
|
||||
|
||||
function get_inform_consent_template($template_type)
|
||||
{
|
||||
$fallback = [
|
||||
'type' => $template_type,
|
||||
'title' => '',
|
||||
'content' => ''
|
||||
];
|
||||
|
||||
$sql_template = "SELECT
|
||||
M_InformConsentType as type,
|
||||
M_InformConsentTitle as title,
|
||||
M_InformConsentContent as content
|
||||
FROM m_informconsent
|
||||
WHERE M_InformConsentType = ?
|
||||
AND M_InformConsentIsActive = 'Y'
|
||||
LIMIT 1";
|
||||
$query_template = $this->db_smartone->query($sql_template, [$template_type]);
|
||||
if (!$query_template) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$dt_template = $query_template->row_array();
|
||||
if (!$dt_template) {
|
||||
return $fallback;
|
||||
}
|
||||
return $dt_template;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2194,84 +2198,100 @@ class Order extends MY_Controller
|
||||
|
||||
function generate_location($header_id, $userid)
|
||||
{
|
||||
$result = [
|
||||
'status' => true,
|
||||
'message' => 'Success',
|
||||
'error_type' => '',
|
||||
'error_detail' => []
|
||||
];
|
||||
|
||||
|
||||
$sql = "SELECT T_SampleStationID,T_SampleStationName
|
||||
$sql = "SELECT T_SampleStationID, T_SampleStationName
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsActive = 'Y'
|
||||
JOIN t_sampletype ON T_TestT_SampleTypeID = T_SampleTypeID
|
||||
JOIN t_bahan ON T_SampleTypeT_BahanID = T_BahanID
|
||||
JOIN t_samplestation ON T_BahanT_SampleStationID = T_SampleStationID
|
||||
WHERE
|
||||
T_OrderDetailT_OrderHeaderID = ?
|
||||
GROUP BY T_SampleStationID ";
|
||||
WHERE T_OrderDetailT_OrderHeaderID = ?
|
||||
GROUP BY T_SampleStationID";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
|
||||
$data_samples = $qry->result_array();
|
||||
foreach ($data_samples as $k => $v) {
|
||||
$sample_id = $v['T_SampleStationID'];
|
||||
$sql = "SELECT M_LocationID as loc_id
|
||||
if (!$qry) {
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_STATIONS_QUERY_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id]
|
||||
);
|
||||
return [
|
||||
'has_failure' => true,
|
||||
'succeeded' => [],
|
||||
'failed' => [['id' => 0, 'name' => 'unknown', 'reason' => 'STATIONS_QUERY_ERROR']],
|
||||
];
|
||||
}
|
||||
|
||||
$stations = $qry->result_array();
|
||||
return $this->_do_generate_location_for_stations($stations, $header_id, $userid);
|
||||
}
|
||||
|
||||
private function _do_generate_location_for_stations(array $stations, $header_id, $userid)
|
||||
{
|
||||
$succeeded = [];
|
||||
$failed = [];
|
||||
|
||||
foreach ($stations as $v) {
|
||||
$sample_id = $v['T_SampleStationID'];
|
||||
$sample_name = $v['T_SampleStationName'];
|
||||
|
||||
$sql = "SELECT M_LocationID as loc_id
|
||||
FROM m_location
|
||||
WHERE M_LocationT_SampleStationID = ? AND M_LocationIsActive = 'Y'
|
||||
WHERE M_LocationT_SampleStationID = ? AND M_LocationIsActive = 'Y'
|
||||
ORDER BY M_LocationPriority DESC, M_LocationID ASC
|
||||
LIMIT 1";
|
||||
$qry = $this->db_smartone->query($sql, $sample_id);
|
||||
|
||||
if (!$qry) {
|
||||
$result['status'] = false;
|
||||
$result['message'] = 'Terjadi kesalahan saat mengambil data lokasi ' . $v['T_SampleStationName'];
|
||||
$result['error_type'] = 'LOCATION_QUERY_ERROR';
|
||||
$result['error_detail'] = [
|
||||
'sql_error' => $this->db_smartone->error(),
|
||||
'last_query' => $this->db_smartone->last_query()
|
||||
];
|
||||
$prm_log = ['SELECT_LOCATION', 'order/generate_location'];
|
||||
$xsql = $this->db_smartone->last_query();
|
||||
$log_error = $this->insert_log_error($xsql, $prm_log);
|
||||
//$this->db_smartone->trans_rollback();
|
||||
return $result;
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'QUERY_ERROR'];
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_QUERY_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$loc_id = $qry->row()->loc_id;
|
||||
|
||||
$row = $qry->row();
|
||||
if (!$row) {
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'NO_MAPPING'];
|
||||
$this->insert_log_error(
|
||||
'no active m_location for station ' . $sample_id,
|
||||
['LOCATION_NO_MAPPING', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$loc_id = $row->loc_id;
|
||||
|
||||
$sql = "INSERT INTO t_order_location(
|
||||
T_OrderLocationT_OrderHeaderID,
|
||||
T_OrderLocationT_OrderHeaderID,
|
||||
T_OrderLocationM_LocationID,
|
||||
T_OrderLocationT_SampleStationID,
|
||||
T_OrderLocationCreated,
|
||||
T_OrderLocationLastUpdated,
|
||||
T_OrderLocationCreated,
|
||||
T_OrderLocationLastUpdated,
|
||||
T_OrderLocationUserID)
|
||||
VALUES (?,?,?,NOW(),NOW(),?)";
|
||||
$prm_orderlocation = [
|
||||
$header_id,
|
||||
$loc_id,
|
||||
$sample_id,
|
||||
$userid
|
||||
];
|
||||
$qry = $this->db_smartone->query($sql, $prm_orderlocation);
|
||||
$qry = $this->db_smartone->query($sql, [$header_id, $loc_id, $sample_id, $userid]);
|
||||
|
||||
if (!$qry) {
|
||||
$result['status'] = false;
|
||||
$result['message'] = 'Terjadi kesalahan saat menyimpan data lokasi';
|
||||
$result['error_type'] = 'ORDER_LOCATION_INSERT_ERROR';
|
||||
$result['error_detail'] = [
|
||||
'sql_error' => $this->db_smartone->error(),
|
||||
'last_query' => $this->db_smartone->last_query()
|
||||
];
|
||||
$prm_log = ['INSERT_T_ORDERLOCATION', 'order/generate_location'];
|
||||
$xsql = $this->db_smartone->last_query();
|
||||
$log_error = $this->insert_log_error($xsql, $prm_log);
|
||||
//$this->db_smartone->trans_rollback();
|
||||
return $result;
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'INSERT_ERROR'];
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_INSERT_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
//file_get_contents("http://127.0.0.1:9088/broadcast/sm.new." . $sample_id . "." . $mcuid . "." . $branch_id);
|
||||
$succeeded[] = ['id' => $sample_id, 'name' => $sample_name];
|
||||
}
|
||||
|
||||
return $result;
|
||||
return [
|
||||
'has_failure' => count($failed) > 0,
|
||||
'succeeded' => $succeeded,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
|
||||
function generate_req($header_id, $req, $userid)
|
||||
@@ -4463,4 +4483,75 @@ GROUP BY T_SampleStationID ";
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
function retry_location()
|
||||
{
|
||||
if (!$this->isLogin) {
|
||||
$this->sys_error("Invalid Token");
|
||||
exit;
|
||||
}
|
||||
|
||||
$prm = $this->sys_input;
|
||||
$header_id = isset($prm['order_id']) ? (int) $prm['order_id'] : 0;
|
||||
$userid = $this->sys_user["M_UserID"];
|
||||
|
||||
if (!$header_id) {
|
||||
$this->sys_error("order_id tidak boleh kosong");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ambil semua station terkait order
|
||||
$sql = "SELECT T_SampleStationID, T_SampleStationName
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsActive = 'Y'
|
||||
JOIN t_sampletype ON T_TestT_SampleTypeID = T_SampleTypeID
|
||||
JOIN t_bahan ON T_SampleTypeT_BahanID = T_BahanID
|
||||
JOIN t_samplestation ON T_BahanT_SampleStationID = T_SampleStationID
|
||||
WHERE T_OrderDetailT_OrderHeaderID = ?
|
||||
GROUP BY T_SampleStationID";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
if (!$qry) {
|
||||
$this->sys_error("Gagal mengambil data station untuk order " . $header_id);
|
||||
exit;
|
||||
}
|
||||
$all_stations = $qry->result_array();
|
||||
|
||||
// Ambil station yang sudah punya lokasi
|
||||
$sql = "SELECT T_OrderLocationT_SampleStationID as station_id
|
||||
FROM t_order_location
|
||||
WHERE T_OrderLocationT_OrderHeaderID = ?";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
if (!$qry) {
|
||||
$this->sys_error("Gagal mengambil data lokasi existing untuk order " . $header_id);
|
||||
exit;
|
||||
}
|
||||
$existing_ids = array_column($qry->result_array(), 'station_id');
|
||||
|
||||
// Hanya proses station yang belum punya lokasi
|
||||
$pending = array_values(array_filter($all_stations, function ($s) use ($existing_ids) {
|
||||
return !in_array($s['T_SampleStationID'], $existing_ids);
|
||||
}));
|
||||
|
||||
if (empty($pending)) {
|
||||
$this->sys_ok([
|
||||
'order_id' => $header_id,
|
||||
'succeeded' => [],
|
||||
'failed' => [],
|
||||
'message' => 'Semua lokasi sudah terbentuk untuk order ini.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $this->_do_generate_location_for_stations($pending, $header_id, $userid);
|
||||
$succeeded_count = count($result['succeeded']);
|
||||
$failed_count = count($result['failed']);
|
||||
|
||||
$this->sys_ok([
|
||||
'order_id' => $header_id,
|
||||
'succeeded' => $result['succeeded'],
|
||||
'failed' => $result['failed'],
|
||||
'message' => "Retry selesai. {$succeeded_count} berhasil, {$failed_count} gagal.",
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
# IBL Registration Order — Location Generation Resilience Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Order header/detail/sample tetap tersimpan walau `generate_location` gagal; lokasi di-generate per-station (tidak abort di kegagalan pertama); error spesifik & actionable; endpoint retry tersedia.
|
||||
|
||||
**Architecture:** Pindahkan `generate_location()` ke luar `trans_begin/commit` agar order tidak ikut rollback saat lokasi gagal. Extract core insert-location logic ke private method `_do_generate_location_for_stations()` yang dipakai bersama oleh `generate_location()` dan endpoint `retry_location()`. Tambahkan field opsional `location_warning` di response sukses.
|
||||
|
||||
**Tech Stack:** PHP 7.x, CodeIgniter 3, MySQL. Tidak ada test runner — verifikasi manual via curl/Postman sesuai skenario UAT.
|
||||
|
||||
---
|
||||
|
||||
## File yang Diubah
|
||||
|
||||
| File | Tipe | Keterangan |
|
||||
|---|---|---|
|
||||
| `application/controllers/mockup/fo/ibl_registration/Order.php` | Modify | Satu-satunya file yang disentuh |
|
||||
|
||||
Semua perubahan ada di file ini:
|
||||
- Fungsi `save()` — ubah urutan pemanggilan, tambah `location_warning` di response
|
||||
- Fungsi `generate_location()` — delegasi ke private method baru
|
||||
- **Baru** private `_do_generate_location_for_stations()` — logika insert per-station
|
||||
- **Baru** public `retry_location()` — endpoint retry
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Extract `_do_generate_location_for_stations()`
|
||||
|
||||
Ini fondasi semua perubahan. Semua logika insert location per-station dipindah ke sini.
|
||||
|
||||
**Files:**
|
||||
- Modify: `application/controllers/mockup/fo/ibl_registration/Order.php` (tambah setelah line 2275)
|
||||
|
||||
- [ ] **Step 1.1 — Verifikasi titik sisip**
|
||||
|
||||
Buka file, konfirmasi line 2275 adalah baris `}` penutup `generate_location()` dan line 2277 adalah baris pertama `generate_req()`.
|
||||
|
||||
```bash
|
||||
sed -n '2273,2280p' /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
return $result;
|
||||
}
|
||||
|
||||
function generate_req($header_id, $req, $userid)
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2 — Sisipkan private method baru setelah baris penutup `generate_location()`**
|
||||
|
||||
Sisipkan blok berikut tepat setelah `}` penutup `generate_location()` (antara line 2275 dan 2277):
|
||||
|
||||
```php
|
||||
private function _do_generate_location_for_stations(array $stations, $header_id, $userid)
|
||||
{
|
||||
$succeeded = [];
|
||||
$failed = [];
|
||||
|
||||
foreach ($stations as $v) {
|
||||
$sample_id = $v['T_SampleStationID'];
|
||||
$sample_name = $v['T_SampleStationName'];
|
||||
|
||||
$sql = "SELECT M_LocationID as loc_id
|
||||
FROM m_location
|
||||
WHERE M_LocationT_SampleStationID = ? AND M_LocationIsActive = 'Y'
|
||||
ORDER BY M_LocationPriority DESC, M_LocationID ASC
|
||||
LIMIT 1";
|
||||
$qry = $this->db_smartone->query($sql, $sample_id);
|
||||
|
||||
if (!$qry) {
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'QUERY_ERROR'];
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_QUERY_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $qry->row();
|
||||
if (!$row) {
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'NO_MAPPING'];
|
||||
$this->insert_log_error(
|
||||
'no active m_location for station ' . $sample_id,
|
||||
['LOCATION_NO_MAPPING', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$loc_id = $row->loc_id;
|
||||
|
||||
$sql = "INSERT INTO t_order_location(
|
||||
T_OrderLocationT_OrderHeaderID,
|
||||
T_OrderLocationM_LocationID,
|
||||
T_OrderLocationT_SampleStationID,
|
||||
T_OrderLocationCreated,
|
||||
T_OrderLocationLastUpdated,
|
||||
T_OrderLocationUserID)
|
||||
VALUES (?,?,?,NOW(),NOW(),?)";
|
||||
$qry = $this->db_smartone->query($sql, [$header_id, $loc_id, $sample_id, $userid]);
|
||||
|
||||
if (!$qry) {
|
||||
$failed[] = ['id' => $sample_id, 'name' => $sample_name, 'reason' => 'INSERT_ERROR'];
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_INSERT_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id, 'station_id' => $sample_id, 'station_name' => $sample_name]
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$succeeded[] = ['id' => $sample_id, 'name' => $sample_name];
|
||||
}
|
||||
|
||||
return [
|
||||
'has_failure' => count($failed) > 0,
|
||||
'succeeded' => $succeeded,
|
||||
'failed' => $failed,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1.3 — Verifikasi method tersisip tanpa syntax error**
|
||||
|
||||
```bash
|
||||
php -l /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Expected: `No syntax errors detected in ...Order.php`
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Refactor `generate_location()` — delegasi ke private method
|
||||
|
||||
Ganti seluruh isi `generate_location()` agar hanya bertugas query stations lalu memanggil `_do_generate_location_for_stations()`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `Order.php` line 2195–2275
|
||||
|
||||
- [ ] **Step 2.1 — Catat baris pertama dan terakhir `generate_location()`**
|
||||
|
||||
```bash
|
||||
sed -n '2195,2275p' /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Pastikan range 2195–2275 masih sesuai (bisa bergeser setelah Task 1).
|
||||
|
||||
- [ ] **Step 2.2 — Ganti isi `generate_location()`**
|
||||
|
||||
Ganti seluruh function body `generate_location` (dari baris `function generate_location` sampai `}` penutupnya) dengan:
|
||||
|
||||
```php
|
||||
function generate_location($header_id, $userid)
|
||||
{
|
||||
$sql = "SELECT T_SampleStationID, T_SampleStationName
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsActive = 'Y'
|
||||
JOIN t_sampletype ON T_TestT_SampleTypeID = T_SampleTypeID
|
||||
JOIN t_bahan ON T_SampleTypeT_BahanID = T_BahanID
|
||||
JOIN t_samplestation ON T_BahanT_SampleStationID = T_SampleStationID
|
||||
WHERE T_OrderDetailT_OrderHeaderID = ?
|
||||
GROUP BY T_SampleStationID";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
|
||||
if (!$qry) {
|
||||
$this->insert_log_error(
|
||||
$this->db_smartone->last_query(),
|
||||
['LOCATION_STATIONS_QUERY_ERROR', 'order/generate_location'],
|
||||
['order_id' => $header_id]
|
||||
);
|
||||
return [
|
||||
'has_failure' => true,
|
||||
'succeeded' => [],
|
||||
'failed' => [['id' => 0, 'name' => 'unknown', 'reason' => 'STATIONS_QUERY_ERROR']],
|
||||
];
|
||||
}
|
||||
|
||||
$stations = $qry->result_array();
|
||||
return $this->_do_generate_location_for_stations($stations, $header_id, $userid);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2.3 — Syntax check**
|
||||
|
||||
```bash
|
||||
php -l /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Expected: `No syntax errors detected`
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Restructure `save()` — pindah `generate_location` ke luar transaksi
|
||||
|
||||
**Files:**
|
||||
- Modify: `Order.php` — dua titik di dalam function `save()`
|
||||
|
||||
- [ ] **Step 3.1 — Hapus blok `generate_location` dari dalam transaksi (line 458–466)**
|
||||
|
||||
Hapus 9 baris berikut (termasuk baris kosong sesudahnya):
|
||||
|
||||
```php
|
||||
$fn_generate_location = $this->generate_location($header_id, $userid);
|
||||
if (!$fn_generate_location['status']) {
|
||||
$this->db_smartone->trans_rollback();
|
||||
$message = $fn_generate_location['message'] ?? 'Terjadi kesalahan saat membuat lokasi';
|
||||
if ($internal)
|
||||
return ['status' => 'ERR', 'message' => $message];
|
||||
$this->sys_error($message);
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2 — Tambahkan blok `generate_location` setelah `trans_commit()` (line 612)**
|
||||
|
||||
Setelah baris `$this->db_smartone->trans_commit();`, sisipkan:
|
||||
|
||||
```php
|
||||
$fn_generate_location = $this->generate_location($header_id, $userid);
|
||||
$location_warning = null;
|
||||
if ($fn_generate_location['has_failure']) {
|
||||
$failed_parts = array_map(function ($f) {
|
||||
return $f['name'] . ' (ID:' . $f['id'] . ')';
|
||||
}, $fn_generate_location['failed']);
|
||||
$failed_str = implode(', ', $failed_parts);
|
||||
$location_warning = [
|
||||
'has_error' => true,
|
||||
'message' => "Order tersimpan sebagian. Gagal membuat lokasi: {$failed_str} belum punya mapping m_location aktif. Tindakan: tambahkan mapping lokasi aktif untuk station tersebut lalu retry generate location. OrderID: {$header_id}.",
|
||||
'failed_stations' => $fn_generate_location['failed'],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3 — Tambahkan `location_warning` ke array `$dt_order`**
|
||||
|
||||
Di array `$dt_order` (sekitar line 772), tambahkan satu entry di akhir (sebelum `]`):
|
||||
|
||||
```php
|
||||
'report_url' => $report_url,
|
||||
'location_warning' => $location_warning,
|
||||
```
|
||||
|
||||
Sehingga `$dt_order` berakhir:
|
||||
```php
|
||||
$dt_order = [
|
||||
'status' => 'OK',
|
||||
'order_id' => $x_data_array['T_OrderHeaderID'],
|
||||
// ... field lainnya tidak berubah ...
|
||||
'report_url' => $report_url,
|
||||
'location_warning' => $location_warning,
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3.4 — Syntax check**
|
||||
|
||||
```bash
|
||||
php -l /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Expected: `No syntax errors detected`
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Tambah endpoint `retry_location()`
|
||||
|
||||
**Files:**
|
||||
- Modify: `Order.php` — tambah method baru sebelum penutup class (cari baris `}` paling akhir file)
|
||||
|
||||
- [ ] **Step 4.1 — Temukan baris penutup class**
|
||||
|
||||
```bash
|
||||
tail -5 /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Catat nomor baris `}` penutup class.
|
||||
|
||||
- [ ] **Step 4.2 — Sisipkan method `retry_location()` sebelum `}` penutup class**
|
||||
|
||||
```php
|
||||
function retry_location()
|
||||
{
|
||||
if (!$this->isLogin) {
|
||||
$this->sys_error("Invalid Token");
|
||||
exit;
|
||||
}
|
||||
|
||||
$prm = $this->sys_input;
|
||||
$header_id = isset($prm['order_id']) ? (int) $prm['order_id'] : 0;
|
||||
$userid = $this->sys_user["M_UserID"];
|
||||
|
||||
if (!$header_id) {
|
||||
$this->sys_error("order_id tidak boleh kosong");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ambil semua station terkait order
|
||||
$sql = "SELECT T_SampleStationID, T_SampleStationName
|
||||
FROM t_orderdetail
|
||||
JOIN t_test ON T_OrderDetailT_TestID = T_TestID AND T_TestIsActive = 'Y'
|
||||
JOIN t_sampletype ON T_TestT_SampleTypeID = T_SampleTypeID
|
||||
JOIN t_bahan ON T_SampleTypeT_BahanID = T_BahanID
|
||||
JOIN t_samplestation ON T_BahanT_SampleStationID = T_SampleStationID
|
||||
WHERE T_OrderDetailT_OrderHeaderID = ?
|
||||
GROUP BY T_SampleStationID";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
if (!$qry) {
|
||||
$this->sys_error("Gagal mengambil data station untuk order " . $header_id);
|
||||
exit;
|
||||
}
|
||||
$all_stations = $qry->result_array();
|
||||
|
||||
// Ambil station yang sudah punya lokasi
|
||||
$sql = "SELECT T_OrderLocationT_SampleStationID as station_id
|
||||
FROM t_order_location
|
||||
WHERE T_OrderLocationT_OrderHeaderID = ?";
|
||||
$qry = $this->db_smartone->query($sql, $header_id);
|
||||
if (!$qry) {
|
||||
$this->sys_error("Gagal mengambil data lokasi existing untuk order " . $header_id);
|
||||
exit;
|
||||
}
|
||||
$existing_ids = array_column($qry->result_array(), 'station_id');
|
||||
|
||||
// Hanya proses station yang belum punya lokasi
|
||||
$pending = array_values(array_filter($all_stations, function ($s) use ($existing_ids) {
|
||||
return !in_array($s['T_SampleStationID'], $existing_ids);
|
||||
}));
|
||||
|
||||
if (empty($pending)) {
|
||||
$this->sys_ok([
|
||||
'order_id' => $header_id,
|
||||
'succeeded' => [],
|
||||
'failed' => [],
|
||||
'message' => 'Semua lokasi sudah terbentuk untuk order ini.',
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $this->_do_generate_location_for_stations($pending, $header_id, $userid);
|
||||
$succeeded_count = count($result['succeeded']);
|
||||
$failed_count = count($result['failed']);
|
||||
|
||||
$this->sys_ok([
|
||||
'order_id' => $header_id,
|
||||
'succeeded' => $result['succeeded'],
|
||||
'failed' => $result['failed'],
|
||||
'message' => "Retry selesai. {$succeeded_count} berhasil, {$failed_count} gagal.",
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3 — Syntax check**
|
||||
|
||||
```bash
|
||||
php -l /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab/application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
```
|
||||
|
||||
Expected: `No syntax errors detected`
|
||||
|
||||
- [ ] **Step 4.4 — Commit semua perubahan**
|
||||
|
||||
```bash
|
||||
cd /Users/fajrihardhitamurti/REPO_GITEA_IBL/BE_IBL/one-api-lab
|
||||
git add application/controllers/mockup/fo/ibl_registration/Order.php
|
||||
git commit -m "feat(ibl-registration): order save resilient terhadap kegagalan generate_location
|
||||
|
||||
- generate_location dipindah ke luar trans_begin/commit agar order tidak rollback
|
||||
- _do_generate_location_for_stations() diextract, iterasi semua station tanpa abort
|
||||
- deteksi NO_MAPPING (loc_id null) sebelum INSERT
|
||||
- location_warning ditambahkan ke response sukses (opsional untuk FE)
|
||||
- endpoint retry_location untuk generate ulang lokasi yang belum terbentuk
|
||||
- tiap kegagalan station dicatat ke error_log_order"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: UAT Manual — 3 Skenario
|
||||
|
||||
Untuk setiap skenario, ganti `<TOKEN>`, `<HOST>`, dan payload sesuai environment dev.
|
||||
|
||||
### Skenario 1 — Happy Path (semua mapping lengkap)
|
||||
|
||||
- [ ] **Step 5.1 — Pastikan semua sample station pada order yang akan dites punya entry aktif di `m_location`**
|
||||
|
||||
```sql
|
||||
-- Jalankan di DB dev, ganti $order_detail_test sesuai test data
|
||||
SELECT T_SampleStationID, T_SampleStationName,
|
||||
M_LocationID, M_LocationIsActive
|
||||
FROM t_samplestation
|
||||
LEFT JOIN m_location ON M_LocationT_SampleStationID = T_SampleStationID
|
||||
AND M_LocationIsActive = 'Y'
|
||||
WHERE T_SampleStationID IN (/* station ID yang akan dipakai test */);
|
||||
```
|
||||
|
||||
Expected: setiap station punya minimal 1 baris dengan `M_LocationIsActive = 'Y'`.
|
||||
|
||||
- [ ] **Step 5.2 — Hit endpoint save**
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://<HOST>/one-api-lab/mockup/fo/ibl_registration/order/save \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '<payload lengkap dengan header/detail/sample>' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"data": {
|
||||
"status": "OK",
|
||||
"order_id": <number>,
|
||||
"noreg": "<string>",
|
||||
"location_warning": null,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5.3 — Verifikasi `t_order_location` terisi**
|
||||
|
||||
```sql
|
||||
SELECT * FROM t_order_location WHERE T_OrderLocationT_OrderHeaderID = <order_id_dari_response>;
|
||||
```
|
||||
|
||||
Expected: ada row untuk setiap sample station order tersebut.
|
||||
|
||||
---
|
||||
|
||||
### Skenario 2 — Partial Failure (satu station tanpa mapping)
|
||||
|
||||
- [ ] **Step 5.4 — Nonaktifkan mapping salah satu station (DEV only)**
|
||||
|
||||
```sql
|
||||
-- Catat station ID yang akan dinonaktifkan
|
||||
UPDATE m_location
|
||||
SET M_LocationIsActive = 'N'
|
||||
WHERE M_LocationT_SampleStationID = <station_id_test>;
|
||||
```
|
||||
|
||||
- [ ] **Step 5.5 — Hit endpoint save dengan order yang mencakup station tersebut**
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://<HOST>/one-api-lab/mockup/fo/ibl_registration/order/save \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '<payload>' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"data": {
|
||||
"status": "OK",
|
||||
"order_id": <number>,
|
||||
"location_warning": {
|
||||
"has_error": true,
|
||||
"message": "Order tersimpan sebagian. Gagal membuat lokasi: <NamaStation> (ID:<id>) belum punya mapping m_location aktif. Tindakan: ...",
|
||||
"failed_stations": [
|
||||
{"id": <station_id>, "name": "<nama>", "reason": "NO_MAPPING"}
|
||||
]
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5.6 — Verifikasi order tersimpan di DB**
|
||||
|
||||
```sql
|
||||
SELECT T_OrderHeaderID, T_OrderHeaderLabNumber
|
||||
FROM t_orderheader WHERE T_OrderHeaderID = <order_id>;
|
||||
```
|
||||
|
||||
Expected: ada 1 row — order tersimpan meski lokasi gagal.
|
||||
|
||||
- [ ] **Step 5.7 — Verifikasi station lain tetap punya lokasi**
|
||||
|
||||
```sql
|
||||
SELECT T_OrderLocationT_SampleStationID
|
||||
FROM t_order_location
|
||||
WHERE T_OrderLocationT_OrderHeaderID = <order_id>;
|
||||
```
|
||||
|
||||
Expected: ada row untuk station yang mapping-nya aktif, tidak ada row untuk station yang dinonaktifkan.
|
||||
|
||||
- [ ] **Step 5.8 — Verifikasi error tercatat di `error_log_order`**
|
||||
|
||||
```sql
|
||||
SELECT * FROM error_log_order
|
||||
WHERE ErrorLogOrderCode = 'LOCATION_NO_MAPPING'
|
||||
ORDER BY ErrorLogOrderCreated DESC LIMIT 5;
|
||||
```
|
||||
|
||||
Expected: ada entry baru dengan `ErrorLogOrderData` berisi order_id dan station_id yang gagal.
|
||||
|
||||
---
|
||||
|
||||
### Skenario 3 — Retry sukses setelah mapping diperbaiki
|
||||
|
||||
- [ ] **Step 5.9 — Aktifkan kembali mapping yang dinonaktifkan di Step 5.4**
|
||||
|
||||
```sql
|
||||
UPDATE m_location
|
||||
SET M_LocationIsActive = 'Y'
|
||||
WHERE M_LocationT_SampleStationID = <station_id_test>;
|
||||
```
|
||||
|
||||
- [ ] **Step 5.10 — Hit endpoint `retry_location`**
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://<HOST>/one-api-lab/mockup/fo/ibl_registration/order/retry_location \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"order_id": <order_id_dari_skenario_2>}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"data": {
|
||||
"order_id": <number>,
|
||||
"succeeded": [{"id": <station_id>, "name": "<nama>"}],
|
||||
"failed": [],
|
||||
"message": "Retry selesai. 1 berhasil, 0 gagal."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5.11 — Verifikasi `t_order_location` kini lengkap**
|
||||
|
||||
```sql
|
||||
SELECT T_OrderLocationT_SampleStationID, T_OrderLocationM_LocationID
|
||||
FROM t_order_location
|
||||
WHERE T_OrderLocationT_OrderHeaderID = <order_id>;
|
||||
```
|
||||
|
||||
Expected: ada row untuk semua station, termasuk yang sebelumnya gagal.
|
||||
|
||||
- [ ] **Step 5.12 — Hit retry_location lagi (idempotency check)**
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://<HOST>/one-api-lab/mockup/fo/ibl_registration/order/retry_location \
|
||||
-H "Authorization: Bearer <TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"order_id": <order_id>}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Expected:
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"data": {
|
||||
"message": "Semua lokasi sudah terbentuk untuk order ini.",
|
||||
"succeeded": [],
|
||||
"failed": []
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
# Design: IBL Registration Order — Location Generation Resilience
|
||||
|
||||
**Date:** 2026-05-19
|
||||
**Scope:** DEV only. No production changes.
|
||||
**File:** `application/controllers/mockup/fo/ibl_registration/Order.php`
|
||||
**Endpoint:** `POST /one-api-lab/mockup/fo/ibl_registration/order/save`
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Saat ini jika `generate_location()` gagal (mapping `m_location` aktif tidak ditemukan untuk suatu sample station), seluruh order — termasuk header, detail, dan sample yang sudah valid — ikut di-rollback. Response ke FE berupa error generik:
|
||||
|
||||
> "Terjadi kesalahan saat menyimpan data lokasi"
|
||||
|
||||
Akibatnya pelayanan stuck dan operator tidak tahu tindakan apa yang harus dilakukan.
|
||||
|
||||
Ada juga silent bug: jika `m_location` tidak ditemukan, `$qry->row()->loc_id` bernilai null, dan INSERT tetap dijalankan dengan `loc_id = null` — bukan ditangkap sebagai "mapping tidak ada".
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Order utama (header/detail/sample) tetap tersimpan walau `generate_location` gagal.
|
||||
2. Jika hanya sebagian station gagal, station lain tetap di-generate lokasinya.
|
||||
3. Error message dibuat spesifik dan actionable — dicatat di backend log dan dikirim ke FE via field opsional `location_warning`.
|
||||
4. Tidak ada perubahan wajib di FE (field `location_warning` opsional, diabaikan FE saat ini).
|
||||
5. Endpoint retry tersedia untuk dev agar ops bisa generate ulang lokasi yang belum terbentuk.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Perubahan Struktur Transaksi di `save()`
|
||||
|
||||
**Sebelum:**
|
||||
```
|
||||
trans_begin()
|
||||
save_order_header()
|
||||
save_order_detail()
|
||||
generate_sample_lab()
|
||||
generate_location() ← di dalam transaksi, gagal = rollback semua
|
||||
generate_req()
|
||||
update_preregister_promise()
|
||||
save_delivery()
|
||||
trans_commit()
|
||||
```
|
||||
|
||||
**Sesudah:**
|
||||
```
|
||||
trans_begin()
|
||||
save_order_header()
|
||||
save_order_detail()
|
||||
generate_sample_lab()
|
||||
generate_req()
|
||||
update_preregister_promise()
|
||||
save_delivery()
|
||||
trans_commit() ← order tersimpan permanen di sini
|
||||
|
||||
generate_location() ← di luar transaksi, mini-transaction sendiri
|
||||
→ sukses semua : location_warning = null
|
||||
→ sebagian gagal: log error, location_warning = {has_error, message, failed_stations}
|
||||
→ semua gagal : log error, location_warning = {has_error, message, failed_stations}
|
||||
|
||||
sys_ok($dt_order) ← selalu OK, location_warning opsional
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Changes
|
||||
|
||||
### A. `save()` (line 423–466)
|
||||
|
||||
1. Hapus `$fn_generate_location` dari dalam blok transaksi.
|
||||
2. Setelah `trans_commit()`, panggil `generate_location($header_id, $userid)`.
|
||||
3. Evaluasi result:
|
||||
- Jika `$fn_generate_location['has_failure']` true → jalankan `insert_log_error()` untuk tiap station gagal, set `$location_warning`.
|
||||
- Jika semua sukses → `$location_warning = null`.
|
||||
4. Tambahkan `'location_warning' => $location_warning` ke array `$dt_order`.
|
||||
|
||||
### B. `generate_location($header_id, $userid)`
|
||||
|
||||
Ubah logic loop station:
|
||||
|
||||
- **Check null mapping:** Setelah query `m_location`, cek apakah `$qry->row()` null. Jika null → tambah ke `$failed[]` dengan `reason = 'NO_MAPPING'`, **lanjut** ke station berikutnya (jangan insert).
|
||||
- **Check query error:** Jika `$qry === false` → tambah ke `$failed[]` dengan `reason = 'QUERY_ERROR'`, lanjut.
|
||||
- **Check insert error:** Jika INSERT gagal → tambah ke `$failed[]` dengan `reason = 'INSERT_ERROR'`, lanjut.
|
||||
- **Sukses:** Tambah ke `$succeeded[]`.
|
||||
|
||||
Return structure:
|
||||
```php
|
||||
[
|
||||
'has_failure' => bool,
|
||||
'succeeded' => [['id' => int, 'name' => str], ...],
|
||||
'failed' => [['id' => int, 'name' => str, 'reason' => str], ...],
|
||||
]
|
||||
```
|
||||
|
||||
Format pesan kegagalan (dibangun di `save()` setelah generate_location):
|
||||
```
|
||||
"Order tersimpan sebagian. Gagal membuat lokasi: {NamaStation} (ID:{id}) belum punya
|
||||
mapping m_location aktif. Tindakan: tambahkan mapping lokasi aktif untuk station
|
||||
tersebut lalu retry generate location. OrderID: {header_id}."
|
||||
```
|
||||
|
||||
### C. Logging Backend
|
||||
|
||||
Untuk tiap station gagal, panggil `insert_log_error()` ke tabel `error_log_order`:
|
||||
- `ErrorLogOrderCode`: `'LOCATION_NO_MAPPING'` / `'LOCATION_QUERY_ERROR'` / `'LOCATION_INSERT_ERROR'`
|
||||
- `ErrorLogOrderFnName`: `'order/generate_location'`
|
||||
- `ErrorLogOrderDescription`: SQL atau keterangan
|
||||
- `ErrorLogOrderData`: `json_encode(['order_id' => $header_id, 'station_id' => $id, 'station_name' => $name])`
|
||||
|
||||
### D. Field `location_warning` di Response
|
||||
|
||||
Ditambahkan ke `$dt_order` (di dalam `data`). FE saat ini mengabaikannya; tersedia untuk update FE berikutnya.
|
||||
|
||||
```json
|
||||
// Jika ada kegagalan:
|
||||
"location_warning": {
|
||||
"has_error": true,
|
||||
"message": "Order tersimpan sebagian. Gagal membuat lokasi: ...",
|
||||
"failed_stations": [
|
||||
{"id": 25, "name": "Dental/Panoramic", "reason": "NO_MAPPING"}
|
||||
]
|
||||
}
|
||||
|
||||
// Jika semua sukses:
|
||||
"location_warning": null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Endpoint: `retry_location`
|
||||
|
||||
**Route:** `POST /mockup/fo/ibl_registration/order/retry_location`
|
||||
**Method:** `retry_location()` di class `Order`
|
||||
|
||||
### Input
|
||||
```json
|
||||
{ "order_id": 123456 }
|
||||
```
|
||||
|
||||
### Flow
|
||||
1. Validasi login (`isLogin`).
|
||||
2. Query semua `T_SampleStationID` terkait order dari `t_orderdetail` (join chain yang sama dengan `generate_location`).
|
||||
3. Filter: ambil hanya station yang **belum ada** di `t_order_location` untuk order tersebut.
|
||||
4. Jalankan logic generate location (private method `_do_generate_location_for_stations($stations, $header_id, $userid)`) — sama dengan yang dipakai `generate_location`.
|
||||
5. Return summary.
|
||||
|
||||
### Output
|
||||
```json
|
||||
{
|
||||
"status": "OK",
|
||||
"data": {
|
||||
"order_id": 123456,
|
||||
"succeeded": [{"id": 10, "name": "Lab Utama"}],
|
||||
"failed": [{"id": 25, "name": "Dental/Panoramic", "reason": "NO_MAPPING"}],
|
||||
"message": "Retry selesai. 1 berhasil, 1 gagal."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Refactor Internal
|
||||
|
||||
Extract logic insert location per-station ke private method `_do_generate_location_for_stations(array $stations, int $header_id, int $userid): array` — dipakai oleh `generate_location()` dan `retry_location()`.
|
||||
|
||||
---
|
||||
|
||||
## UAT Scenarios (DEV)
|
||||
|
||||
| Skenario | Kondisi | Expected Result |
|
||||
|---|---|---|
|
||||
| 1. Happy path | Semua station punya mapping aktif | `status: OK`, `location_warning: null` |
|
||||
| 2. Partial failure | 1 station tanpa mapping aktif | `status: OK`, `location_warning.has_error: true`, station lain tetap tersimpan di `t_order_location` |
|
||||
| 3. Retry sukses | Mapping diperbaiki, hit `retry_location` | `succeeded` berisi station yang sebelumnya gagal, `failed: []` |
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- Dev only. Tidak ada perubahan production.
|
||||
- Tidak ada perubahan schema DB (tabel `t_order_location` dan `error_log_order` tidak berubah).
|
||||
- Tidak ada perubahan FE yang wajib.
|
||||
- Perubahan hanya di `Order.php` pada path `mockup/fo/ibl_registration/`.
|
||||
Reference in New Issue
Block a user