diff --git a/adm/shop_admin/admin.shop.lib.php b/adm/shop_admin/admin.shop.lib.php index b2be62547..ccd62bcc4 100644 --- a/adm/shop_admin/admin.shop.lib.php +++ b/adm/shop_admin/admin.shop.lib.php @@ -88,7 +88,8 @@ function change_status($od_id, $current_status, $change_status) $sql = " update {$g5['g5_shop_order_table']} set od_status = '{$change_status}' where od_id = '{$od_id}' and od_status = '{$current_status}' "; sql_query($sql, true); - $sql = " update {$g5['g5_shop_cart_table']} set ct_status = '{$change_status}' where od_id = '{$od_id}' and ct_status = '{$current_status}' "; + $complete_time_sql = get_cart_complete_time_sql($change_status); + $sql = " update {$g5['g5_shop_cart_table']} set ct_complete_time = $complete_time_sql, ct_status = '{$change_status}' where od_id = '{$od_id}' and ct_status = '{$current_status}' "; sql_query($sql, true); } diff --git a/adm/shop_admin/configform.php b/adm/shop_admin/configform.php index 53c001164..ee048e6fb 100644 --- a/adm/shop_admin/configform.php +++ b/adm/shop_admin/configform.php @@ -587,8 +587,8 @@ if( function_exists('pg_setting_check') ){ - - 주문 완료 일 이후에 포인트를 지급 + + 배송 완료 일 이후에 포인트를 지급 diff --git a/adm/shop_admin/orderformcartupdate.php b/adm/shop_admin/orderformcartupdate.php index 94ceee6ea..f005cfbcc 100644 --- a/adm/shop_admin/orderformcartupdate.php +++ b/adm/shop_admin/orderformcartupdate.php @@ -322,8 +322,10 @@ for ($i=0; $i<$cnt; $i++) $now = G5_TIME_YMDHIS; $ct_history="\n$ct_status|{$member['mb_id']}|$now|$REMOTE_ADDR"; + $complete_time_sql = get_cart_complete_time_sql($ct_status); $sql = " update {$g5['g5_shop_cart_table']} - set ct_point_use = '$point_use', + set ct_complete_time = $complete_time_sql, + ct_point_use = '$point_use', ct_stock_use = '$stock_use', ct_status = '$ct_status', ct_history = CONCAT(ct_history,'$ct_history') @@ -336,6 +338,11 @@ for ($i=0; $i<$cnt; $i++) $arr_it_id[] = $ct['it_id']; } +// 완료 처리 직후 실행하여 0일 설정도 즉시 적용한다. +if ($ct_status == '완료') { + save_order_point(); +} + // 상품 판매수량 반영 if(is_array($arr_it_id) && !empty($arr_it_id)) { $unq_it_id = array_unique($arr_it_id); diff --git a/adm/shop_admin/orderlistupdate.php b/adm/shop_admin/orderlistupdate.php index c7b6c89b4..19b1542a3 100644 --- a/adm/shop_admin/orderlistupdate.php +++ b/adm/shop_admin/orderlistupdate.php @@ -161,6 +161,11 @@ for ($i=0; $i<$count_post_chk; $i++) } +// 일괄 완료 처리도 0일 설정이면 즉시 적립한다. +if ($od_status == '완료') { + save_order_point(); +} + // SMS $sms_count = count($sms_messages); if($sms_count > 0) { diff --git a/docs/order-completion-point.md b/docs/order-completion-point.md new file mode 100644 index 000000000..40b595d4b --- /dev/null +++ b/docs/order-completion-point.md @@ -0,0 +1,23 @@ +# 배송완료 기준 주문 포인트 + +주문 포인트 지급 기간은 장바구니 상품·옵션 레코드(`ct_id`)별 배송완료 시각(`ct_complete_time`)부터 계산한다. 주문 접수 시각, 장바구니 생성 시각, 송장 등록 시각과는 구분한다. + +## 상태 변경과 지급 + +- 관리자 주문 상세의 선택 상품 완료 처리와 주문 목록의 일괄 완료 처리에서 완료 시각을 기록한다. +- 이미 완료된 상품을 다시 완료로 저장하면 기존 완료 시각을 유지한다. +- 완료를 해제하면 완료 시각을 `NULL`로 초기화한다. 이후 다시 완료하면 새 시각부터 지급 기간을 계산한다. +- 회원의 포인트가 있는 상품 중 완료 상태이고 아직 적립되지 않은 상품에 지급한다. +- 설정이 0일이면 완료 처리 시 지급한다. 그 외에는 설정 기간 경과 후 관리자 주문 상세 조회 또는 완료 처리 시 지급한다. 별도 예약 작업으로 정시에 지급하는 방식은 아니다. + +## 기존 설치 업그레이드 + +변경된 코드로 주문을 처리하기 전에 **환경설정 → DB업그레이드** 또는 CLI에서 `20260909_002_cart_complete_time` 마이그레이션을 적용한다. 선행 마이그레이션이 미적용 상태이면 먼저 적용해야 한다. + +기존 레코드는 완료 시각을 `NULL`로 유지한다. 기존 배송완료·미지급 상품은 지급을 추가로 늦추지 않도록 종전의 `ct_time` 기준을 적용한다. 완료 시각을 추정하거나 업그레이드 시각으로 덮어쓰지 않는다. 기존 상품도 완료를 해제한 후 다시 완료하면 새 기준을 적용한다. 이미 지급한 포인트는 마이그레이션으로 변경하지 않는다. + +신규 설치 스키마에도 같은 컬럼을 포함한다. 별도 확장 기능에서 상품 상태를 변경한다면 완료 시각 기록과 초기화도 함께 구현해야 한다. `get_cart_complete_time_sql()`의 반환식을 `ct_status` 대입보다 먼저 사용하면 기존 상태를 기준으로 완료 시각을 결정할 수 있다. + +## 검증 + +`php tests/order_completion_point.php`는 `data/dbconfig.php`의 DB 연결로 세션 전용 임시 테이블을 만들어 상태 변경, 기존 상품 전환 및 지급 조건을 검증한다. 실제 주문·포인트 테이블은 변경하지 않으며 포인트 등록 함수는 테스트 대역을 사용한다. DB 계정에 임시 테이블 생성 권한이 필요하다. diff --git a/install/gnuboard5shop.sql b/install/gnuboard5shop.sql index 680cde423..4fb827d5d 100644 --- a/install/gnuboard5shop.sql +++ b/install/gnuboard5shop.sql @@ -58,6 +58,7 @@ CREATE TABLE IF NOT EXISTS `g5_shop_cart` ( `ct_direct` tinyint(4) NOT NULL DEFAULT '0', `ct_select` tinyint(4) NOT NULL DEFAULT '0', `ct_select_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `ct_complete_time` datetime DEFAULT NULL, PRIMARY KEY (`ct_id`), KEY `od_id` (`od_id`), KEY `it_id` (`it_id`), diff --git a/lib/shop.lib.php b/lib/shop.lib.php index 92dbefcb5..a969c9d63 100644 --- a/lib/shop.lib.php +++ b/lib/shop.lib.php @@ -2500,17 +2500,33 @@ function exists_inicis_shop_order($oid, $pp=array(), $od_time='', $od_ip='') return ''; } +// 상태 변경 UPDATE에서 ct_status를 대입하기 전에 사용한다. +// 완료 상태에서 다시 저장하면 최초 완료 시각을 유지하고, 완료 해제 시 초기화한다. +function get_cart_complete_time_sql($ct_status) +{ + if ($ct_status != '완료') { + return 'NULL'; + } + + return "CASE WHEN ct_status = '완료' THEN ct_complete_time ELSE '".G5_TIME_YMDHIS."' END"; +} + //------------------------------------------------------------------------------ // 주문포인트를 적립한다. -// 설정일이 지난 포인트 부여되지 않은 배송완료된 장바구니 자료에 포인트 부여 -// 설정일이 0 이면 주문서 완료 설정 시점에서 포인트를 바로 부여합니다. +// 배송완료 시각(ct_complete_time)부터 설정일이 지난 미적립 상품에 포인트 부여 +// 설정일이 0 이면 배송완료 처리 시 포인트를 바로 부여합니다. //------------------------------------------------------------------------------ function save_order_point($ct_status="완료") { global $g5, $default; + // 완료 시각이 없는 기존 상품에만 종전의 장바구니 생성 시각 기준을 유지한다. $beforedays = date("Y-m-d H:i:s", ( time() - (86400 * (int)$default['de_point_days']) ) ); // 86400초는 하루 - $sql = " select * from {$g5['g5_shop_cart_table']} where ct_status = '$ct_status' and ct_point_use = '0' and ct_time <= '$beforedays' "; + $sql = " select * from {$g5['g5_shop_cart_table']} + where ct_status = '$ct_status' + and ct_point_use = '0' + and ((ct_complete_time is not null and ct_complete_time <= '$beforedays') + or (ct_complete_time is null and ct_time <= '$beforedays')) "; $result = sql_query($sql); for ($i=0; $row=sql_fetch_array($result); $i++) { // 회원 ID 를 얻는다. diff --git a/migrations/20260909_002_cart_complete_time.sql b/migrations/20260909_002_cart_complete_time.sql new file mode 100644 index 000000000..06d56ea34 --- /dev/null +++ b/migrations/20260909_002_cart_complete_time.sql @@ -0,0 +1,4 @@ +-- @description 상품별 배송완료 시각 추가 (기존 완료 상품은 종전 포인트 지급 기준 유지) +-- @if-table-exists {{g5_shop_cart_table}} +-- @if-column-missing {{g5_shop_cart_table}} ct_complete_time +ALTER TABLE `{{g5_shop_cart_table}}` ADD `ct_complete_time` datetime DEFAULT NULL AFTER `ct_select_time`; diff --git a/tests/order_completion_point.php b/tests/order_completion_point.php new file mode 100644 index 000000000..3595e6819 --- /dev/null +++ b/tests/order_completion_point.php @@ -0,0 +1,112 @@ +set_charset('utf8mb4'); +$g5['g5_shop_cart_table'] = 'test_completion_cart_'.getmypid(); +$g5['g5_shop_order_table'] = 'test_completion_order_'.getmypid(); +$cart = $g5['g5_shop_cart_table']; +$order = $g5['g5_shop_order_table']; +$points = array(); +function sql_query($sql, $error = true) { global $db; return $db->query($sql); } +function sql_fetch_array($result) { return $result->fetch_assoc(); } +function sql_fetch($sql) { return sql_fetch_array(sql_query($sql)); } +function insert_point($member, $amount, $content, $table, $id, $action) { + global $points; + $points[] = array($member, $amount, $action); +} +function expect_completion($condition, $message) { + if (!$condition) throw new Exception($message); +} +function completion_row($id) { + global $cart; + return sql_fetch("SELECT * FROM $cart WHERE ct_id = $id"); +} +function complete_item($id, $status) { + // 관리자 개별 처리의 실제 UPDATE 구문을 실행한다. + global $cart, $root, $g5; + $source = file_get_contents($root.'/adm/shop_admin/orderformcartupdate.php'); + $start = strpos($source, ' $complete_time_sql = get_cart_complete_time_sql($ct_status);'); + $end = strpos($source, ' sql_query($sql);', $start) + strlen(' sql_query($sql);'); + $ct_status = $status; + $ct_id = $id; + $ct = completion_row($id); + $od_id = $ct['od_id']; + $point_use = $ct['ct_point_use']; + $stock_use = 0; + $ct_history = ''; + eval(substr($source, $start, $end - $start)); +} +sql_query("CREATE TEMPORARY TABLE $order (od_id bigint PRIMARY KEY, mb_id varchar(255), od_status varchar(255))"); +sql_query("CREATE TEMPORARY TABLE $cart (ct_id int PRIMARY KEY, od_id bigint, ct_status varchar(255), ct_point_use int DEFAULT 0, ct_point int DEFAULT 10, ct_qty int DEFAULT 2, ct_stock_use int DEFAULT 0, ct_history text, ct_time datetime, ct_select_time datetime)"); +$old = date('Y-m-d H:i:s', time() - 86400 * 30); +$now = G5_TIME_YMDHIS; +sql_query("INSERT INTO $order VALUES (1, 'member', '배송'), (2, '', '완료')"); +sql_query("INSERT INTO $cart (ct_id, od_id, ct_status, ct_time, ct_select_time, ct_history) VALUES (1, 1, '완료', '$old', '$old', ''), (2, 1, '배송', '$old', '$old', '')"); +$migration = g5_migration_parse_file($root.'/migrations/20260909_002_cart_complete_time.sql'); +expect_completion(!isset($migration['error']), '마이그레이션 파싱'); +foreach ($migration['statements'] as $statement) { + sql_query(g5_migration_replace_placeholders($statement['sql'])); +} +expect_completion(completion_row(1)['ct_complete_time'] === null, '기존 완료 시각은 NULL 유지'); +complete_item(1, '완료'); +expect_completion(completion_row(1)['ct_complete_time'] === null, '기존 완료 재저장 시 종전 정책 유지'); +change_status(1, '배송', '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '일괄 완료 시각 기록'); +expect_completion(completion_row(1)['ct_complete_time'] === null, '일괄 처리에서 기존 완료 상품 유지'); +complete_item(2, '배송'); +expect_completion(completion_row(2)['ct_complete_time'] === null, '개별 완료 해제'); +complete_item(2, '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '개별 재완료 시각 기록'); +sql_query("UPDATE $cart SET ct_complete_time = '$old' WHERE ct_id = 2"); +complete_item(2, '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $old, '완료 재저장 시 최초 시각 유지'); +change_status(1, '완료', '배송'); +expect_completion(completion_row(2)['ct_complete_time'] === null, '일괄 완료 해제'); +change_status(1, '배송', '완료'); +expect_completion(completion_row(2)['ct_complete_time'] === $now, '일괄 재완료'); + +// 날짜 경계와 적립 루프를 실제 SQL로 검증한다. +sql_query("DELETE FROM $cart"); +$cutoff = date('Y-m-d H:i:s', time() - 86400 * 7); +$future = date('Y-m-d H:i:s', time() + 86400); +$rows = array( + array(1, 1, '완료', 0, $old, $now), // 오래된 장바구니, 오늘 완료 + array(2, 1, '완료', 0, $old, $cutoff), // 정확히 7일 경과 + array(3, 1, '완료', 0, $old, $old), + array(4, 1, '배송', 0, $old, $old), + array(5, 1, '취소', 0, $old, $old), + array(6, 1, '완료', 1, $old, $old), + array(7, 1, '완료', 0, $old, null), // 기존 완료 + array(8, 1, '완료', 0, $now, null), // 기존 기준에서도 대기 + array(9, 2, '완료', 0, $old, $old), // 비회원 + array(10, 1, '완료', 0, $old, $future) +); +foreach ($rows as $row) { + list($id, $od_id, $status, $used, $created, $completed) = $row; + $completed_sql = $completed === null ? 'NULL' : "'$completed'"; + sql_query("INSERT INTO $cart (ct_id, od_id, ct_status, ct_point_use, ct_time, ct_complete_time) VALUES ($id, $od_id, '$status', $used, '$created', $completed_sql)"); +} +$default = array('de_point_days' => 7); +save_order_point(); +expect_completion($points === array(array('member', 20, '1,2'), array('member', 20, '1,3'), array('member', 20, '1,7')), '배송완료 기간, 기존 정책, 상태, 회원, 수량 검증'); +save_order_point(); +expect_completion(count($points) === 3, '중복 지급 방지'); +$default['de_point_days'] = 0; +save_order_point(); +expect_completion(count($points) === 5, '0일이면 오늘 완료 및 기존 당일 상품 지급'); +expect_completion(completion_row(10)['ct_point_use'] == 0, '미래 완료 시각 제외'); +foreach (array('orderformcartupdate.php', 'orderlistupdate.php') as $file) { + $source = file_get_contents($root.'/adm/shop_admin/'.$file); + expect_completion(strpos($source, 'save_order_point();') !== false, $file.' 완료 후 적립 호출'); +} +echo "배송완료 시각 기록·유지·초기화, 부분 완료, 기존 상품 전환, 7일·0일 지급, 중복 지급 방지 통과\n";