diff --git a/app/Forms/CommonForm.php b/app/Forms/CommonForm.php index 5ed013f..6418249 100644 --- a/app/Forms/CommonForm.php +++ b/app/Forms/CommonForm.php @@ -180,34 +180,6 @@ abstract class CommonForm return $data; } - /** - * 2) 숫자/FK 필드 정규화 - * - 폼에서 미선택은 보통 ''로 들어옴 -> NULL로 변환 - * - 숫자 문자열은 int 캐스팅 (선택) - * - * 주의: - * - "빈값을 0으로 취급" 같은 정책이 있다면 여기에서 조정해야 함. - */ - protected function normalizeNumericEmptyToNull(array $data, array $numericFields): array - { - foreach ($numericFields as $f) { - if (!array_key_exists($f, $data)) { - continue; - } - - if ($data[$f] === '') { - $data[$f] = null; - continue; - } - - if (is_string($data[$f]) && ctype_digit($data[$f])) { - $data[$f] = (int) $data[$f]; - } - } - - return $data; - } - /** * 3) role.* 같은 배열 원소 규칙이 있을 때, 부모 배열 존재/타입 보정 */ @@ -259,38 +231,6 @@ abstract class CommonForm } } - - /** - * 4) 검증 rule에 따라 "numeric(특히 FK)"로 취급할 필드를 수집 - * - getFormRule()에서 permit_empty|numeric 로 정의되는 필드를 공통 처리하기 위함 - * - * 구현 전략: - * - formRules에서 rule 문자열에 'numeric'가 포함된 필드를 모음 - * - wildcard(role.*) 제외 - */ - protected function collectNumericFieldsFromRules(array $formRules): array - { - $numericFields = []; - - foreach ($formRules as $field => $rule) { - $fieldName = (string) $field; - - if (str_contains($fieldName, '.*')) { - continue; - } - - // getValidationRule hook 적용 (필드명/룰이 바뀔 수 있으니) - [$fieldName, $ruleStr] = $this->getValidationRule($fieldName, (string) $rule); - - if (is_string($ruleStr) && str_contains($ruleStr, 'numeric')) { - $numericFields[] = $fieldName; - } - } - - // 중복 제거 - return array_values(array_unique($numericFields)); - } - /* --------------------------------------------------------------------- * Validation * --------------------------------------------------------------------- */ @@ -299,68 +239,70 @@ abstract class CommonForm * 데이터를 검증하고 유효하지 않을 경우 예외를 발생시킵니다. * 2025 CI4 표준: 규칙 배열 내에 label을 포함하여 한글 메시지 출력을 보장합니다. */ - final public function validate(array &$formDatas): void + public function validate_process(array &$formDatas, $isDebug = false): array + { + // 1) 전체 라벨/룰 + $allFormFields = $this->getFormFields(); // ['field' => '라벨', ...] + $allFormRules = $this->getFormRules(); // ['field' => 'rules', ...] + if (empty($allFormRules)) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다."); + } + + if ($this->formAction === 'modify') { + // (1) formDatas에 실제로 넘어온 필드만 + $targetFields = array_keys($formDatas); + if ($isDebug) { + log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields Begin DEBUG"); + log_message('debug', var_export($targetFields, true)); + } + // // (2) 내부 제어용 키 제거(프로젝트에 맞게 추가/삭제) + // $exclude = ['_method', 'csrf_test_name', 'submit', 'token', 'action']; + // $targetFields = array_values(array_diff($targetFields, $exclude)); + // // (3) wildcard(role.*) 같은 규칙이 있으면 부모 기반으로 같이 포함 + // // - formDatas에 role이 있으면 role.* 규칙도 함께 검사되도록 추가 + // foreach ($allFormRules as $ruleField => $_ruleStr) { + // $ruleField = (string) $ruleField; + // if (!str_contains($ruleField, '.*')) { + // continue; + // } + // $parent = str_replace('.*', '', $ruleField); + // if (in_array($parent, $targetFields, true)) { + // $targetFields[] = $ruleField; // e.g. 'role.*' + // } + // } + // // (4) 실제로 룰이 정의된 필드만 남김 + // $targetFields = array_values(array_intersect($targetFields, array_keys($allFormRules))); + if ($isDebug) { + log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate targetFields End DEBUG"); + log_message('debug', var_export($targetFields, true)); + } + // 최종: modify에서는 "타겟만" 룰/라벨 세팅 + $formFields = $this->getFormFields($targetFields); + $formRules = $this->getFormRules($targetFields); + // throw new RuntimeException(static::class . "->targetFields" . implode(",", $targetFields) . "\nformRules:" . implode(",", array_keys($formRules)) . "\nformFields:" . implode(",", array_keys($formFields))); + }//modify + // 2) 액션별 "검증 대상 필드" 결정 + // $formFields = $allFormFields; + // $formRules = $allFormRules; + return array($formFields, $formRules, $allFormFields); + } + final public function validate(array &$formDatas, $isDebug = false): void { log_message('debug', '>>> CommonForm::validate CALLED: ' . static::class . ', formAction:' . $this->formAction); try { // 0) 데이터 구조 정리 (null 변환 X) $formDatas = $this->sanitizeFormDatas($formDatas); - - // 1) 전체 라벨/룰 - $allFormFields = $this->getFormFields(); // ['field' => '라벨', ...] - $allFormRules = $this->getFormRules(); // ['field' => 'rules', ...] - if (empty($allFormRules)) { - throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 지정된 Form RULE이 없습니다."); - } - - // 2) 액션별 "검증 대상 필드" 결정 - $formRules = $allFormRules; - $formFields = $allFormFields; - if ($this->formAction === 'modify') { - // (1) formDatas에 실제로 넘어온 필드만 - $targetFields = array_keys($formDatas); - - // (2) 내부 제어용 키 제거(프로젝트에 맞게 추가/삭제) - $exclude = ['_method', 'csrf_test_name', 'submit', 'token', 'action']; - $targetFields = array_values(array_diff($targetFields, $exclude)); - - // (3) wildcard(role.*) 같은 규칙이 있으면 부모 기반으로 같이 포함 - // - formDatas에 role이 있으면 role.* 규칙도 함께 검사되도록 추가 - foreach ($allFormRules as $ruleField => $_ruleStr) { - $ruleField = (string) $ruleField; - if (!str_contains($ruleField, '.*')) { - continue; - } - $parent = str_replace('.*', '', $ruleField); - if (in_array($parent, $targetFields, true)) { - $targetFields[] = $ruleField; // e.g. 'role.*' - } - } - - // (4) 실제로 룰이 정의된 필드만 남김 - $targetFields = array_values(array_intersect($targetFields, array_keys($allFormRules))); - - // 최종: modify에서는 "타겟만" 룰/라벨 세팅 - $formRules = $this->getFormRules($targetFields); - $formFields = $this->getFormFields($targetFields); - // throw new RuntimeException(static::class . "->targetFields" . implode(",", $targetFields) . "\nformRules:" . implode(",", array_keys($formRules)) . "\nformFields:" . implode(",", array_keys($formFields))); - } + list($formFields, $formRules, $allFormFields) = $this->validate_process($formDatas, $isDebug); if (empty($formRules)) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 검증할 대상 RULE이 없습니다."); } - // 3) wildcard(role.*) 부모 배열 보정 $this->ensureParentArrayForWildcardRules($formDatas, $formRules); - // 4) numeric(FK 포함) 필드: '' -> null, 숫자 문자열 -> int - $numericFields = $this->collectNumericFieldsFromRules($formRules); - $formDatas = $this->normalizeNumericEmptyToNull($formDatas, $numericFields); - // 5) dynamicRules 구성 $dynamicRules = []; foreach ($formRules as $field => $rule) { [$fieldName, $ruleStr] = $this->getValidationRule((string) $field, (string) $rule); - // label 결정 if (isset($formFields[$fieldName])) { $label = $formFields[$fieldName]; @@ -370,13 +312,18 @@ abstract class CommonForm } else { $label = $fieldName; } - $dynamicRules[$fieldName] = [ 'label' => $label, 'rules' => $ruleStr, ]; } + if ($isDebug) { + log_message('debug', static::class . '->' . __FUNCTION__ . "에서 Validate dynamicRules DEBUG"); + log_message('debug', var_export($formDatas, true)); + log_message('debug', var_export($dynamicRules, true)); + } + $this->_validation->setRules($dynamicRules); if (!$this->_validation->run($formDatas)) { $errors = $this->_validation->getErrors(); @@ -394,7 +341,7 @@ abstract class CommonForm if ($e instanceof RuntimeException) { throw $e; } - throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage()); + // throw new RuntimeException("유효성 검사 중 시스템 오류 발생: " . $e->getMessage()); } } diff --git a/app/Forms/Customer/ServiceForm.php b/app/Forms/Customer/ServiceForm.php index b8bd6dd..14ab8d3 100644 --- a/app/Forms/Customer/ServiceForm.php +++ b/app/Forms/Customer/ServiceForm.php @@ -43,6 +43,9 @@ class ServiceForm extends CustomerForm 'status' ]; switch ($action) { + case 'modify': + $fields = [...$fields, 'amount']; + break; case 'view': $fields = [...$fields, 'created_at']; break; diff --git a/app/Services/CommonService.php b/app/Services/CommonService.php index 8529888..4858b65 100644 --- a/app/Services/CommonService.php +++ b/app/Services/CommonService.php @@ -232,6 +232,10 @@ abstract class CommonService } //생성용 + protected function create_process_validate(CommonForm &$actionForm, array $formDatas) + { + $actionForm->validate($formDatas); // ✅ 여기서 검증 + } protected function create_process(array $formDatas): CommonEntity { try { @@ -240,7 +244,7 @@ abstract class CommonService throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다."); } $actionForm->form_init_process('create', $formDatas); - $actionForm->validate($formDatas); // ✅ 여기서 검증 + $this->create_process_validate($actionForm, $formDatas); // 검증 통과 후 엔티티 반영용 foreach ($formDatas as $field => $value) { $formDatas = $this->fieldhook_process($field, $value, $formDatas); @@ -273,6 +277,10 @@ abstract class CommonService } //수정용 + protected function modify_process_validate(CommonForm &$actionForm, array $formDatas) + { + $actionForm->validate($formDatas); // ✅ 여기서 검증 + } protected function modify_process($entity, array $formDatas): CommonEntity { try { @@ -281,7 +289,7 @@ abstract class CommonService throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: actionForm이 정의되지 않았습니다."); } $actionForm->form_init_process('modify', $formDatas); - $actionForm->validate($formDatas); // ✅ 여기서 검증 + $this->modify_process_validate($actionForm, $formDatas); // 검증 통과 후 엔티티 반영 foreach ($formDatas as $field => $value) { $formDatas = $this->fieldhook_process($field, $value, $formDatas); diff --git a/app/Services/Customer/ServiceService.php b/app/Services/Customer/ServiceService.php index ad2c97f..7ed19d0 100644 --- a/app/Services/Customer/ServiceService.php +++ b/app/Services/Customer/ServiceService.php @@ -2,14 +2,15 @@ namespace App\Services\Customer; +use App\Entities\CommonEntity; +use App\Entities\Customer\ServiceEntity; +use App\Forms\CommonForm; +use App\Forms\Customer\ServiceForm; +use App\Helpers\Customer\ServiceHelper; +use App\Models\Customer\ServiceModel; +use DateTimeImmutable; use DateTimeZone; use RuntimeException; -use DateTimeImmutable; -use App\Entities\CommonEntity; -use App\Forms\Customer\ServiceForm; -use App\Models\Customer\ServiceModel; -use App\Helpers\Customer\ServiceHelper; -use App\Entities\Customer\ServiceEntity; class ServiceService extends CustomerService { @@ -61,46 +62,31 @@ class ServiceService extends CustomerService return $date->format('Y-m-d'); } - private function getCalculatedAmount(int $rack_price, int $line_price, int $sale_price, int $serverinfo_uid): int - { - $server_amount = service('equipment_serverservice')->getCalculatedAmount($serverinfo_uid); - return (int) $server_amount + $rack_price + $line_price - $sale_price; - } - - final protected function updateAmount(ServiceEntity $entity): ServiceEntity - { - $serverUid = $entity->getServerInfoUid(); - // ✅ 서버 미지정(해지/분리 상태) 방어: 계산 시도 자체를 막는다 - if (empty($serverUid)) { // null, 0, '' 모두 방어 - throw new RuntimeException( - static::class . '->' . __FUNCTION__ - . " 오류: 서비스({$entity->getPK()})에 serverinfo_uid가 없습니다. (해지/분리 상태)" - ); - } - $entity->amount = $this->getCalculatedAmount( - $entity->getRack(), - $entity->getLine(), - $entity->getSale(), - (int) $serverUid - ); - // dd($entity); - if (!$this->model->save($entity)) { - throw new RuntimeException("금액 업데이트 중 DB 저장 오류: " . implode(', ', $this->model->errors())); - } - return $entity; - } - - final public function updateBillingAt($uid, string $billing_at): ServiceEntity + final public function updateBillingAt(int $uid, string $billing_at): CommonEntity { $entity = $this->getEntity($uid); if (!$entity instanceof ServiceEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:{$uid}에 해당하는 서비스정보를 찾을 수 업습니다."); } + $formDatas = ['billing_at' => $billing_at]; + return parent::modify_process($entity, $formDatas); + } - $entity->billing_at = $billing_at; - if (!$this->model->save($entity)) { - $errors = $this->model->errors(); - throw new RuntimeException("금액 업데이트 중 DB 저장 오류: " . implode(', ', $errors)); + //서비스 관련 총 금액 + private function getCalculatedAmount(ServiceEntity $entity): int + { + //서버 관련 금액 + $server_amount = service('equipment_serverservice')->getCalculatedAmount($entity->getServerInfoUid()); + return (int) $server_amount + (int) $entity->getRack() + (int) $entity->getLine() - $entity->getSale(); + } + + //서비스 금액 설정 + final public function recalcAmount(ServiceEntity $entity): ServiceEntity + { + $formDatas = ['amount' => $this->getCalculatedAmount($entity)]; + $entity = parent::modify_process($entity, $formDatas); + if (!$entity instanceof ServiceEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); } return $entity; } @@ -110,74 +96,74 @@ class ServiceService extends CustomerService return $entity; } - // ✅ (internal) payment까지 동기화하는 일반용 로직 (트랜잭션 없음) - final public function recalcAmountAndSyncPaymentInternal(ServiceEntity $old, ServiceEntity $current): ServiceEntity - { - $current = $this->updateAmount($current); - service('paymentservice')->setByService($old, $current); - return $current; - } - - // ✅ (internal) amount만 재계산 (해지 시 0원될 수 있으나 payment 건드리지 않음) - final public function recalcAmountInternal(ServiceEntity $current): ServiceEntity - { - return $this->updateAmount($current); - } - - // ✅ (public) dbTransaction 보류: 내부 로직만 호출 - final public function recalcAmountAndSyncPayment(ServiceEntity $serviceEntity): ServiceEntity - { - $old = clone $serviceEntity; - return $this->recalcAmountAndSyncPaymentInternal($old, $serviceEntity); - } - - final public function recalcAmountAndSyncPaymentByOld(ServiceEntity $old, ServiceEntity $current): ServiceEntity - { - return $this->recalcAmountAndSyncPaymentInternal($old, $current); - } - protected function create_process(array $formDatas): ServiceEntity { - if (empty($formDatas['site'])) { + if (!array_key_exists('site', $formDatas) || empty($formDatas['site'])) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 사이트가 지정되지 않았습니다."); } - if (empty($formDatas['serverinfo_uid'])) { + if (!array_key_exists('serverinfo_uid', $formDatas) || empty($formDatas['serverinfo_uid'])) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서버가 지정되지 않았습니다."); } $formDatas['code'] = $formDatas['site'] . "_s" . uniqid(); $formDatas['amount'] = 0; - $entity = parent::create_process($formDatas); if (!$entity instanceof ServiceEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); } - + //서버 설정 service('equipment_serverservice')->attatchToService($entity, $entity->getServerInfoUid()); - - return $this->recalcAmountAndSyncPayment($entity); + //서비스 금액 설정 + $entity = $this->recalcAmount($entity); + //결제 추가 + service('paymentservice')->createByService($entity); + return $entity; } + protected function modify_process_validate(CommonForm &$actionForm, array $formDatas) + { + $actionForm->validate($formDatas, true); // ✅ 여기서 검증 + } protected function modify_process($entity, array $formDatas): ServiceEntity { if (empty($formDatas['serverinfo_uid'])) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서버가 지정되지 않았습니다."); } - + //기존 정보 저장 $oldEntity = clone $entity; - $formDatas['code'] = $entity->getCode(); - $formDatas['amount'] = 0; $entity = parent::modify_process($entity, $formDatas); if (!$entity instanceof ServiceEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); } - + //서버 설정 if ($oldEntity->getServerInfoUid() !== $entity->getServerInfoUid()) { service('equipment_serverservice')->modifyByService($oldEntity, $entity); } + //서비스 금액 설정 + $entity = $this->recalcAmount($entity); + //결제비 설정 + service('paymentservice')->modifyByService($oldEntity, $entity); + return $entity; + } - return $this->recalcAmountAndSyncPaymentByOld($oldEntity, $entity); + protected function delete_process($entity): ServiceEntity + { + if (!$entity instanceof ServiceEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); + } + + //기존정보 우선 저장 + $oldEntity = clone $entity; + + $entity = parent::delete_process($entity); + if (!$entity instanceof ServiceEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); + } + if ($oldEntity->getServerInfoUid()) { + service('equipment_serverservice')->deatchFromService($oldEntity->getServerInfoUid()); + } + return $oldEntity; } public function history(string|int $uid, string $history): CommonEntity diff --git a/app/Services/Equipment/ServerPartService.php b/app/Services/Equipment/ServerPartService.php index d246c44..40693c3 100644 --- a/app/Services/Equipment/ServerPartService.php +++ b/app/Services/Equipment/ServerPartService.php @@ -93,55 +93,18 @@ class ServerPartService extends EquipmentService return $formDatas; } - /** - * ✅ MONTH 파트 변경 시: - * - "서비스가 유지중"이면: (serverinfo_uid 기준) 현재 서버가 붙은 서비스로 amount 재계산 + 월 Payment upsert - * - "서버가 서비스에서 분리된 상태(server.serviceinfo_uid==null)"이면: 월 미납 결제 status=TERMINATED (amount 재계산 금지) - * - * 핵심: ServerPartService는 'serviceUid'가 아니라 'serverUid'를 기준으로 판단한다. - */ - private function syncMonthlyServiceAndPaymentByServer( - int $serverUid, - ?ServiceEntity $oldServiceSnapshot = null, - bool $serverDetached = false - ): void { - $server = service('equipment_serverservice')->getEntity($serverUid); - if (!$server instanceof ServerEntity) { - throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$serverUid} 서버정보를 찾을수 없습니다."); - } - - // ✅ 해지 케이스 - if ($serverDetached) { - if ($oldServiceSnapshot instanceof ServiceEntity) { - service('paymentservice')->terminateUnpaidMonthlyByService($oldServiceSnapshot); + //서비스 금액 재계산용 + private function recalcAmount(ServerPartEntity $entity): void + { + //월비용 서버파트 정보일 경우 서버금액 재계산 + if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) { + $serverEntity = service('equipment_serverservice')->getEntity($entity->getServerInfoUid()); + if ($serverEntity instanceof ServerEntity) { + service('equipment_serverservice')->recalcAmount($serverEntity); } - return; } - - $serviceUid = $server->getServiceInfoUid(); - if (!$serviceUid) { - throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 서비스가 정의되지 않은 서버정보입니다."); - } - - $svcService = service('customer_serviceservice'); - $current = $svcService->getEntity($serviceUid); - if (!$current instanceof ServiceEntity) { - throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$serviceUid} 서비스정보를 찾을수 없습니다."); - } - - // ✅ 핵심 추가: 메인서버만 amount/payment 동기화 허용 - if ((int) $current->getServerInfoUid() !== (int) $serverUid) { - // 대체서버(alternative)에서는 서비스금액/결제 동기화 금지 - return; // 또는 정책상 필요하면 예외로 변경 가능 - // throw new RuntimeException("메인서버가 아닌 서버({$serverUid})에서 월동기화 금지"); - } - - // ✅ 일반 케이스: amount + payment upsert - $old = $oldServiceSnapshot instanceof ServiceEntity ? $oldServiceSnapshot : clone $current; - $svcService->recalcAmountAndSyncPaymentInternal($old, $current); } - protected function create_process(array $formDatas): CommonEntity { $serverEntity = service('equipment_serverservice')->getEntity($formDatas['serverinfo_uid']); @@ -162,7 +125,7 @@ class ServerPartService extends EquipmentService // ✅ 서버가 서비스에 붙어 있을 때만 결제/동기화 if ($entity->getServiceInfoUid()) { if ($entity->getBilling() == PAYMENT['BILLING']['MONTH']) { - $this->syncMonthlyServiceAndPaymentByServer((int) $entity->getServerInfoUid()); + $this->recalcAmount($entity); } if ($entity->getBilling() == PAYMENT['BILLING']['ONETIME']) { service('paymentservice')->createByServerPart($entity); @@ -178,45 +141,34 @@ class ServerPartService extends EquipmentService throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$formDatas['serverinfo_uid']}에 해당하는 서버정보을 찾을수 없습니다."); } - $formDatas = $this->getFormDatasForServerPart($formDatas, $serverEntity); - + //기존정보 우선 저장 $oldEntity = clone $entity; - $entity = parent::modify_process($entity, $formDatas); + $entity = parent::modify_process($entity, $this->getFormDatasForServerPart($formDatas, $serverEntity)); if (!$entity instanceof ServerPartEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Return Type은 ServerPartEntity만 가능"); } - $this->getPartService($entity->getType())->modifyServerPart($oldEntity, $entity); - // ✅ MONTH 동기화는 serviceinfo_uid가 아니라 serverinfo_uid 기준 - $serverUidsToSync = []; - - if ($oldEntity->getBilling() == PAYMENT['BILLING']['MONTH'] && $oldEntity->getServerInfoUid()) { - $serverUidsToSync[(int) $oldEntity->getServerInfoUid()] = true; - } + // ✅ 월별용 처리 (서버파트가 변경되었으므로 서버파트의 Billing이 MONTH이면 서비스/결제 동기화) if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) { - $serverUidsToSync[(int) $entity->getServerInfoUid()] = true; + $this->recalcAmount($entity); } - - foreach (array_keys($serverUidsToSync) as $serverUid) { - $this->syncMonthlyServiceAndPaymentByServer((int) $serverUid); - } - + // ✅ 일회성용 처리 if ($entity->getBilling() == PAYMENT['BILLING']['ONETIME']) { service('paymentservice')->modifyByServerPart($oldEntity, $entity); } - return $entity; } - protected function delete_process($entity): CommonEntity + protected function delete_process($entity): ServerPartEntity { if (!$entity instanceof ServerPartEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: ServerPartEntity만 가능"); } - $old = clone $entity; + //기존정보 우선 저장 + $oldEntity = clone $entity; $this->getPartService($entity->getType())->detachFromServerPart($entity); @@ -224,12 +176,10 @@ class ServerPartService extends EquipmentService if (!$entity instanceof ServerPartEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: Return Type은 ServerPartEntity만 가능"); } - - // ✅ MONTH면 서버 기준으로 서비스/결제 동기화 - if ($old->getBilling() == PAYMENT['BILLING']['MONTH'] && $old->getServerInfoUid()) { - $this->syncMonthlyServiceAndPaymentByServer((int) $old->getServerInfoUid()); + // ✅서버파트가 삭제되었으므로 서버파트의 Billing이 MONTH이면 서비스/결제 동기화 + if ($oldEntity->getBilling() == PAYMENT['BILLING']['MONTH'] && $oldEntity->getServerInfoUid()) { + $this->recalcAmount($oldEntity); } - // ✅ ONETIME 미납 결제 삭제는 "보류" (여기서는 아무것도 안함) return $entity; } @@ -268,37 +218,21 @@ class ServerPartService extends EquipmentService /** * ✅ 서버 해지/분리 시 서버파트 회수 처리 - * - BASE 제외 전부 삭제(정책상 OK) - * - MONTH 삭제가 있었다면: - * - server.serviceinfo_uid == null 인 상태에서만 TERMINATED 처리 (amount 재계산 금지) - * - ONETIME 미납 삭제는 보류 - * - * @param ServiceEntity|null $oldServiceEntity 분리 전 서비스 스냅샷(권장: TERMINATED 매칭용) */ - public function detachFromServer(ServerEntity $serverEntity, ?ServiceEntity $oldServiceEntity = null): void + public function detachFromServer(ServerEntity $serverEntity): void { - $monthlyChanged = false; foreach ($this->getEntities(['serverinfo_uid' => $serverEntity->getPK(), "billing !=" => PAYMENT['BILLING']['BASE']]) as $entity) { - if (!$entity instanceof ServerPartEntity) + if (!$entity instanceof ServerPartEntity) { continue; + } + //파트정보 해지 $this->getPartService($entity->getType())->detachFromServerPart($entity); - if ($entity->getBilling() == PAYMENT['BILLING']['MONTH']) { - $monthlyChanged = true; - } + //서버파트 해지 parent::delete_process($entity); - } - if (!$monthlyChanged) { - return; - } - // ✅ 분리 완료 후(server.serviceinfo_uid == null)에서만 TERMINATED 처리 - $serverDetached = ($serverEntity->getServiceInfoUid() === null); - if ($serverDetached) { - if ($oldServiceEntity instanceof ServiceEntity) { - $this->syncMonthlyServiceAndPaymentByServer((int) $serverEntity->getPK(), $oldServiceEntity, true); + //서비스금액 재계산 + if ($entity->getBilling() == PAYMENT['BILLING']['MONTH'] && $entity->getServerInfoUid()) { + $this->recalcAmount($entity); } - return; } - // ✅ (이 케이스는 정상 플로우에서는 거의 없지만) 서비스 유지중이면 정상 upsert - $this->syncMonthlyServiceAndPaymentByServer((int) $serverEntity->getPK()); } } diff --git a/app/Services/Equipment/ServerService.php b/app/Services/Equipment/ServerService.php index e7e7564..359617b 100644 --- a/app/Services/Equipment/ServerService.php +++ b/app/Services/Equipment/ServerService.php @@ -25,6 +25,12 @@ class ServerService extends EquipmentService return ServerEntity::class; } + protected function getEntity_process(mixed $entity): ServerEntity + { + return $entity; + } + + final public function getTotalServiceCount(array $where = []): array { $totalCounts = [ @@ -100,18 +106,29 @@ class ServerService extends EquipmentService if (!$entity instanceof ServerEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid} 서버 정보를 찾을수 없습니다."); } - $serverPartService = service('equipment_serverpartservice'); $caculatedAmount = $entity->getPrice(); - foreach ($serverPartService->getEntities(['serverinfo_uid' => $entity->getPK(), 'billing' => PAYMENT['BILLING']['MONTH']]) as $serverPartEntity) { + foreach (service('equipment_serverpartservice')->getEntities(['serverinfo_uid' => $entity->getPK(), 'billing' => PAYMENT['BILLING']['MONTH']]) as $serverPartEntity) { log_message('debug', $serverPartEntity->getCustomTitle() . '::' . $serverPartEntity->getCalculatedAmount()); $caculatedAmount += $serverPartEntity->getCalculatedAmount(); } return $caculatedAmount; } - protected function getEntity_process(mixed $entity): ServerEntity + public function recalcAmount(ServerEntity $entity): void { - return $entity; + if ($entity->getServiceInfoUid()) { + $serviceEntity = service('customer_serviceservice')->getEntity($entity->getServiceInfoUid()); + if (!$serviceEntity instanceof ServiceEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$entity->getServiceInfoUid()} 서비스정보를 찾을수 없습니다."); + } + // ✅ 핵심 추가: 서비스금액 변경 및 payment Sync용 + if ($serviceEntity->getServerInfoUid() === $entity->getServiceInfoUid()) { + $oldServiceEntity = clone $serviceEntity; + $serviceEntity = service('customer_serviceservice')->recalcAmount($serviceEntity); + //결제비 설정 + service('paymentservice')->modifyByService($oldServiceEntity, $serviceEntity); + } + } } protected function create_process(array $formDatas): ServerEntity @@ -120,22 +137,14 @@ class ServerService extends EquipmentService if (!$entity instanceof ServerEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServerEntity만 가능"); } - - try { - if ($entity->getIP()) { - service('part_ipservice')->attachToServer($entity); - } - } catch (\Throwable $e) { - log_message('debug', static::class . '->' . __FUNCTION__ . '에서:' . $e->getMessage()); + if ($entity->getIP()) { + service('part_ipservice')->attachToServer($entity); } - if ($entity->getSwitchInfoUid()) { service('part_switchservice')->attachToServer($entity); } - service('equipment_chassisservice')->attachToServer($entity); service('equipment_serverpartservice')->attachToServer($entity); - return $entity; } @@ -145,22 +154,20 @@ class ServerService extends EquipmentService throw new RuntimeException(static::class . '->' . __FUNCTION__ . '에서 오류발생: 샷시정보가 정의되지 않았습니다.'); } + //기존정보 저장 $oldEntity = clone $entity; + $entity = parent::modify_process($entity, $formDatas); if (!$entity instanceof ServerEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServerEntity만 가능"); } if ($oldEntity->getIP() !== $entity->getIP()) { - try { - if ($oldEntity->getIP()) { - service('part_ipservice')->detachFromServer($oldEntity); - } - if ($entity->getIP()) { - service('part_ipservice')->attachToServer($entity); - } - } catch (\Throwable $e) { - log_message('debug', static::class . '->' . __FUNCTION__ . '에서:' . $e->getMessage()); + if ($oldEntity->getIP()) { + service('part_ipservice')->detachFromServer($oldEntity); + } + if ($entity->getIP()) { + service('part_ipservice')->attachToServer($entity); } } @@ -179,22 +186,51 @@ class ServerService extends EquipmentService service('equipment_chassisservice')->attachToServer($entity); } } - //가격 변동이 있는 경우 if ($oldEntity->getPrice() !== $entity->getPrice()) { - // ✅ 서비스 유지중이면 정상 동기화 (해지 시는 detachFromService에서 따로 처리) - if ($entity->getServiceInfoUid() !== null) { - $serviceService = service('customer_serviceservice'); - $serviceEntity = $serviceService->getEntity($entity->getServiceInfoUid()); - if (!$serviceEntity instanceof ServiceEntity) { - throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$entity->getServiceInfoUid()}에 해당하는 서비스정보을 찾을수 없습니다."); - } - $serviceService->recalcAmountAndSyncPayment($serviceEntity); - } + $this->recalcAmount($entity); } return $entity; } + protected function delete_process($entity): ServerEntity + { + if (!$entity instanceof ServerEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); + } + + //기존정보 우선 저장 + $oldEntity = clone $entity; + + //서버정보 + $entity = parent::delete_process($entity); + if (!$entity instanceof ServerEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); + } + + //서비스연결이 유지된 상태인경우 + if ($entity->getServiceInfoUid() !== null) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:{$entity->getTitle()} 서버는 아직 서비스와 연결된 상태입니다."); + } + //IP정보 + if ($oldEntity->getIP()) { + service('part_ipservice')->detachFromServer($oldEntity); + } + //Switch정보 + if ($oldEntity->getSwitchInfoUid()) { + if (is_int($oldEntity->getSwitchInfoUid())) { //null이거나 공백인경우 + service('part_switchservice')->detachFromServer($oldEntity); + } + } + //샷시정보 + if ($oldEntity->getChassisInfoUid()) { + service('equipment_chassisservice')->detachFromServer($oldEntity); + } + //파트정보 + service('equipment_serverpartservice')->detachFromServer($oldEntity); + return $oldEntity; + } + public function setSearchWord(string $word): void { $this->model->orLike($this->model->getTable() . '.ip', $word, 'both'); @@ -208,11 +244,9 @@ class ServerService extends EquipmentService if (!$entity instanceof ServerEntity) { throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: {$uid}에 해당하는 서버정보을 찾을수 없습니다."); } - $formDatas['serviceinfo_uid'] = $serviceEntity->getPK(); $formDatas["clientinfo_uid"] = $serviceEntity->getClientInfoUid(); $formDatas['status'] = $formDatas['status'] ?? STATUS['OCCUPIED']; - parent::modify_process($entity, $formDatas); } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 7590a24..dc143b4 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -93,41 +93,6 @@ class PaymentService extends CommonService return $walletService; } - // ✅ 서비스 해지(서버 분리) 시: 월 미납 청구는 "삭제/0원수정" 금지, 상태만 TERMINATED - public function terminateUnpaidMonthlyByService(ServiceEntity $oldServiceEntity): void - { - $entity = $this->getEntity([ - 'serviceinfo_uid' => $oldServiceEntity->getPK(), - 'billing' => PAYMENT['BILLING']['MONTH'], - 'billing_at' => $oldServiceEntity->getBillingAt(), - 'status' => STATUS['UNPAID'], - ]); - - if (!$entity instanceof PaymentEntity) { - return; - } - - // amount/title 건드리지 않고 status만 변경 - parent::modify_process($entity, ['status' => STATUS['TERMINATED']]); - } - - //서비스정보로 결제정보 생성 또는 수정 (일반 운영용: upsert 유지) - public function setByService(ServiceEntity $oldServiceEntity, ServiceEntity $serviceEntity): PaymentEntity - { - $formDatas = $this->getFormDatasFromService($serviceEntity); - $entity = $this->getEntity([ - 'serviceinfo_uid' => $oldServiceEntity->getPK(), - 'billing' => PAYMENT['BILLING']['MONTH'], - 'billing_at' => $oldServiceEntity->getBillingAt(), - 'status' => STATUS['UNPAID'] - ]); - //매칭되는게 있으면 - if ($entity instanceof PaymentEntity) { - return $this->modify_process($entity, $formDatas); - } - return $this->create_process($formDatas); - } - //일회성,선결제,쿠폰,포인트 입력 관련 protected function create_process(array $formDatas): PaymentEntity { @@ -251,6 +216,23 @@ class PaymentService extends CommonService $formDatas = $this->getFormDatasFromService($serviceEntity); return $this->create_process($formDatas); } + //서비스정보로 결제정보 생성 또는 수정 (일반 운영용: upsert 유지) + public function modifyByService(ServiceEntity $oldServiceEntity, ServiceEntity $serviceEntity): PaymentEntity + { + $entity = $this->getEntity([ + 'serviceinfo_uid' => $oldServiceEntity->getPK(), + 'billing' => PAYMENT['BILLING']['MONTH'], + 'billing_at' => $oldServiceEntity->getBillingAt(), + 'status' => STATUS['UNPAID'] + ]); + if (!$entity instanceof PaymentEntity) { + throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생:Return Type은 ServiceEntity만 가능"); + } + //매칭되는게 있으면(기존 서비스인경우) 아니면 신규등록 + $formDatas = $this->getFormDatasFromService($serviceEntity); + return $this->modify_process($entity, $formDatas); + } + //서버파트별 일회성 관련 private function getFormDatasFromServerPart(ServerPartEntity $serverPartEntity, array $formDatas = []): array @@ -287,15 +269,26 @@ class PaymentService extends CommonService ]); if (!$entity instanceof PaymentEntity) { - log_message('error', sprintf( - "\n------Last Query (%s)-----\nQuery: %s\n------------------------------\n", - static::class . '->' . __FUNCTION__, - $this->model->getLastQuery() ?? "No Query Available", - )); throw new RuntimeException(static::class . '->' . __FUNCTION__ . "에서 오류발생: 기존 서버파트정보의 {$oldServerPartEntity->getTitle()}에 해당하는 결제정보가 존재하지 않습니다."); } $formDatas = $this->getFormDatasFromServerPart($serverPartEntity); return parent::modify_process($entity, $formDatas); } + + // ✅ 서비스 해지(서버 분리) 시: 월비용 파트정보는 고객만 연결 + public function terminateByServerPart(ServiceEntity $oldServiceEntity): void + { + $entity = $this->getEntity([ + 'serviceinfo_uid' => $oldServiceEntity->getPK(), + 'billing' => PAYMENT['BILLING']['MONTH'], + 'billing_at' => $oldServiceEntity->getBillingAt(), + 'status' => STATUS['UNPAID'], + ]); + if (!$entity instanceof PaymentEntity) { + return; + } + //매칭되는게 있으면(기존 파트정보서비스인경우) + parent::modify_process($entity, ['status' => STATUS['TERMINATED']]); + } }