refactor: KkotalkDocDTO > KkotalkApiDTO로 변경

main
Jonguk. Lim 3 months ago
parent 0e285685b1
commit 32446d7cda

@ -1,7 +1,7 @@
package kr.xit.ens.kakao.v2.service; package kr.xit.ens.kakao.v2.service;
import kr.xit.biz.ens.model.kakao.v2.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO; import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.core.model.ApiResponseDTO; import kr.xit.core.model.ApiResponseDTO;
/** /**
@ -25,8 +25,8 @@ public interface IKkotalkEltrcDocService {
* *
* -. . * -. .
* </pre> * </pre>
* @param reqDTO KkotalkDocDTO.SendRequest * @param reqDTO KkotalkApiDTO.SendRequest
* @return KkotalkDocDTO.SendResponse * @return KkotalkApiDTO.SendResponse
*/ */
KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO); KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO);
@ -34,10 +34,10 @@ public interface IKkotalkEltrcDocService {
* <pre> * <pre>
* (Redirect URL /) * (Redirect URL /)
* </pre> * </pre>
* @param reqDTO KkotalkDocDTO.ValidTokenRequest * @param reqDTO KkotalkApiDTO.ValidTokenRequest
* @return KkotalkDocDTO.ValidTokenResponse> * @return KkotalkApiDTO.ValidTokenResponse>
*/ */
KkotalkDocDTO.ValidTokenResponse validToken(final KkotalkDocDTO.ValidTokenRequest reqDTO); KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.ValidTokenRequest reqDTO);
/** /**
* <pre> * <pre>
@ -62,9 +62,9 @@ public interface IKkotalkEltrcDocService {
* : RECEIVE(, ) > READ()/EXPIRED * : RECEIVE(, ) > READ()/EXPIRED
* </pre> * </pre>
* @param reqDTO KkotalkDTO.EnvelopeId * @param reqDTO KkotalkDTO.EnvelopeId
* @return KkotalkDocDTO.EnvelopeStatusResponse * @return KkotalkApiDTO.EnvelopeStatusResponse
*/ */
KkotalkDocDTO.EnvelopeStatusResponse findStatus(final KkotalkDocDTO.EnvelopeId reqDTO); KkotalkApiDTO.EnvelopeStatusResponse findStatus(final KkotalkApiDTO.EnvelopeId reqDTO);
/** /**
* <pre> * <pre>
@ -90,6 +90,6 @@ public interface IKkotalkEltrcDocService {
KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO); KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO);
ApiResponseDTO<KkotalkDocDTO.ValidTokenResponse> findMyDocReadyAndMblPage(KkotalkDocDTO.ValidTokenRequest reqDTO); ApiResponseDTO<KkotalkApiDTO.ValidTokenResponse> findMyDocReadyAndMblPage(KkotalkApiDTO.ValidTokenRequest reqDTO);
} }

@ -17,8 +17,8 @@ import org.springframework.stereotype.Component;
import kr.xit.biz.common.ApiConstants.SndngSeCode; import kr.xit.biz.common.ApiConstants.SndngSeCode;
import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO; import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO;
import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO; import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO; import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.core.exception.BizRuntimeException; import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.model.ApiResponseDTO; import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.service.AbstractService; import kr.xit.core.service.AbstractService;
@ -91,7 +91,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
errors = validate(reqDTO.getEnvelope(), errors); errors = validate(reqDTO.getEnvelope(), errors);
final KkotalkDocDTO.Envelope envelope = reqDTO.getEnvelope(); final KkotalkApiDTO.Envelope envelope = reqDTO.getEnvelope();
if(envelope.getReviewExpiresAt() != null){ if(envelope.getReviewExpiresAt() != null){
if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){ if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요."); errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
@ -121,7 +121,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
*/ */
@Override @Override
@TraceLogging @TraceLogging
public KkotalkDocDTO.ValidTokenResponse validToken(final KkotalkDocDTO.ValidTokenRequest reqDTO) { public KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.ValidTokenRequest reqDTO) {
validate(reqDTO, null); validate(reqDTO, null);
return webClient.exchangeKkotalk( return webClient.exchangeKkotalk(
@ -130,7 +130,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
.replace("{TOKEN}", reqDTO.getToken()), .replace("{TOKEN}", reqDTO.getToken()),
HttpMethod.valueOf(API_VALID_TOKEN[1]), HttpMethod.valueOf(API_VALID_TOKEN[1]),
null, null,
KkotalkDocDTO.ValidTokenResponse.class, KkotalkApiDTO.ValidTokenResponse.class,
getRlaybsnmInfo(reqDTO)); getRlaybsnmInfo(reqDTO));
} }
@ -163,11 +163,11 @@ public class KkotalkEltrcDocService extends AbstractService implements
* : RECEIVE(,) > READ()/EXPIRED * : RECEIVE(,) > READ()/EXPIRED
* </pre> * </pre>
* @param reqDTO KkotalkDTO.EnvelopeId * @param reqDTO KkotalkDTO.EnvelopeId
* @return KkotalkDocDTO.EnvelopeStatusResponse * @return KkotalkApiDTO.EnvelopeStatusResponse
*/ */
@Override @Override
@TraceLogging @TraceLogging
public KkotalkDocDTO.EnvelopeStatusResponse findStatus(final KkotalkDocDTO.EnvelopeId reqDTO){ public KkotalkApiDTO.EnvelopeStatusResponse findStatus(final KkotalkApiDTO.EnvelopeId reqDTO){
validate(reqDTO, null); validate(reqDTO, null);
String param = "{\"envelopeIds\":" + JsonUtils.toJson(List.of(reqDTO.getEnvelopeId())) + "}"; String param = "{\"envelopeIds\":" + JsonUtils.toJson(List.of(reqDTO.getEnvelopeId())) + "}";
@ -197,9 +197,9 @@ public class KkotalkEltrcDocService extends AbstractService implements
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
List<KkotalkDocDTO.Envelope> envelopes = reqDTO.getEnvelopes(); List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx < envelopes.size(); idx++) { for(int idx = 0; idx < envelopes.size(); idx++) {
final Set<ConstraintViolation<KkotalkDocDTO.Envelope>> list = validator.validate(envelopes.get(idx)); final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> list = validator.validate(envelopes.get(idx));
if (!list.isEmpty()) { if (!list.isEmpty()) {
int finalIdx = idx; int finalIdx = idx;
@ -211,7 +211,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
} }
for(int idx = 0; idx < envelopes.size(); idx++) { for(int idx = 0; idx < envelopes.size(); idx++) {
final KkotalkDocDTO.Envelope envelope = envelopes.get(idx); final KkotalkApiDTO.Envelope envelope = envelopes.get(idx);
if(envelope.getReviewExpiresAt() != null){ if(envelope.getReviewExpiresAt() != null){
if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){ if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
@ -282,13 +282,13 @@ public class KkotalkEltrcDocService extends AbstractService implements
} }
@Override @Override
public ApiResponseDTO<KkotalkDocDTO.ValidTokenResponse> findMyDocReadyAndMblPage(final KkotalkDocDTO.ValidTokenRequest reqDTO) { public ApiResponseDTO<KkotalkApiDTO.ValidTokenResponse> findMyDocReadyAndMblPage(final KkotalkApiDTO.ValidTokenRequest reqDTO) {
final String url = HOST + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId()) final String url = HOST + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId())
.replace("{TOKEN}", reqDTO.getToken()); .replace("{TOKEN}", reqDTO.getToken());
// 유효성 검증 // 유효성 검증
final KkotalkDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, final KkotalkApiDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkotalkDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO)); KkotalkApiDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));
// FIXME: USED ?? // FIXME: USED ??
// if(!"USED".equals(validTokenRes.getStatus())){ // if(!"USED".equals(validTokenRes.getStatus())){
@ -300,7 +300,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
// 정상 : HttpStatus.NO_CONTENT(204) return // 정상 : HttpStatus.NO_CONTENT(204) return
// error : body에 error_code, error_message return // error : body에 error_code, error_message return
final KkotalkDocDTO.KkotalkErrorDTO errorDTO = webClient.exchangeKkotalk(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, KkotalkDocDTO.KkotalkErrorDTO.class, getRlaybsnmInfo(reqDTO)); final KkotalkApiDTO.KkotalkErrorDTO errorDTO = webClient.exchangeKkotalk(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, KkotalkApiDTO.KkotalkErrorDTO.class, getRlaybsnmInfo(reqDTO));
if(errorDTO != null){ if(errorDTO != null){
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage()); return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
} }

