SUPPORT-9339: Fix

This commit is contained in:
Eduard Tihomiorv 2025-10-01 18:44:14 +03:00
parent ede5bf85a0
commit ab7cdbb6b9
12 changed files with 223 additions and 124 deletions

View file

@ -101,7 +101,7 @@ public class WebDavClient {
String key = entry.getKey();
MultipartFile file = entry.getValue();
if (file != null) {
result.put(key, putAndGetUrl(file.getBytes(), file.getOriginalFilename(), sardine, fileCatalog));
result.put(key, putAndGetUrl(file.getBytes(), getNewFilename(file.getOriginalFilename()), sardine, fileCatalog));
}
}
return result;
@ -119,6 +119,12 @@ public class WebDavClient {
}
}
private String getNewFilename(String filename) {
int lastIndexOf = filename.lastIndexOf(".");
String fileExtension = lastIndexOf == -1 ? "" : filename.substring(lastIndexOf);
return UUID.randomUUID() + fileExtension;
}
public String putAndGetUrl(byte[] fileBytes, String fileName, Sardine client, String fileCatalog) throws IOException {
if (badServersCache.size() == urls.length) {
return null;

View file

@ -8,7 +8,9 @@ public enum FileStatusCode {
FILE_INFECTED("02"),
FILE_CLEAN("03"),
FILE_ACCEPTED("04"),
FILE_NOT_CHECKED("11");
FILE_NOT_CHECKED("11"),
SIGN_INVALID("12"),
MCHD_INVALID("13");
private final String code;

View file

@ -70,6 +70,8 @@ import static ervu.enums.FileStatusCode.FILE_ACCEPTED;
import static ervu.enums.FileStatusCode.FILE_INFECTED;
import static ervu.enums.FileStatusCode.FILE_NOT_CHECKED;
import static ervu.enums.FileStatusCode.FILE_UPLOADED;
import static ervu.enums.FileStatusCode.MCHD_INVALID;
import static ervu.enums.FileStatusCode.SIGN_INVALID;
import static ru.micord.ervu.util.StringUtils.convertToFio;
/**
@ -79,6 +81,8 @@ import static ru.micord.ervu.util.StringUtils.convertToFio;
public class EmployeeInfoFileUploadService {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeInfoFileUploadService.class);
private static final String DOCUMENT = "DOCUMENT";
private static final String SIGNATURE = "SIGNATURE";
private static final String MCHD = "MCHD";
private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor(
"messages/common_errors_messages");
private final WebDavClient webDavClient;
@ -157,11 +161,11 @@ public class EmployeeInfoFileUploadService {
departureDateTime, offset, DOCUMENT
);
FileInfo signFileInfo = createFileInfo(signFile, employeeInfoFileFormType,
departureDateTime, offset, "SIGNATURE"
departureDateTime, offset, SIGNATURE
);
FileInfo mchdFileInfo = mchdFile != null ?
createFileInfo(mchdFile, employeeInfoFileFormType, departureDateTime,
offset, "MCHD"
offset, MCHD
) : null;
FileStatus fileStatus = new FileStatus();
@ -219,32 +223,35 @@ public class EmployeeInfoFileUploadService {
interactionService.updateStatus(fileInfo.getFileId(), fileInfo.getFileStatus().getStatus(),
uploadOrgInfo.getOrgId()
);
auditService.processUploadEvent(uploadOrgInfo, fileInfos);
auditService.processUploadEvent(uploadOrgInfo, fileInfos, fileStatus);
throw new FileUploadException(e);
}
fileInfo = Arrays.stream(downloadResponse.filesInfo())
.filter(file -> DOCUMENT.equals(file.getType()))
.findFirst()
.orElse(fileInfo);
signFileInfo = Arrays.stream(downloadResponse.filesInfo())
.filter(file -> SIGNATURE.equals(file.getType()))
.findFirst()
.orElse(signFileInfo);
mchdFileInfo = Arrays.stream(downloadResponse.filesInfo())
.filter(file -> MCHD.equals(file.getType()))
.findFirst()
.orElse(mchdFileInfo);
VerifyDocumentSignResponse verifyDocumentSignResponse;
try {
verifyDocumentSignResponse = validateSign(multipartFile, signFile);
}
catch (Exception e) {
fileStatus.setCode(FILE_NOT_CHECKED.getCode());
fileStatus.setStatus("Некорректная ЭП");
fileStatus.setDescription("Не пройдена проверка ЭП");
Arrays.stream(downloadResponse.filesInfo())
.forEach(fileInfo1 -> fileInfo1.setFileStatus(fileStatus));
interactionService.updateStatus(fileInfo.getFileId(), fileInfo.getFileStatus().getStatus(),
uploadOrgInfo.getOrgId()
);
auditService.processUploadEvent(uploadOrgInfo, new FileInfo[] {fileInfo, signFileInfo});
clearS3(response);
handeSignError(fileInfo, signFileInfo, uploadOrgInfo, response);
throw e;
}
return validateSignerAndMchd(verifyDocumentSignResponse, chiefModel, uploadOrgInfo,
mchdFile, accessToken, fileStatus, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
mchdFile, accessToken, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
);
}
@ -295,7 +302,7 @@ public class EmployeeInfoFileUploadService {
new FileInfo[] {fileInfo, signFileInfo, mchdFileInfo} :
new FileInfo[] {fileInfo, signFileInfo};
auditService.processUploadEvent(uploadOrgInfo, fileInfos);
auditService.processUploadEvent(uploadOrgInfo, fileInfos, fileStatus);
}
private String sendKafkaMessage(UploadOrgInfo uploadOrgInfo, FileInfo[] fileInfos,
@ -315,7 +322,7 @@ public class EmployeeInfoFileUploadService {
private boolean validateSignerAndMchd(VerifyDocumentSignResponse verifyDocumentSignResponse,
EmployeeModel chiefModel, UploadOrgInfo uploadOrgInfo, MultipartFile mchdFile,
String accessToken, FileStatus fileStatus, FileInfo fileInfo, FileInfo signFileInfo,
String accessToken, FileInfo fileInfo, FileInfo signFileInfo,
FileInfo mchdFileInfo, String ervuId, String response) {
String signerInfo = verifyDocumentSignResponse.getSignerInfo();
@ -332,45 +339,68 @@ public class EmployeeInfoFileUploadService {
if (isSignerValid) {
interactionService.updateStatus(fileInfo.getFileId(), "Направлено в ЕРВУ", ervuId);
sendMessage(response);
return true;
FileStatusResponse fileStatusResponse = new FileStatusResponse(uploadOrgInfo,
new FileInfo[] {fileInfo, signFileInfo}, fileInfo.getFileStatus()
);
try {
sendMessage(fileStatusResponse);
return true;
}
catch (JsonProcessingException e) {
handeSignError(fileInfo, signFileInfo, uploadOrgInfo, response);
throw new JsonParsingException(
String.format("Fail get json from: %s", fileStatusResponse), e);
}
}
if (mchdFile == null) {
handleMchdValidationError(fileStatus, uploadOrgInfo, fileInfo, signFileInfo, null, ervuId, response
handleMchdValidationError(uploadOrgInfo, fileInfo, signFileInfo, null, ervuId, response
);
throw new LocalizedException("mchd_null", MESSAGE_SOURCE);
}
FileStatusResponse fileStatusResponse = new FileStatusResponse(uploadOrgInfo,
new FileInfo[] {fileInfo, signFileInfo, mchdFileInfo}, fileInfo.getFileStatus()
);
try {
validateMchd(mchdFile, accessToken, signerInfoMap.get("SN") + " " + signerInfoMap.get("G"),
chiefFirstName, chiefLastName, chiefMiddleName
);
interactionService.updateStatus(fileInfo.getFileId(), "Направлено в ЕРВУ", ervuId);
sendMessage(response);
sendMessage(fileStatusResponse);
return true;
}
catch (JsonProcessingException e) {
handleMchdValidationError(uploadOrgInfo, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
);
throw new JsonParsingException(
String.format("Fail get json from: %s", fileStatusResponse), e);
}
catch (Exception e) {
handleMchdValidationError(fileStatus, uploadOrgInfo, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
handleMchdValidationError(uploadOrgInfo, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
);
throw e;
}
}
private void handleMchdValidationError(FileStatus fileStatus, UploadOrgInfo uploadOrgInfo,
private void handleMchdValidationError(UploadOrgInfo uploadOrgInfo,
FileInfo fileInfo, FileInfo signFileInfo, FileInfo mchdFileInfo, String ervuId, String response) {
FileStatus packInfo = new FileStatus();
packInfo.setCode(MCHD_INVALID.getCode());
packInfo.setStatus("Некорректная МЧД");
packInfo.setDescription("Проверка МЧД не пройдена");
fileStatus.setCode(FILE_NOT_CHECKED.getCode());
fileStatus.setStatus("Некорректная МЧД");
fileStatus.setDescription("Проверка МЧД не пройдена");
interactionService.updateStatus(fileInfo.getFileId(), packInfo.getStatus(), ervuId);
FileInfo[] fileInfos;
if (mchdFileInfo != null) {
mchdFileInfo.setFileStatus(packInfo);
fileInfos = new FileInfo[] {fileInfo, signFileInfo, mchdFileInfo};
}
else {
fileInfos = new FileInfo[] {fileInfo, signFileInfo};
}
interactionService.updateStatus(fileInfo.getFileId(), fileStatus.getStatus(), ervuId);
FileInfo[] fileInfos = mchdFileInfo != null ?
new FileInfo[] {fileInfo, signFileInfo, mchdFileInfo} :
new FileInfo[] {fileInfo, signFileInfo};
auditService.processUploadEvent(uploadOrgInfo, fileInfos);
auditService.processUploadEvent(uploadOrgInfo, fileInfos, packInfo);
clearS3(response);
}
@ -416,9 +446,8 @@ public class EmployeeInfoFileUploadService {
}
private String getJsonKafkaMessage(EmployeeInfoKafkaMessage employeeInfoKafkaMessage) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(employeeInfoKafkaMessage);
return objectMapper.writeValueAsString(employeeInfoKafkaMessage);
}
catch (JsonProcessingException e) {
throw new JsonParsingException(
@ -557,7 +586,8 @@ public class EmployeeInfoFileUploadService {
}
}
private boolean sendMessage(String message) {
private boolean sendMessage(FileStatusResponse fileStatusResponse) throws JsonProcessingException {
String message = objectMapper.writeValueAsString(fileStatusResponse);
ProducerRecord<String, String> record = new ProducerRecord<>(this.kafkaDownloadRequestTopic, message);
record.headers()
.add("messageId", UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
@ -595,15 +625,33 @@ public class EmployeeInfoFileUploadService {
if (fileStatus1.getCode().equals(FILE_INFECTED.getCode())) {
interactionService.updateStatus(avFile.getFileId(), avFile.getFileStatus().getStatus(),
downloadResponse.orgInfo().getOrgId());
sendMessage(response);
FileStatusResponse fileStatusResponse = new FileStatusResponse(downloadResponse.orgInfo(),
downloadResponse.filesInfo(), fileStatus1
);
sendMessage(fileStatusResponse);
throw new LocalizedException("av_file_infected", MESSAGE_SOURCE);
}
else if (fileStatus1.getCode().equals(FILE_NOT_CHECKED.getCode())) {
interactionService.updateStatus(avFile.getFileId(), avFile.getFileStatus().getStatus(),
downloadResponse.orgInfo().getOrgId());
auditService.processUploadEvent(downloadResponse.orgInfo(), downloadResponse.filesInfo());
auditService.processUploadEvent(downloadResponse.orgInfo(), downloadResponse.filesInfo(), fileStatus1);
throw new FileUploadException("File not checked: " + avFile.getFileName());
}
return downloadResponse;
}
private void handeSignError(FileInfo fileInfo, FileInfo signFileInfo, UploadOrgInfo uploadOrgInfo, String response) {
FileStatus packInfo = new FileStatus();
packInfo.setCode(SIGN_INVALID.getCode());
packInfo.setStatus("Некорректная ЭП");
packInfo.setDescription("Не пройдена проверка ЭП");
signFileInfo.setFileStatus(packInfo);
interactionService.updateStatus(fileInfo.getFileId(), packInfo.getStatus(),
uploadOrgInfo.getOrgId()
);
auditService.processUploadEvent(uploadOrgInfo, new FileInfo[] {fileInfo, signFileInfo}, packInfo);
clearS3(response);
}
}

View file

@ -2,6 +2,7 @@ package ru.micord.ervu.audit.model;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.UploadOrgInfo;
/**
@ -11,10 +12,12 @@ import ervu.model.fileupload.UploadOrgInfo;
public class AuditUploadEvent {
private UploadOrgInfo orgInfo;
private FileInfo[] filesInfo;
private FileStatus packInfo;
public AuditUploadEvent(UploadOrgInfo orgInfo, FileInfo[] filesInfo) {
public AuditUploadEvent(UploadOrgInfo orgInfo, FileInfo[] filesInfo, FileStatus packInfo) {
this.orgInfo = orgInfo;
this.filesInfo = filesInfo;
this.packInfo = packInfo;
}
public UploadOrgInfo getOrgInfo() {
@ -32,4 +35,12 @@ public class AuditUploadEvent {
public void setFilesInfo(FileInfo[] filesInfo) {
this.filesInfo = filesInfo;
}
public FileStatus getPackInfo() {
return packInfo;
}
public void setPackInfo(FileStatus packInfo) {
this.packInfo = packInfo;
}
}

View file

@ -3,6 +3,7 @@ package ru.micord.ervu.audit.service;
import javax.servlet.http.HttpServletRequest;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.UploadOrgInfo;
import ru.micord.ervu.audit.model.AuditActionRequest;
import ru.micord.ervu.kafka.model.OrgInfo;
@ -16,7 +17,7 @@ public interface AuditService {
void processAuthEvent(HttpServletRequest request, OrgInfo orgInfo, String prnOid, String status,
String eventType);
void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo[] filesInfo);
void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo[] filesInfo, FileStatus packInfo);
void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName,
int formatRegistry, String status, String s3FileUrl);

View file

@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.UploadOrgInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Conditional;
@ -110,10 +111,11 @@ public class BaseAuditService implements AuditService {
}
@Override
public void processUploadEvent(UploadOrgInfo orgInfo, FileInfo[] filesInfo) {
public void processUploadEvent(UploadOrgInfo orgInfo, FileInfo[] filesInfo, FileStatus packInfo) {
AuditUploadEvent auditUploadEvent = new AuditUploadEvent(
orgInfo,
filesInfo
filesInfo,
packInfo
);
String message = convertToMessage(auditUploadEvent);

View file

@ -3,6 +3,7 @@ package ru.micord.ervu.audit.service.impl;
import javax.servlet.http.HttpServletRequest;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.UploadOrgInfo;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
@ -26,7 +27,7 @@ public class StubAuditService implements AuditService {
String status, String eventType) {}
@Override
public void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo[] filesInfo) {}
public void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo[] filesInfo, FileStatus packInfo) {}
@Override
public void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName,

View file

@ -1,6 +1,7 @@
package ru.micord.ervu.journal;
import java.time.LocalDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@ -9,78 +10,17 @@ import ru.micord.ervu.journal.deserializer.DepartureDateTimeDeserializer;
@JsonIgnoreProperties(ignoreUnknown = true)
public class JournalFileInfo {
private String fileId; //ИД файла полученный при создании записи о файле в реестр организаций (в ЕРВУ)
private String fileName; // Название файла
private Integer filePatternCode; // Номер шаблона(Формы)
private String filePatternName;
@JsonDeserialize(using = DepartureDateTimeDeserializer.class)
private LocalDateTime departureDateTime; // Дата-время отправки файла
private String timeZone; //Таймзона
private JournalFileStatus fileStatus;
private List<JournalFileDetails> packFiles; // Список файлов (csv, sig, mchd)
private SenderInfo senderInfo;
private Integer rowsCount; //Общее количество записей отправленных в файле
private Integer rowsSuccess; //Количество записей принятых в файле
public String getFileId() {
return fileId;
public List<JournalFileDetails> getPackFiles() {
return packFiles;
}
public JournalFileInfo setFileId(String fileId) {
this.fileId = fileId;
return this;
}
public String getFileName() {
return fileName;
}
public JournalFileInfo setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Integer getFilePatternCode() {
return filePatternCode;
}
public JournalFileInfo setFilePatternCode(Integer filePatternCode) {
this.filePatternCode = filePatternCode;
return this;
}
public String getFilePatternName() {
return filePatternName;
}
public JournalFileInfo setFilePatternName(String filePatternName) {
this.filePatternName = filePatternName;
return this;
}
public LocalDateTime getDepartureDateTime() {
return departureDateTime;
}
public JournalFileInfo setDepartureDateTime(LocalDateTime departureDateTime) {
this.departureDateTime = departureDateTime;
return this;
}
public String getTimeZone() {
return timeZone;
}
public JournalFileInfo setTimeZone(String timeZone) {
this.timeZone = timeZone;
return this;
}
public JournalFileStatus getFileStatus() {
return fileStatus;
}
public JournalFileInfo setFileStatus(JournalFileStatus fileStatus) {
this.fileStatus = fileStatus;
public JournalFileInfo setPackFiles(List<JournalFileDetails> packFiles) {
this.packFiles = packFiles;
return this;
}
@ -110,4 +50,88 @@ public class JournalFileInfo {
this.rowsSuccess = rowsSuccess;
return this;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class JournalFileDetails {
private String fileId; //ИД файла полученный при создании записи о файле в реестр организаций (в ЕРВУ)
private String fileName; // Название файла
private Integer filePatternCode; // Номер шаблона(Формы)
private String filePatternName;
@JsonDeserialize(using = DepartureDateTimeDeserializer.class)
private LocalDateTime departureDateTime; // Дата-время отправки файла
private String timeZone; //Таймзона
private JournalFileStatus fileStatus;
private String type;
public String getFileId() {
return fileId;
}
public JournalFileDetails setFileId(String fileId) {
this.fileId = fileId;
return this;
}
public String getFileName() {
return fileName;
}
public JournalFileDetails setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Integer getFilePatternCode() {
return filePatternCode;
}
public JournalFileDetails setFilePatternCode(Integer filePatternCode) {
this.filePatternCode = filePatternCode;
return this;
}
public String getFilePatternName() {
return filePatternName;
}
public JournalFileDetails setFilePatternName(String filePatternName) {
this.filePatternName = filePatternName;
return this;
}
public LocalDateTime getDepartureDateTime() {
return departureDateTime;
}
public JournalFileDetails setDepartureDateTime(LocalDateTime departureDateTime) {
this.departureDateTime = departureDateTime;
return this;
}
public String getTimeZone() {
return timeZone;
}
public JournalFileDetails setTimeZone(String timeZone) {
this.timeZone = timeZone;
return this;
}
public JournalFileStatus getFileStatus() {
return fileStatus;
}
public JournalFileDetails setFileStatus(JournalFileStatus fileStatus) {
this.fileStatus = fileStatus;
return this;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
}

View file

@ -11,18 +11,19 @@ import static ru.micord.ervu.util.StringUtils.convertToFio;
public class JournalDtoMapper {
public static JournalDto mapToJournalDto(JournalFileInfo journalFileInfo) {
public static JournalDto mapToJournalDto(JournalFileInfo journalFileInfo,
JournalFileInfo.JournalFileDetails journalFileDetails) {
SenderInfo senderInfo = journalFileInfo.getSenderInfo();
return new JournalDto()
.setFileId(journalFileInfo.getFileId())
.setDepartureDateTime(Timestamp.valueOf(journalFileInfo.getDepartureDateTime()).toString())
.setFileName(journalFileInfo.getFileName())
.setFilePatternCode(journalFileInfo.getFilePatternCode())
.setFileId(journalFileDetails.getFileId())
.setDepartureDateTime(Timestamp.valueOf(journalFileDetails.getDepartureDateTime()).toString())
.setFileName(journalFileDetails.getFileName())
.setFilePatternCode(journalFileDetails.getFilePatternCode())
.setSenderFio(convertToFio(senderInfo.getFirstName(), senderInfo.getMiddleName(),
senderInfo.getLastName()
)
)
.setStatus(journalFileInfo.getFileStatus().getStatus())
.setStatus(journalFileDetails.getFileStatus().getStatus())
.setFilesSentCount(journalFileInfo.getRowsCount())
.setAcceptedFilesCount(journalFileInfo.getRowsSuccess());
}

View file

@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.service.AuditService;
@ -48,7 +48,7 @@ public class ErvuKafkaController {
@Autowired
private ObjectMapper objectMapper;
@RequestMapping(value = "/kafka/excerpt")
@GetMapping(value = "/kafka/excerpt")
public ResponseEntity<Resource> getExcerptFile(HttpServletRequest request) {
String fileUrl = null;
String fileName = null;

View file

@ -68,7 +68,10 @@ public class JournalInMemoryStaticGridLoadService implements
JournalFileDataResponse journalFileDataResponse = objectMapper.readValue(responseJsonString,
JournalFileDataResponse.class);
ervuJournalList = journalFileDataResponse.getFilesInfo().stream()
.map(JournalDtoMapper::mapToJournalDto)
.flatMap(fileInfo -> fileInfo.getPackFiles().stream()
.filter(fileDetail -> "DOCUMENT".equals(fileDetail.getType()))
.map(fileDetail -> JournalDtoMapper.mapToJournalDto(fileInfo, fileDetail))
)
.toList();
}
catch (JsonProcessingException e) {

View file

@ -49,7 +49,7 @@ export class SetFilter implements IFilterComp {
private initCheckBox(id: string, value: string, index: number): HTMLInputElement {
this.eGui.insertAdjacentHTML('beforeend', this.OPTION_TEMPLATE);
this.eGui.querySelectorAll('.ag-filter-value')[index].innerHTML = value;
(this.eGui.querySelectorAll('.ag-filter-value')[index] as HTMLElement).textContent = value;
let checkbox = this.eGui.querySelectorAll('.ag-filter-checkbox')[index] as HTMLInputElement;
checkbox.setAttribute('id', id);
checkbox.addEventListener('change', this.onCheckBoxChanged.bind(this));