diff --git a/migration/1770470071165-AddUserDataVerificationCallTime.js b/migration/1770470071165-AddUserDataVerificationCallTime.js new file mode 100644 index 0000000000..48ae8a5240 --- /dev/null +++ b/migration/1770470071165-AddUserDataVerificationCallTime.js @@ -0,0 +1,28 @@ +/** + * @typedef {import('typeorm').MigrationInterface} MigrationInterface + * @typedef {import('typeorm').QueryRunner} QueryRunner + */ + +/** + * @class + * @implements {MigrationInterface} + */ +module.exports = class AddUserDataVerificationCallTime1770470071165 { + name = 'AddUserDataVerificationCallTime1770470071165' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" ADD "phoneCallTimes" nvarchar(256)`); + await queryRunner.query(`ALTER TABLE "user_data" ADD "phoneCallStatus" nvarchar(256)`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "phoneCallStatus"`); + await queryRunner.query(`ALTER TABLE "user_data" DROP COLUMN "phoneCallTimes"`); + } +} diff --git a/src/integration/sift/dto/sift.dto.ts b/src/integration/sift/dto/sift.dto.ts index 843a139f0b..b566de0c69 100644 --- a/src/integration/sift/dto/sift.dto.ts +++ b/src/integration/sift/dto/sift.dto.ts @@ -1034,6 +1034,7 @@ export const SiftAmlDeclineMap: { [method in AmlReason]: DeclineCategory } = { [AmlReason.MANUAL_CHECK_PHONE]: DeclineCategory.RISKY, [AmlReason.MANUAL_CHECK_IP_PHONE]: DeclineCategory.RISKY, [AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE]: DeclineCategory.RISKY, + [AmlReason.MANUAL_CHECK_PHONE_REJECTED]: DeclineCategory.RISKY, [AmlReason.BANK_RELEASE_PENDING]: DeclineCategory.OTHER, [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: DeclineCategory.RISKY, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: DeclineCategory.RISKY, diff --git a/src/shared/i18n/de/mail.json b/src/shared/i18n/de/mail.json index aaa27b0fe0..71fbcbf791 100644 --- a/src/shared/i18n/de/mail.json +++ b/src/shared/i18n/de/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Für die Transaktion werden deine KYC Daten benötigt", "bank_tx_needed": "Um diese Transaktion auszuführen ist zuvor eine Banktransaktion erforderlich", - "manual_check_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", - "manual_check_ip_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", - "manual_check_ip_country_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen", + "manual_check_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Wir konnten dich unter deiner angegebenen Telefonnummer nicht erreichen. Du kannst einen erneuten Anruf und eine bevorzugte Uhrzeit für das Telefongespräch selber hier beantragen: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "Das Telefonat war nicht erfolgreich oder wurde abgelehnt", "merge_incomplete": "Die Email Bestätigung wurde nicht akzeptiert", "intermediary_without_sender": "Die Absenderbank (Wise/Revolut) hat nur den Banknamen übermittelt, nicht aber den Namen des Kontoinhabers. DFX kann daher den tatsächlichen Absender nicht verifizieren und die Transaktion nicht verarbeiten.", "name_too_short": "Dein Name ist zu kurz für die Bankverarbeitung. Banken benötigen mindestens 4 Buchstaben im Namen des Kontoinhabers." @@ -215,7 +216,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Wir haben deine Einzahlung erhalten.", "line2": "Wir werden uns in Kürze auf der Telefonnummer {phone} bei dir melden.", "line3": "Wenn alle Fragen geklärt sind, wird deine Transaktion automatisch weiterverarbeitet.", - "line4": "", + "line4": "Du kannst eine bevorzugte Uhrzeit für das Telefongespräch selber hier angeben: [url:https://app.dfx.swiss/settings]", "line5": "Wenn du stattdessen eine Rückzahlung anfordern möchtest:
[url:Klick hier]" } }, diff --git a/src/shared/i18n/en/mail.json b/src/shared/i18n/en/mail.json index db40214c22..868cb87c2e 100644 --- a/src/shared/i18n/en/mail.json +++ b/src/shared/i18n/en/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Your KYC data is required for the transaction", "bank_tx_needed": "A bank transaction is required before this transaction can be carried out", - "manual_check_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided", + "manual_check_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "The phone call was unsuccessful or rejected", "merge_incomplete": "The email confirmation was not accepted", "intermediary_without_sender": "The sender bank (Wise/Revolut) only transmitted the bank name, not the account holder's name. DFX is therefore unable to verify the actual sender and cannot process the transaction.", "name_too_short": "Your name is too short for bank processing. Banks require at least 4 letters in the account holder name." @@ -215,7 +216,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" } }, diff --git a/src/shared/i18n/es/mail.json b/src/shared/i18n/es/mail.json index d1a3aa8c6e..76c11bded5 100644 --- a/src/shared/i18n/es/mail.json +++ b/src/shared/i18n/es/mail.json @@ -91,9 +91,10 @@ "test_only": "Prueba", "kyc_data_needed": "Sus datos KYC son necesarios para la transacción", "bank_tx_needed": "Para poder realizar esta operación es necesaria una transacción bancaria", - "manual_check_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", - "manual_check_ip_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", - "manual_check_ip_country_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó", + "manual_check_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "No hemos podido contactar con usted al número de teléfono que nos facilitó. Aquí puede solicitar una llamada y elegir la hora que prefiera para recibirla: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "La llamada telefónica no se ha podido realizar o ha sido rechazada.", "merge_incomplete": "El correo electrónico de confirmación no fue aceptado", "intermediary_without_sender": "El banco emisor (Wise/Revolut) solo transmitió el nombre del banco, no el nombre del titular de la cuenta. Por lo tanto, DFX no puede verificar el remitente real y no puede procesar la transacción.", "name_too_short": "Tu nombre es demasiado corto para el procesamiento bancario. Los bancos requieren al menos 4 letras en el nombre del titular de la cuenta." @@ -215,7 +216,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Hemos recibido su depósito.", "line2": "Nos pondremos en contacto con usted en breve en el {phone}.", "line3": "Una vez que se hayan aclarado todas las preguntas, su transacción se procesará automáticamente.", - "line4": "", + "line4": "Aquí puede especificar la hora que prefiera para la llamada telefónica: [url:https://app.dfx.swiss/settings]", "line5": "Si desea solicitar un reembolso en su lugar:
[url:haga clic aquí]" } }, diff --git a/src/shared/i18n/fr/mail.json b/src/shared/i18n/fr/mail.json index a4c7ef800d..a855e0cfc8 100644 --- a/src/shared/i18n/fr/mail.json +++ b/src/shared/i18n/fr/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Vos données KYC sont nécessaires pour la transaction", "bank_tx_needed": "Une transaction bancaire est nécessaire pour que cette opération puisse être effectuée", - "manual_check_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", - "manual_check_ip_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", - "manual_check_ip_country_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni", + "manual_check_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Nous n'avons pas réussi à vous joindre au numéro de téléphone que vous avez fourni. Vous pouvez demander ici à être rappelé et indiquer l'heure à laquelle vous souhaitez recevoir l'appel: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "L'appel téléphonique n'a pas abouti ou a été rejeté", "merge_incomplete": "L'e-mail de confirmation n'a pas été accepté", "intermediary_without_sender": "La banque émettrice (Wise/Revolut) n'a transmis que le nom de la banque, et non le nom du titulaire du compte. DFX ne peut donc pas vérifier l'expéditeur réel et ne peut pas traiter la transaction.", "name_too_short": "Votre nom est trop court pour le traitement bancaire. Les banques exigent au moins 4 lettres dans le nom du titulaire du compte." @@ -215,7 +216,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Nous avons bien reçu votre acompte.", "line2": "Nous vous contacterons sous peu au {phone}.", "line3": "Une fois toutes les questions clarifiées, votre transaction sera traitée automatiquement.", - "line4": "", + "line4": "Vous pouvez indiquer ici l'heure à laquelle vous souhaitez recevoir l'appel téléphonique: [url:https://app.dfx.swiss/settings]", "line5": "Si vous souhaitez demander un remboursement:
[url:cliquez ici]" } }, diff --git a/src/shared/i18n/it/mail.json b/src/shared/i18n/it/mail.json index 1f341d742b..17143376c3 100644 --- a/src/shared/i18n/it/mail.json +++ b/src/shared/i18n/it/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "I dati KYC sono necessari per la transazione", "bank_tx_needed": "Per effettuare questa transazione è necessaria una transazione bancaria", - "manual_check_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", - "manual_check_ip_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", - "manual_check_ip_country_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito", + "manual_check_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "Non siamo riusciti a contattarti al numero di telefono che ci hai fornito. Puoi richiedere tu stesso una richiamata e indicare l'orario che preferisci per la telefonata qui: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "La telefonata non è andata a buon fine o è stata rifiutata.", "merge_incomplete": "L'e-mail di conferma non è stata accettata", "intermediary_without_sender": "La banca mittente (Wise/Revolut) ha trasmesso solo il nome della banca, non il nome del titolare del conto. DFX non può quindi verificare il mittente effettivo e non può elaborare la transazione.", "name_too_short": "Il tuo nome è troppo corto per l'elaborazione bancaria. Le banche richiedono almeno 4 lettere nel nome del titolare del conto." @@ -215,7 +216,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "Abbiamo ricevuto il tuo deposito.", "line2": "Ti contatteremo a breve al numero {phone}.", "line3": "Una volta chiariti tutti i dubbi, la transazione verrà elaborata automaticamente.", - "line4": "", + "line4": "Qui puoi specificare l'orario che preferisci per la telefonata: [url:https://app.dfx.swiss/settings]", "line5": "Se invece si desidera richiedere un rimborso:
[url:clicca qui]" } }, diff --git a/src/shared/i18n/pt/mail.json b/src/shared/i18n/pt/mail.json index 192b00f9a9..170ef4b445 100644 --- a/src/shared/i18n/pt/mail.json +++ b/src/shared/i18n/pt/mail.json @@ -91,9 +91,10 @@ "test_only": "Test", "kyc_data_needed": "Your KYC data is required for the transaction", "bank_tx_needed": "A bank transaction is required before this transaction can be carried out", - "manual_check_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_phone": "We were unable to reach you at the phone number you provided", - "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided", + "manual_check_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_ip_country_phone": "We were unable to reach you at the phone number you provided. You can request a callback and a preferred time for the phone call yourself here: [url:https://app.dfx.swiss/settings]", + "manual_check_phone_rejected": "The phone call was unsuccessful or rejected", "merge_incomplete": "The email confirmation was not accepted", "intermediary_without_sender": "O banco remetente (Wise/Revolut) transmitiu apenas o nome do banco, não o nome do titular da conta. Portanto, a DFX não pode verificar o remetente real e não pode processar a transação.", "name_too_short": "O seu nome é muito curto para o processamento bancário. Os bancos exigem pelo menos 4 letras no nome do titular da conta." @@ -215,7 +216,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "merge_incomplete": { @@ -242,7 +243,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" }, "manual_check_ip_country_phone": { @@ -251,7 +252,7 @@ "line1": "We have received your deposit.", "line2": "We will contact you shortly at {phone}.", "line3": "Once all questions have been clarified, your transaction will be processed automatically.", - "line4": "", + "line4": "You can specify your preferred time for the phone call here: [url:https://app.dfx.swiss/settings]", "line5": "If you would like to request a refund instead:
[url:click here]" } }, diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts index 367bee6b8d..8fc473f82c 100644 --- a/src/subdomains/core/aml/enums/aml-error.enum.ts +++ b/src/subdomains/core/aml/enums/aml-error.enum.ts @@ -62,6 +62,7 @@ export enum AmlError { IP_BLACKLISTED_WITHOUT_KYC = 'IpBlacklistedWithoutKyc', BANK_RELEASE_DATE_MISSING = 'BankReleaseDateMissing', IP_COUNTRY_MISMATCH = 'IpCountryMismatch', + USER_DATA_REJECTED_CALL = 'UserDataRejectedCall', TRADE_APPROVAL_DATE_MISSING = 'TradeApprovalDateMissing', BANK_TX_CUSTOMER_NAME_MISSING = 'BankTxCustomerNameMissing', FORCE_MANUAL_CHECK = 'ForceManualCheck', @@ -301,6 +302,11 @@ export const AmlErrorResult: { amlCheck: CheckStatus.PENDING, amlReason: AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE, }, + [AmlError.USER_DATA_REJECTED_CALL]: { + type: AmlErrorType.CRUCIAL, + amlCheck: CheckStatus.FAIL, + amlReason: AmlReason.MANUAL_CHECK_PHONE_REJECTED, + }, [AmlError.TRADE_APPROVAL_DATE_MISSING]: { type: AmlErrorType.CRUCIAL, amlCheck: CheckStatus.PENDING, diff --git a/src/subdomains/core/aml/enums/aml-reason.enum.ts b/src/subdomains/core/aml/enums/aml-reason.enum.ts index 5f55419276..dafbf87657 100644 --- a/src/subdomains/core/aml/enums/aml-reason.enum.ts +++ b/src/subdomains/core/aml/enums/aml-reason.enum.ts @@ -36,6 +36,7 @@ export enum AmlReason { MANUAL_CHECK_PHONE = 'ManualCheckPhone', MANUAL_CHECK_IP_PHONE = 'ManualCheckIpPhone', MANUAL_CHECK_IP_COUNTRY_PHONE = 'ManualCheckIpCountryPhone', + MANUAL_CHECK_PHONE_REJECTED = 'ManualCheckPhoneRejected', BANK_RELEASE_PENDING = 'BankReleasePending', VIRTUAL_IBAN_USER_MISMATCH = 'VirtualIbanUserMismatch', INTERMEDIARY_WITHOUT_SENDER = 'IntermediaryWithoutSender', diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index 124b9e9a68..abc20923fa 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -6,7 +6,12 @@ import { Util } from 'src/shared/utils/util'; import { ReviewStatus } from 'src/subdomains/generic/kyc/enums/review-status.enum'; import { BankData, BankDataVerificationError } from 'src/subdomains/generic/user/models/bank-data/bank-data.entity'; import { AccountType } from 'src/subdomains/generic/user/models/user-data/account-type.enum'; -import { KycLevel, KycType, UserDataStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; +import { + KycLevel, + KycType, + PhoneCallStatus, + UserDataStatus, +} from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { User } from 'src/subdomains/generic/user/models/user/user.entity'; import { UserStatus } from 'src/subdomains/generic/user/models/user/user.enum'; import { Bank } from 'src/subdomains/supporting/bank/bank/bank.entity'; @@ -56,7 +61,11 @@ export class AmlHelperService { !entity.userData.tradeApprovalDate && !entity.wallet.autoTradeApproval ) - errors.push(AmlError.TRADE_APPROVAL_DATE_MISSING); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.TRADE_APPROVAL_DATE_MISSING, + ); if (entity.inputReferenceAmount < minVolume * 0.9) errors.push(AmlError.MIN_VOLUME_NOT_REACHED); if (entity.user.isBlocked) errors.push(AmlError.USER_BLOCKED); if (entity.user.isDeleted) errors.push(AmlError.USER_DELETED); @@ -88,7 +97,11 @@ export class AmlHelperService { errors.push(AmlError.YEARLY_LIMIT_WO_KYC_REACHED); if (entity.userData.hasIpRisk && !entity.userData.phoneCallIpCheckDate) { if (entity.userData.kycLevel >= KycLevel.LEVEL_50) { - errors.push(AmlError.IP_PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.IP_PHONE_VERIFICATION_NEEDED, + ); } else { errors.push(AmlError.IP_BLACKLISTED_WITHOUT_KYC); } @@ -194,7 +207,11 @@ export class AmlHelperService { ![l, entity.userData.country.symbol].every((c) => Config.allowedBorderRegions.includes(c)), ) ) - errors.push(AmlError.IP_COUNTRY_MISMATCH); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.IP_COUNTRY_MISMATCH, + ); if ( entity.userData.hasSuspiciousMail && @@ -220,7 +237,11 @@ export class AmlHelperService { (!entity.userData.accountType || entity.userData.accountType === AccountType.PERSONAL) && Util.yearsDiff(entity.userData.birthday) > 55 ) - errors.push(AmlError.PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.PHONE_VERIFICATION_NEEDED, + ); if (entity.bankTx) { // bank @@ -442,7 +463,11 @@ export class AmlHelperService { entity.userData.accountType === AccountType.PERSONAL && !entity.userData.phoneCallCheckDate ) - errors.push(AmlError.PHONE_VERIFICATION_NEEDED); + errors.push( + [PhoneCallStatus.REJECTED, PhoneCallStatus.FAILED].includes(entity.userData.phoneCallStatus) + ? AmlError.USER_DATA_REJECTED_CALL + : AmlError.PHONE_VERIFICATION_NEEDED, + ); break; } diff --git a/src/subdomains/generic/user/models/user-data/user-data.entity.ts b/src/subdomains/generic/user/models/user-data/user-data.entity.ts index d5430deb17..663b01bf21 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.entity.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.entity.ts @@ -41,6 +41,7 @@ import { LegalEntity, LimitPeriod, Moderator, + PhoneCallStatus, RiskStatus, SignatoryPower, UserDataStatus, @@ -246,6 +247,12 @@ export class UserData extends IEntity { @Column({ type: 'datetime2', nullable: true }) phoneCallIpCountryCheckDate?: Date; + @Column({ length: 256, nullable: true }) + phoneCallTimes: string; // PhoneCallPreferredTimes array + + @Column({ length: 256, nullable: true }) + phoneCallStatus: PhoneCallStatus; + @Column({ type: 'datetime2', nullable: true }) tradeApprovalDate?: Date; @@ -499,6 +506,8 @@ export class UserData extends IEntity { phone: dto.phone ?? this.phone, language: dto.language ?? this.language, currency: dto.currency ?? this.currency, + phoneCallTimes: dto.preferredTimes ? dto.preferredTimes.join(';') : undefined, + phoneCallStatus: dto.rejectCall ? PhoneCallStatus.REJECTED : dto.repeatCall ? PhoneCallStatus.REPEAT : undefined, }; Object.assign(this, update); diff --git a/src/subdomains/generic/user/models/user-data/user-data.enum.ts b/src/subdomains/generic/user/models/user-data/user-data.enum.ts index 0a5e2f4aaa..8a41c236ca 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.enum.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.enum.ts @@ -15,6 +15,26 @@ export enum RiskStatus { RELEASED = 'Released', } +export enum PhoneCallPreferredTime { + BETWEEN_9_AND_10 = 'Between9And10', + BETWEEN_10_AND_11 = 'Between10And11', + BETWEEN_11_AND_12 = 'Between11And12', + BETWEEN_12_AND_13 = 'Between12And13', + BETWEEN_13_AND_14 = 'Between13And14', + BETWEEN_14_AND_15 = 'Between14And15', + BETWEEN_15_AND_16 = 'Between15And16', + BETWEEN_9_AND_16 = 'Between9And16', +} + +export enum PhoneCallStatus { + REPEAT = 'Repeat', + REJECTED = 'Rejected', + UNAVAILABLE = 'Unavailable', + FAILED = 'Failed', + COMPLETED = 'Completed', + SUSPICIOUS = 'Suspicious', +} + export enum KycLevel { // automatic levels LEVEL_0 = 0, // nothing diff --git a/src/subdomains/generic/user/models/user-data/user-data.service.ts b/src/subdomains/generic/user/models/user-data/user-data.service.ts index 1f06812044..88a4feb8be 100644 --- a/src/subdomains/generic/user/models/user-data/user-data.service.ts +++ b/src/subdomains/generic/user/models/user-data/user-data.service.ts @@ -727,6 +727,9 @@ export class UserDataService { if (!dto.currency) throw new BadRequestException('Currency not found'); } + if (userData.phoneCallStatus && (dto.rejectCall || dto.repeatCall)) + throw new BadRequestException('Phone call status is already set'); + const phoneChanged = dto.phone && dto.phone !== userData.phone; const updateSiftAccount: CreateAccount = { $time: Date.now() }; diff --git a/src/subdomains/generic/user/models/user/dto/update-user.dto.ts b/src/subdomains/generic/user/models/user/dto/update-user.dto.ts index 444db51b6b..bf316fc422 100644 --- a/src/subdomains/generic/user/models/user/dto/update-user.dto.ts +++ b/src/subdomains/generic/user/models/user/dto/update-user.dto.ts @@ -1,11 +1,22 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsBoolean, + IsEmail, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + ValidateIf, + ValidateNested, +} from 'class-validator'; import { EntityDto } from 'src/shared/dto/entity.dto'; import { Fiat } from 'src/shared/models/fiat/fiat.entity'; import { Language } from 'src/shared/models/language/language.entity'; import { Util } from 'src/shared/utils/util'; import { DfxPhoneTransform, IsDfxPhone } from '../../user-data/is-dfx-phone.validator'; +import { PhoneCallPreferredTime } from '../../user-data/user-data.enum'; export class UpdateUserDto { @ApiPropertyOptional() @@ -28,6 +39,23 @@ export class UpdateUserDto { @ValidateNested() @Type(() => EntityDto) currency?: Fiat; + + @ApiPropertyOptional({ type: String, isArray: true }) + @IsOptional() + @IsEnum(PhoneCallPreferredTime) + preferredTimes?: PhoneCallPreferredTime[]; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdateUserDto) => Boolean(a.rejectCall || !a.repeatCall)) + @IsBoolean() + rejectCall?: boolean; + + @ApiPropertyOptional() + @IsNotEmpty() + @ValidateIf((a: UpdateUserDto) => Boolean(a.repeatCall || !a.rejectCall)) + @IsBoolean() + repeatCall?: boolean; } export class UpdateUserMailDto { diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 2f7876c026..d05afbfd82 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -111,6 +111,7 @@ export const TransactionReasonMapper: { [AmlReason.MANUAL_CHECK_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.MANUAL_CHECK_IP_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.MANUAL_CHECK_IP_COUNTRY_PHONE]: TransactionReason.PHONE_VERIFICATION_NEEDED, + [AmlReason.MANUAL_CHECK_PHONE_REJECTED]: TransactionReason.PHONE_VERIFICATION_NEEDED, [AmlReason.BANK_RELEASE_PENDING]: TransactionReason.BANK_RELEASE_PENDING, [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: TransactionReason.UNKNOWN, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: TransactionReason.BANK_NOT_ALLOWED, diff --git a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts index 236ab3890e..e80cc9cf15 100644 --- a/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts +++ b/src/subdomains/supporting/support-issue/enums/support-issue.enum.ts @@ -9,6 +9,7 @@ export enum SupportIssueInternalState { export enum SupportIssueType { GENERIC_ISSUE = 'GenericIssue', TRANSACTION_ISSUE = 'TransactionIssue', + VERIFICATION_CALL = 'VerificationCall', KYC_ISSUE = 'KycIssue', LIMIT_REQUEST = 'LimitRequest', PARTNERSHIP_REQUEST = 'PartnershipRequest', @@ -24,6 +25,11 @@ export enum SupportIssueReason { FUNDS_NOT_RECEIVED = 'FundsNotReceived', TRANSACTION_MISSING = 'TransactionMissing', + // verification call + REJECT_CALL = 'RejectCall', + REPEAT_CALL = 'RepeatCall', + PREFERRED_CALL_TIME = 'PreferredCallTime', + // notification of changes issue NAME_CHANGED = 'NameChanged', ADDRESS_CHANGED = 'AddressChanged', diff --git a/src/subdomains/supporting/support-issue/services/support-issue.service.ts b/src/subdomains/supporting/support-issue/services/support-issue.service.ts index 27c6620dbc..edbfe6991b 100644 --- a/src/subdomains/supporting/support-issue/services/support-issue.service.ts +++ b/src/subdomains/supporting/support-issue/services/support-issue.service.ts @@ -8,11 +8,11 @@ import { import { Config } from 'src/config/config'; import { BlobContent } from 'src/integration/infrastructure/azure-storage.service'; import { UserRole } from 'src/shared/auth/user-role.enum'; -import { FiatService } from 'src/shared/models/fiat/fiat.service'; import { Util } from 'src/shared/utils/util'; import { ContentType } from 'src/subdomains/generic/kyc/enums/content-type.enum'; import { BankDataService } from 'src/subdomains/generic/user/models/bank-data/bank-data.service'; import { UserData } from 'src/subdomains/generic/user/models/user-data/user-data.entity'; +import { PhoneCallStatus } from 'src/subdomains/generic/user/models/user-data/user-data.enum'; import { UserDataService } from 'src/subdomains/generic/user/models/user-data/user-data.service'; import { FindOptionsWhere, In, IsNull, MoreThan, Not } from 'typeorm'; import { TransactionRequestType } from '../../payment/entities/transaction-request.entity'; @@ -27,8 +27,7 @@ import { SupportIssueDto, SupportIssueInternalDataDto, SupportMessageDto } from import { UpdateSupportIssueDto } from '../dto/update-support-issue.dto'; import { SupportIssue } from '../entities/support-issue.entity'; import { AutoResponder, CustomerAuthor, SupportMessage } from '../entities/support-message.entity'; -import { Department } from '../enums/department.enum'; -import { SupportIssueInternalState } from '../enums/support-issue.enum'; +import { SupportIssueInternalState, SupportIssueReason, SupportIssueType } from '../enums/support-issue.enum'; import { SupportLogType } from '../enums/support-log.enum'; import { SupportIssueRepository } from '../repositories/support-issue.repository'; import { SupportMessageRepository } from '../repositories/support-message.repository'; @@ -50,7 +49,6 @@ export class SupportIssueService { private readonly transactionRequestService: TransactionRequestService, private readonly supportLogService: SupportLogService, private readonly bankDataService: BankDataService, - private readonly fiatService: FiatService, ) {} async createTransactionRequestIssue(dto: CreateSupportIssueBaseDto): Promise { @@ -145,9 +143,22 @@ export class SupportIssueService { } // create limit request - if (dto.limitRequest) { - newIssue.department = Department.COMPLIANCE; + if (dto.limitRequest) newIssue.limitRequest = await this.limitRequestService.increaseLimitInternal(dto.limitRequest, userData); + + if ( + !userData.phoneCallStatus && + dto.type === SupportIssueType.VERIFICATION_CALL && + [SupportIssueReason.REJECT_CALL, SupportIssueReason.REPEAT_CALL].includes(dto.reason) + ) { + await this.userDataService.updateUserDataInternal(userData, { + phoneCallStatus: + dto.reason === SupportIssueReason.REJECT_CALL + ? PhoneCallStatus.REJECTED + : dto.reason === SupportIssueReason.REPEAT_CALL + ? PhoneCallStatus.REPEAT + : undefined, + }); } } diff --git a/src/subdomains/supporting/support-issue/support-issue.controller.ts b/src/subdomains/supporting/support-issue/support-issue.controller.ts index 16edfa1c00..387c0c6f2e 100644 --- a/src/subdomains/supporting/support-issue/support-issue.controller.ts +++ b/src/subdomains/supporting/support-issue/support-issue.controller.ts @@ -16,6 +16,7 @@ import { UpdateSupportIssueDto } from './dto/update-support-issue.dto'; import { SupportIssue } from './entities/support-issue.entity'; import { CustomerAuthor } from './entities/support-message.entity'; import { Department } from './enums/department.enum'; +import { SupportIssueType } from './enums/support-issue.enum'; import { SupportIssueService } from './services/support-issue.service'; @ApiTags('Support') @@ -30,7 +31,14 @@ export class SupportIssueController { @GetJwt() jwt: JwtPayload | undefined, @Body() dto: CreateSupportIssueDto, ): Promise { - const input: CreateSupportIssueDto = { ...dto, author: CustomerAuthor, department: Department.SUPPORT }; + const input: CreateSupportIssueDto = { + ...dto, + author: CustomerAuthor, + department: + dto.type === SupportIssueType.VERIFICATION_CALL || dto.limitRequest + ? Department.COMPLIANCE + : Department.SUPPORT, + }; return jwt?.account ? this.supportIssueService.createIssue(jwt.account, input) : this.supportIssueService.createTransactionRequestIssue(input);