@ -10,8 +10,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import kr.xit.biz.ens.model.kakao.v2.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO; import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.core.model.ApiResponseDTO; import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.model.IApiResponse; import kr.xit.core.model.IApiResponse;
import kr.xit.ens.kakao.v2.service.IKkotalkEltrcDocService; import kr.xit.ens.kakao.v2.service.IKkotalkEltrcDocService;
@ -126,13 +126,13 @@ public class KkotalkEltrcDocController {
* 1) API . * 1) API .
* 2) API(/v1/envelopes/${ENVELOPE_ID}/read) read_at ) . * 2) API(/v1/envelopes/${ENVELOPE_ID}/read) read_at ) .
* </pre> * </pre>
* @param reqDTO KkotalkDocDTO.EnvelopeStatusResponse * @param reqDTO KkotalkApiDTO.EnvelopeStatusResponse
* @return ApiResponseDTO<Void> * @return ApiResponseDTO<Void>
*/ */
@Operation(summary = "문서열람처리(문서 상태 변경)", description = "문서열람처리(문서 상태 변경)") @Operation(summary = "문서열람처리(문서 상태 변경)", description = "문서열람처리(문서 상태 변경)")
@PostMapping(value = "/modifyStatus", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/modifyStatus", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse modifyStatus( public IApiResponse modifyStatus(
@RequestBody final KkotalkDocDTO.EnvelopeId reqDTO @RequestBody final KkotalkApiDTO.EnvelopeId reqDTO
) { ) {
service.modifyStatus(reqDTO); service.modifyStatus(reqDTO);
return ApiResponseDTO.empty(); return ApiResponseDTO.empty();
@ -148,12 +148,12 @@ public class KkotalkEltrcDocController {
* : SENT() > RECEIVED() > READ()/EXPIRED( ) * : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre> * </pre>
* @param reqDTO KkotalkDTO.EnvelopeId * @param reqDTO KkotalkDTO.EnvelopeId
* @return ApiResponseDTO<KkotalkDocDTO.EnvelopeStatusResponse> * @return ApiResponseDTO<KkotalkApiDTO.EnvelopeStatusResponse>
*/ */
@Operation(summary = "문서 상태 조회", description = "문서 상태 조회") @Operation(summary = "문서 상태 조회", description = "문서 상태 조회")
@PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse findStatus( public IApiResponse findStatus(
@RequestBody final KkotalkDocDTO.EnvelopeId reqDTO @RequestBody final KkotalkApiDTO.EnvelopeId reqDTO
) { ) {
return ApiResponseDTO.success(service.findStatus(reqDTO)); return ApiResponseDTO.success(service.findStatus(reqDTO));
} }
@ -248,8 +248,8 @@ public class KkotalkEltrcDocController {
* *
* -. . * -. .
* </pre> * </pre>
* @param reqDTO KkotalkDocDTO.BulkStatusRequest * @param reqDTO KkotalkApiDTO.BulkStatusRequest
* @return KkotalkDocDTO.BulkStatusResponse * @return KkotalkApiDTO.BulkStatusResponse
*/ */
@Operation(summary = "대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출") @Operation(summary = "대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출")
@PostMapping(value = "/envelopes/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/envelopes/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE)

@ -35,8 +35,8 @@ import kr.xit.biz.ens.model.EnsKtBcDTO;
import kr.xit.biz.ens.model.cmm.SndngMssageParam; import kr.xit.biz.ens.model.cmm.SndngMssageParam;
import kr.xit.biz.ens.model.kakao.v1.KkopayDocAttrDTO; import kr.xit.biz.ens.model.kakao.v1.KkopayDocAttrDTO;
import kr.xit.biz.ens.model.kakao.v1.KkopayDocBulkDTO; import kr.xit.biz.ens.model.kakao.v1.KkopayDocBulkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO; import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.biz.ens.model.kt.KtCommonDTO.ErrorMsg; import kr.xit.biz.ens.model.kt.KtCommonDTO.ErrorMsg;
import kr.xit.biz.ens.model.kt.KtCommonDTO.KtCommonResponse; import kr.xit.biz.ens.model.kt.KtCommonDTO.KtCommonResponse;
import kr.xit.biz.ens.model.kt.KtMmsSendDTO.KtMainSendReqData; import kr.xit.biz.ens.model.kt.KtMmsSendDTO.KtMainSendReqData;
@ -260,7 +260,7 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
if(list.isEmpty()) return; if(list.isEmpty()) return;
final String mstId = list.get(0).getSndngMastrId(); final String mstId = list.get(0).getSndngMastrId();
final List<KkotalkDocDTO.Envelope> bulkList = setKkotalkSendBulks(list); final List<KkotalkApiDTO.Envelope> bulkList = setKkotalkSendBulks(list);
// validation check // validation check
try { try {
@ -270,7 +270,7 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
throw e; throw e;
} }
final List<List<KkotalkDocDTO.Envelope>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt); final List<List<KkotalkApiDTO.Envelope>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt);
//noinspection rawtypes //noinspection rawtypes
final List<ApiResponseDTO> apiResults = partitions.stream() final List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> apiWebClient.exchange( .map(bulkSendList -> apiWebClient.exchange(
@ -367,24 +367,24 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
* @param list List<EnsDTO.SendKakaoNewTgt> * @param list List<EnsDTO.SendKakaoNewTgt>
* @return List<KkoNewDocAttrDTO.Send> * @return List<KkoNewDocAttrDTO.Send>
*/ */
private static List<KkotalkDocDTO.Envelope> setKkotalkSendBulks(List<EnsDTO.SendKakaotalkTgt> list) { private static List<KkotalkApiDTO.Envelope> setKkotalkSendBulks(List<EnsDTO.SendKakaotalkTgt> list) {
final List<KkotalkDocDTO.Envelope> bulkList = new ArrayList<>(); final List<KkotalkApiDTO.Envelope> bulkList = new ArrayList<>();
for (EnsDTO.SendKakaotalkTgt sendTgtDTO : list) { for (EnsDTO.SendKakaotalkTgt sendTgtDTO : list) {
/* /*
: CI : CI
*/ */
KkotalkDocDTO.Envelope bulkReqDTO = null; KkotalkApiDTO.Envelope bulkReqDTO = null;
if(StringUtils.isNotEmpty(sendTgtDTO.getCi())){ if(StringUtils.isNotEmpty(sendTgtDTO.getCi())){
bulkReqDTO = KkotalkDocDTO.Envelope.builder() bulkReqDTO = KkotalkApiDTO.Envelope.builder()
.ci(sendTgtDTO.getCi()) .ci(sendTgtDTO.getCi())
.build(); .build();
}else{ }else{
bulkReqDTO = KkotalkDocDTO.Envelope.builder() bulkReqDTO = KkotalkApiDTO.Envelope.builder()
.build(); .build();
} }
final KkotalkDocDTO.Content content = KkotalkDocDTO.Content.builder() final KkotalkApiDTO.Content content = KkotalkApiDTO.Content.builder()
.link(sendTgtDTO.getContentLink()) .link(sendTgtDTO.getContentLink())
.build(); .build();
@ -459,12 +459,12 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
* *
* @param bulkList List<KkoNewDocBulkDTO.Send> * @param bulkList List<KkoNewDocBulkDTO.Send>
*/ */
private void validatedKkotalkSendBulks(List<KkotalkDocDTO.Envelope> bulkList) { private void validatedKkotalkSendBulks(List<KkotalkApiDTO.Envelope> bulkList) {
List<String> errors = new ArrayList<>(); List<String> errors = new ArrayList<>();
int idx = 0; int idx = 0;
for (KkotalkDocDTO.Envelope dto : bulkList) { for (KkotalkApiDTO.Envelope dto : bulkList) {
final Set<ConstraintViolation<KkotalkDocDTO.Envelope>> errList = validator.validate(dto); final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> errList = validator.validate(dto);
if(!errList.isEmpty()) { if(!errList.isEmpty()) {
int finalIdx = idx; int finalIdx = idx;

@ -25,7 +25,7 @@ import lombok.experimental.SuperBuilder;
* API json . * API json .
* => @JsonInclude(JsonInclude.Include.NON_EMPTY) * => @JsonInclude(JsonInclude.Include.NON_EMPTY)
* packageName : kr.xit.biz.ens.model.kakao.v2 * packageName : kr.xit.biz.ens.model.kakao.v2
* fileName : KkotalkDocDTO * fileName : KkotalkApiDTO
* author : limju * author : limju
* date : 2024-08-12 * date : 2024-08-12
* ====================================================================== * ======================================================================
@ -35,7 +35,7 @@ import lombok.experimental.SuperBuilder;
* *
* </pre> * </pre>
*/ */
public class KkotalkDocDTO { public class KkotalkApiDTO {
//------------------- Envelope ------------------------------------------------------------------------------------------------ //------------------- Envelope ------------------------------------------------------------------------------------------------
@Schema(name = "Envelope", description = "문서발송(단건) 요청 파라메터 DTO") @Schema(name = "Envelope", description = "문서발송(단건) 요청 파라메터 DTO")

@ -30,7 +30,7 @@ import lombok.experimental.SuperBuilder;
* *
* </pre> * </pre>
*/ */
public class KkotalkDTO extends KkotalkDocDTO { public class KkotalkDTO extends KkotalkApiDTO {
//------------------ envelop ---------------------------------------------------------------------- //------------------ envelop ----------------------------------------------------------------------
@Schema(name = "SendRequest DTO", description = "문서발송 request DTO") @Schema(name = "SendRequest DTO", description = "문서발송 request DTO")
@ -109,7 +109,7 @@ public class KkotalkDTO extends KkotalkDocDTO {
@AllArgsConstructor @AllArgsConstructor
@SuperBuilder @SuperBuilder
public static class BulkStatusResponse implements IApiResponse { public static class BulkStatusResponse implements IApiResponse {
private List<KkotalkDocDTO.EnvelopeStatusResponse> envelopeStatus; private List<KkotalkApiDTO.EnvelopeStatusResponse> envelopeStatus;
} }
//------------------ bulk ---------------------------------------------------------------------- //------------------ bulk ----------------------------------------------------------------------

Loading…
Cancel
Save