From 0d5efe0bf8a97ca43b93718c7d5cc4445aa4dfcc Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Fri, 23 Aug 2024 16:42:34 -0400 Subject: [PATCH 1/8] feat: add meta endpoint --- app/main.py | 1 + app/models.py | 10 ++-- app/routers/guest/__init__.py | 2 +- app/routers/guest/meta.py | 37 +++++++++++++++ docs/frontend.rst | 10 +--- locale/es/LC_MESSAGES/messages.po | 79 +++++++++++++++---------------- 6 files changed, 84 insertions(+), 55 deletions(-) create mode 100644 app/routers/guest/meta.py diff --git a/app/main.py b/app/main.py index e16edb33..f6540b8b 100644 --- a/app/main.py +++ b/app/main.py @@ -21,3 +21,4 @@ app.include_router(downloads.router) app.include_router(lenders.router) app.include_router(statistics.router) +app.include_router(guest.meta.router) diff --git a/app/models.py b/app/models.py index f740c6c1..5a8e541b 100644 --- a/app/models.py +++ b/app/models.py @@ -316,13 +316,13 @@ class BorrowerSector(StrEnum): class CreditType(StrEnum): - LOAN = "LOAN" - CREDIT_LINE = "CREDIT_LINE" + LOAN = i("LOAN") + CREDIT_LINE = i("CREDIT_LINE") class BorrowerType(StrEnum): - NATURAL_PERSON = "NATURAL_PERSON" - LEGAL_PERSON = "LEGAL_PERSON" + NATURAL_PERSON = i("NATURAL_PERSON") + LEGAL_PERSON = i("LEGAL_PERSON") class StatisticType(StrEnum): @@ -346,7 +346,7 @@ class LenderBase(SQLModel): #: .. seealso:: :attr:`~app.settings.Settings.progress_to_remind_started_applications` sla_days: int | None #: Additional HTML content to include in a :attr:`app.models.MessageType.APPROVED_APPLICATION` message, if the - #: "additional_comments" key in the application's :attr:`app.models.APplication.lender_approved_data` isn't set. + #: "additional_comments" key in the application's :attr:`app.models.Application.lender_approved_data` isn't set. default_pre_approval_message: str = Field(default="") diff --git a/app/routers/guest/__init__.py b/app/routers/guest/__init__.py index b1ad0ab3..c72d2451 100644 --- a/app/routers/guest/__init__.py +++ b/app/routers/guest/__init__.py @@ -1 +1 @@ -from . import applications, emails # noqa: F401 +from . import applications, emails, meta # noqa: F401 diff --git a/app/routers/guest/meta.py b/app/routers/guest/meta.py new file mode 100644 index 00000000..97969ad8 --- /dev/null +++ b/app/routers/guest/meta.py @@ -0,0 +1,37 @@ +from typing import Any + +from fastapi import APIRouter, HTTPException, status + +from app import models +from app.i18n import _ + +router = APIRouter() + + +@router.get( + "/meta/{domain}", + tags=["meta"], +) +async def get_settings_by_domain(domain: str) -> list[dict[str, str | Any]]: + """ + Get the keys and localized descriptions of a specific domain, where a domain can be: + - BorrowerType + - CreditType + - BorrowerDocumentType + - BorrowerSize + - BorrowerSector, + + :return: A dict of the domain key and localized value. + """ + if domain not in [ + "BorrowerType", + "CreditType", + "BorrowerDocumentType", + "BorrowerSize", + "BorrowerSector", + ]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=_("Domain doesn't exist"), + ) + return [{"label": _(name), "value": name} for name in getattr(models, domain)] diff --git a/docs/frontend.rst b/docs/frontend.rst index 3a3d2ed5..c176447b 100644 --- a/docs/frontend.rst +++ b/docs/frontend.rst @@ -17,14 +17,8 @@ Credere frontend's ``src/constants/index.ts`` constants should match ``app.model - Frontend * - ApplicationStatus - APPLICATION_STATUS - * - BorrowerType - - BORROWER_TYPE - * - CreditType - - CREDIT_PRODUCT_TYPE - * - BorrowerDocumentType - - DOCUMENTS_TYPE - * - BorrowerSize - - MSME_TYPES + * - BorrowerSize.NOT_INFORMED + - DEFAULT_BORROWER_SIZE * - StatisticCustomRange - STATISTICS_DATE_FILTER * - UserType diff --git a/locale/es/LC_MESSAGES/messages.po b/locale/es/LC_MESSAGES/messages.po index fa6bd145..7e8d7ac3 100644 --- a/locale/es/LC_MESSAGES/messages.po +++ b/locale/es/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-08-23 11:37-0400\n" +"POT-Creation-Date: 2024-08-23 16:41-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es\n" @@ -43,45 +43,28 @@ msgstr "Falta el nombre de usuario" msgid "User not found" msgstr "Usuario no encontrado" -#: app/dependencies.py:70 tests/routers/test_lenders.py:48 -#: tests/routers/test_lenders.py:102 tests/routers/test_lenders.py:141 -#: tests/routers/test_lenders.py:174 tests/routers/test_users.py:35 -#: tests/routers/test_users.py:58 +#: app/dependencies.py:70 msgid "Insufficient permissions" msgstr "Permisos insuficientes" -#: app/dependencies.py:98 tests/routers/test_applications.py:104 -#: tests/routers/test_applications.py:109 -#: tests/routers/test_applications.py:162 -#: tests/routers/test_applications.py:167 -#: tests/routers/test_applications.py:172 -#: tests/routers/test_applications.py:182 -#: tests/routers/test_applications.py:187 +#: app/dependencies.py:98 msgid "User is not authorized" msgstr "Usuario no autorizado" -#: app/dependencies.py:106 tests/routers/guest/test_applications.py:69 +#: app/dependencies.py:106 msgid "Application expired" msgstr "La aplicación ha expirado" -#: app/dependencies.py:113 tests/routers/guest/test_applications.py:22 -#: tests/routers/guest/test_applications.py:38 -#: tests/routers/test_applications.py:24 tests/routers/test_applications.py:67 -#: tests/routers/test_applications.py:157 +#: app/dependencies.py:113 #, python-format msgid "Application status should not be %(status)s" msgstr "El estado de la aplicación no debe ser %(status)s" #: app/dependencies.py:126 app/dependencies.py:166 -#: tests/routers/test_applications.py:192 -#: tests/routers/test_applications.py:197 -#: tests/routers/test_applications.py:333 -#: tests/routers/test_applications.py:354 -#: tests/routers/test_applications.py:358 msgid "Application not found" msgstr "La aplicación no ha sido encontrada" -#: app/dependencies.py:172 tests/routers/test_applications.py:350 +#: app/dependencies.py:172 msgid "Application lapsed" msgstr "La aplicación ha caducado" @@ -193,21 +176,19 @@ msgstr "Cámara de comercio" msgid "THREE_LAST_BANK_STATEMENT" msgstr "Últimos tres extractos bancarios" -#: app/models.py:163 tests/routers/guest/test_applications.py:38 -#: tests/routers/test_applications.py:24 tests/test_i18n.py:11 -#: tests/test_i18n.py:16 +#: app/models.py:163 msgid "PENDING" msgstr "Pendiente" -#: app/models.py:167 tests/routers/guest/test_applications.py:22 +#: app/models.py:167 msgid "DECLINED" msgstr "Declinado" -#: app/models.py:171 tests/routers/test_applications.py:67 +#: app/models.py:171 msgid "ACCEPTED" msgstr "Aceptado" -#: app/models.py:175 tests/routers/test_applications.py:157 +#: app/models.py:175 msgid "SUBMITTED" msgstr "Enviado" @@ -346,14 +327,28 @@ msgstr "" msgid "actividades_organizaciones_extraterritoriales" msgstr "Actividades de organizaciones y entidades extraterritoriales" -#: app/util.py:54 tests/routers/test_applications.py:245 -#: tests/routers/test_lenders.py:62 tests/routers/test_lenders.py:77 -#: tests/routers/test_lenders.py:156 tests/routers/test_users.py:27 +#: app/models.py:319 +msgid "LOAN" +msgstr "Préstamo" + +#: app/models.py:320 +msgid "CREDIT_LINE" +msgstr "Línea de crédito" + +#: app/models.py:324 +msgid "NATURAL_PERSON" +msgstr "Persona natural" + +#: app/models.py:325 +msgid "LEGAL_PERSON" +msgstr "Persona jurídica" + +#: app/util.py:54 #, python-format msgid "%(model_name)s not found" msgstr "%(model_name)s no encontrado" -#: app/util.py:109 tests/routers/test_applications.py:221 +#: app/util.py:109 msgid "Format not allowed. It must be a PNG, JPEG, or PDF file" msgstr "Formato no permitido. El formato debe ser un PNG, JPEG or archivo PDF" @@ -361,11 +356,11 @@ msgstr "Formato no permitido. El formato debe ser un PNG, JPEG or archivo PDF" msgid "File is too large" msgstr "El archivo es muy grande" -#: app/routers/applications.py:175 tests/routers/test_applications.py:250 +#: app/routers/applications.py:175 msgid "Some borrower data field are not verified" msgstr "Algunos campos de datos de la empresa no fueron verificados" -#: app/routers/applications.py:186 tests/routers/test_applications.py:268 +#: app/routers/applications.py:186 msgid "Some documents are not verified" msgstr "Algunos documentos no fueron verificados" @@ -414,7 +409,6 @@ msgid "Stage" msgstr "Etapa" #: app/routers/lenders.py:49 app/routers/lenders.py:126 -#: tests/routers/test_lenders.py:98 msgid "Lender already exists" msgstr "La entidad financiera ya existe" @@ -423,7 +417,7 @@ msgstr "La entidad financiera ya existe" msgid "Credit product not found" msgstr "Producto crediticio no encontrado" -#: app/routers/users.py:61 tests/routers/test_users.py:68 +#: app/routers/users.py:61 msgid "Username already exists" msgstr "El nombre de usuario ya existe" @@ -455,8 +449,7 @@ msgstr "Ocurrió un error tratando de configurar el MFA" msgid "MFA configured successfully" msgstr "El MFA fue configurado correctamente" -#: app/routers/users.py:237 tests/routers/test_users.py:77 -#: tests/routers/test_users.py:87 +#: app/routers/users.py:237 msgid "User logged out successfully" msgstr "Usuario deslogueado correctamente" @@ -498,11 +491,11 @@ msgstr "Una nueva aplicación ya ha sido creada desde esta" msgid "There was a problem copying the application. %(exception)s" msgstr "Ocurrió un problema copiando la aplicación. %(exception)s" -#: app/routers/guest/emails.py:34 tests/routers/test_applications.py:117 +#: app/routers/guest/emails.py:34 msgid "New email is not valid" msgstr "El nuevo correo no es válido" -#: app/routers/guest/emails.py:81 tests/routers/test_applications.py:143 +#: app/routers/guest/emails.py:81 msgid "Application is not pending an email confirmation" msgstr "La aplicación no tiene la confirmación de correo pendiente" @@ -510,6 +503,10 @@ msgstr "La aplicación no tiene la confirmación de correo pendiente" msgid "Not authorized to modify this application" msgstr "No autorizado a modificar la aplicación en esta etapa" +#: app/routers/guest/meta.py:35 +msgid "Domain doesn't exist" +msgstr "El dominio no existe" + #: app/utils/tables.py:43 msgid "Financing Options" msgstr "Opciones crediticias" From c82cda83d160989fe3838df752aee1af16414970 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Fri, 23 Aug 2024 17:11:30 -0400 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: James McKinney <26463+jpmckinney@users.noreply.github.com> --- app/routers/guest/meta.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/routers/guest/meta.py b/app/routers/guest/meta.py index 97969ad8..1540a0a3 100644 --- a/app/routers/guest/meta.py +++ b/app/routers/guest/meta.py @@ -1,5 +1,3 @@ -from typing import Any - from fastapi import APIRouter, HTTPException, status from app import models @@ -12,26 +10,27 @@ "/meta/{domain}", tags=["meta"], ) -async def get_settings_by_domain(domain: str) -> list[dict[str, str | Any]]: +async def get_settings_by_domain(domain: str) -> list[dict[str, str]]: """ Get the keys and localized descriptions of a specific domain, where a domain can be: - - BorrowerType - - CreditType + - BorrowerDocumentType + - BorrowerSector - BorrowerSize - - BorrowerSector, + - BorrowerType + - CreditType :return: A dict of the domain key and localized value. """ - if domain not in [ - "BorrowerType", - "CreditType", + if domain not in ( "BorrowerDocumentType", - "BorrowerSize", "BorrowerSector", - ]: + "BorrowerSize", + "BorrowerType", + "CreditType", + ): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_404_NOT_FOUND, detail=_("Domain doesn't exist"), ) return [{"label": _(name), "value": name} for name in getattr(models, domain)] From 052b013a4ec4fc4ccc6146342bafbdeeb64d1356 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Fri, 23 Aug 2024 17:12:15 -0400 Subject: [PATCH 3/8] Apply suggestions from code review --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 09780aed..4d793b5a 100644 --- a/app/main.py +++ b/app/main.py @@ -20,10 +20,10 @@ app.include_router(applications.router) app.include_router(guest.applications.router) app.include_router(guest.emails.router) +app.include_router(guest.meta.router) app.include_router(downloads.router) app.include_router(lenders.router) app.include_router(statistics.router) -app.include_router(guest.meta.router) @app.exception_handler(500) From b074079268cfed4f63de25d8633ed555703b6ac0 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Fri, 23 Aug 2024 19:44:51 -0400 Subject: [PATCH 4/8] fix: remove domain parameter from meta endpoint --- app/routers/guest/meta.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/routers/guest/meta.py b/app/routers/guest/meta.py index 1540a0a3..723e7add 100644 --- a/app/routers/guest/meta.py +++ b/app/routers/guest/meta.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, status +from fastapi import APIRouter from app import models from app.i18n import _ @@ -7,12 +7,12 @@ @router.get( - "/meta/{domain}", + "/meta", tags=["meta"], ) -async def get_settings_by_domain(domain: str) -> list[dict[str, str]]: +async def get_settings_by_domain() -> dict[str, list[dict[str, str]]]: """ - Get the keys and localized descriptions of a specific domain, where a domain can be: + Get the keys and localized descriptions of constants, where a constant can be: - BorrowerDocumentType - BorrowerSector @@ -20,17 +20,15 @@ async def get_settings_by_domain(domain: str) -> list[dict[str, str]]: - BorrowerType - CreditType - :return: A dict of the domain key and localized value. + :return: A dict of constants with their keys and localized values. """ - if domain not in ( + constants = {} + for domain in ( "BorrowerDocumentType", "BorrowerSector", "BorrowerSize", "BorrowerType", "CreditType", ): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=_("Domain doesn't exist"), - ) - return [{"label": _(name), "value": name} for name in getattr(models, domain)] + constants[domain] = [{"label": _(name), "value": name} for name in getattr(models, domain)] + return constants From f6e14c4d043d73c4bd1e6a46974a2aebb90ebac3 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Fri, 23 Aug 2024 19:48:21 -0400 Subject: [PATCH 5/8] update locale --- locale/es/LC_MESSAGES/messages.po | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/locale/es/LC_MESSAGES/messages.po b/locale/es/LC_MESSAGES/messages.po index f820d8f5..bb5cb36a 100644 --- a/locale/es/LC_MESSAGES/messages.po +++ b/locale/es/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-08-23 16:53-0400\n" +"POT-Creation-Date: 2024-08-23 19:47-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es\n" @@ -498,10 +498,6 @@ msgstr "La aplicación no tiene la confirmación de correo pendiente" msgid "Not authorized to modify this application" msgstr "No autorizado a modificar la aplicación en esta etapa" -#: app/routers/guest/meta.py:35 -msgid "Domain doesn't exist" -msgstr "El dominio no existe" - #: app/utils/tables.py:43 msgid "Financing Options" msgstr "Opciones crediticias" @@ -635,4 +631,3 @@ msgstr "Correo institucional" #: app/utils/tables.py:249 msgid "MSME Documents" msgstr "Documentos de la empresa" - From d3e54d55b18a59c4cb74c8550dc36e4011d35a88 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 11:48:56 -0400 Subject: [PATCH 6/8] fix: Remove CreditType as it has special logic in the front end --- app/routers/guest/meta.py | 2 -- docs/frontend.rst | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/routers/guest/meta.py b/app/routers/guest/meta.py index 723e7add..c4c174a3 100644 --- a/app/routers/guest/meta.py +++ b/app/routers/guest/meta.py @@ -18,7 +18,6 @@ async def get_settings_by_domain() -> dict[str, list[dict[str, str]]]: - BorrowerSector - BorrowerSize - BorrowerType - - CreditType :return: A dict of constants with their keys and localized values. """ @@ -28,7 +27,6 @@ async def get_settings_by_domain() -> dict[str, list[dict[str, str]]]: "BorrowerSector", "BorrowerSize", "BorrowerType", - "CreditType", ): constants[domain] = [{"label": _(name), "value": name} for name in getattr(models, domain)] return constants diff --git a/docs/frontend.rst b/docs/frontend.rst index c176447b..6deb636a 100644 --- a/docs/frontend.rst +++ b/docs/frontend.rst @@ -19,6 +19,10 @@ Credere frontend's ``src/constants/index.ts`` constants should match ``app.model - APPLICATION_STATUS * - BorrowerSize.NOT_INFORMED - DEFAULT_BORROWER_SIZE + * - BorrowerDocumentType.SIGNED_CONTRACT + - SIGNED_CONTRACT_DOCUMENT_TYPE + * - CreditType + - CREDIT_PRODUCT_TYPE * - StatisticCustomRange - STATISTICS_DATE_FILTER * - UserType From 31f4df9d5f42cd392664ae3b99f7971fc1071cf0 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 11:56:29 -0400 Subject: [PATCH 7/8] fix: update document types descriptions --- locale/es/LC_MESSAGES/messages.po | 32 +++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/locale/es/LC_MESSAGES/messages.po b/locale/es/LC_MESSAGES/messages.po index bb5cb36a..175d7955 100644 --- a/locale/es/LC_MESSAGES/messages.po +++ b/locale/es/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-08-23 19:47-0400\n" +"POT-Creation-Date: 2024-08-24 11:56-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es\n" @@ -154,11 +154,11 @@ msgstr "Ocurrió un error inesperado" #: app/models.py:130 msgid "INCORPORATION_DOCUMENT" -msgstr "Documento de incorporación" +msgstr "Certificado de incorporación de tu empresa" #: app/models.py:131 msgid "SUPPLIER_REGISTRATION_DOCUMENT" -msgstr "Documento de registro de proveedor" +msgstr "Registro Único de Proponentes (RUP) de su empresa" #: app/models.py:132 msgid "BANK_NAME" @@ -166,11 +166,19 @@ msgstr "Nombre del banco" #: app/models.py:133 msgid "BANK_CERTIFICATION_DOCUMENT" -msgstr "Certificado bancario" +msgstr "" +"Certificado Bancario de la empresa: Documento debe estar firmado y con " +"emisión menor a 3 meses." #: app/models.py:134 msgid "FINANCIAL_STATEMENT" -msgstr "Estados financieros" +msgstr "" +"Documento de Estados Financieros de su empresa: A corte del último " +"trimestre y del corte anual y el anterior (ejemplo; si estamos en " +"Septiembre, la información disponible estaría a Junio 2023 y a Diciembre " +"2022 y Diciembre 2021). Si no se tiene disponible la información a corte " +"parcial del trimestre, se recibe pero puede llegar a ser solicitada más " +"adelante por la institución financiera." #: app/models.py:135 msgid "SIGNED_CONTRACT" @@ -178,15 +186,22 @@ msgstr "Contrato firmado" #: app/models.py:136 msgid "SHAREHOLDER_COMPOSITION" -msgstr "Composición accionaria" +msgstr "" +"Documento de Composición Accionaria: Debe enviarse completo, hasta llegar" +" al beneficiario final con concentración mayor al 5%." #: app/models.py:137 msgid "CHAMBER_OF_COMMERCE" -msgstr "Cámara de comercio" +msgstr "" +"Documento de Registro de la Cámara de Comercio: Documento debe estar " +"firmado y con emisión menor a 3 meses." #: app/models.py:138 msgid "THREE_LAST_BANK_STATEMENT" -msgstr "Últimos tres extractos bancarios" +msgstr "" +"Extractos Bancarios de los últimos 3 meses: De la o las cuentas " +"principales de la empresa. El documento deber ser emitido por el banco y " +"deber ser enviado SIN alterar." #: app/models.py:163 msgid "PENDING" @@ -631,3 +646,4 @@ msgstr "Correo institucional" #: app/utils/tables.py:249 msgid "MSME Documents" msgstr "Documentos de la empresa" + From b07cc7f3724a61a8b37448836964866da7b59978 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 12:58:21 -0400 Subject: [PATCH 8/8] fix: remove % from translation string --- locale/es/LC_MESSAGES/messages.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locale/es/LC_MESSAGES/messages.po b/locale/es/LC_MESSAGES/messages.po index 175d7955..8f196f5b 100644 --- a/locale/es/LC_MESSAGES/messages.po +++ b/locale/es/LC_MESSAGES/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-08-24 11:56-0400\n" +"POT-Creation-Date: 2024-08-24 12:58-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es\n" @@ -188,7 +188,7 @@ msgstr "Contrato firmado" msgid "SHAREHOLDER_COMPOSITION" msgstr "" "Documento de Composición Accionaria: Debe enviarse completo, hasta llegar" -" al beneficiario final con concentración mayor al 5%." +" al beneficiario final con concentración mayor al 5 por ciento." #: app/models.py:137 msgid "CHAMBER_OF_COMMERCE"