Skip to content

Credit Line Requests

Structured credit authorization flow that replaces the free-form credit ticket with a formal, multi-stage process backed by a DocuSign contract signature.

Overview

Before this module, credit requests were handled entirely through free-form ticket messages. This module introduces a structured lifecycle for credit line requests with distinct commercial and legal review stages, document collection and per-document approval/rejection, automated DocuSign contract dispatch and webhook-based status synchronization, and a dedicated admin interface under the Contabilidad menu group.

Promissory Note (Pagaré) Step — Mexico

Some countries legally require a signed promissory note (pagaré) before the credit line can be activated. This step is currently enabled only for Mexico. It inserts an extra legal sub-flow between document approval and contract creation: legal uploads a pagaré format for the client to print and sign, the client uploads the signed copy, and legal validates it before the DocuSign contract can be created.

The pagaré requirement is data-driven — a country goes through this sub-flow only when an active generic_forms row with form = 'credit_line_request_pagare' exists for its country_code. The implementation is therefore multi-country ready, but at the moment only Mexico has a pagaré form configured (and the client-facing pagaré instructions reference Envia's Monterrey legal office).

Key Concepts

ConceptDescription
Credit Line RequestA formal application for a revolving credit line, linked 1:1 to a support ticket of type credit
Request StatusLifecycle stage. Base path: pending_commercialapproved_commercialcontract_sentlegal_review/active/rejected. Pagaré countries insert pagare_sentpagare_reviewpagare_validated between document approval and contract creation. A rejected document moves the request to document_rejected
Contract StatusDocuSign envelope state: no_contractsentcreatedsigned (or manual for an externally uploaded PDF)
Pagaré (Promissory Note)Mexico-only signed legal document required before contract creation. Tracked as two documents on the request: pagare_format (uploaded by legal) and pagare_signed (uploaded by the client, validated by legal)
Pagaré FormAn active generic_forms row with form = 'credit_line_request_pagare' for a country. Its mere existence flags the country as requiring the pagaré step (shared by KYB and KYC, no person-type suffix)
DocuSign EnvelopeAn electronic signing package sent to the client via DocuSign, tracked by docusign_envelope_id
DocuSign Credit ContractAdmin-registered mapping of a DocuSign Template ID to a locale + person type combination
Document ConfigPer-country, per-person-type list of required supporting documents stored as JSON in generic_forms (form values credit_line_request_kyb / credit_line_request_kyc)
Document CatalogGlobal registry of available document variables (catalog_credit_line_variables); referenced from the form JSON via catalogId
SLADays elapsed since request creation; color-coded as On Time / Delayed / Overdue in the admin table

Cross-Repository Scope

This feature spans three repositories. The admon repo is the primary owner of the admin backend and frontend. The other two repos implement the client-facing side and the shared API layer.

queries — Client API (Node.js)

Handles credit line requests from the client portal perspective:

  • Eligibility check (GET /credit-line/eligibility): evaluates three criteria — account antiquity ≥ 90 days, ≥ 300 delivered guides in the last 30 days, and used balance ≥ MXN 100,000 equivalent. Returns eligible, criteria breakdown, and any blocking status.
  • Required documents (GET /credit-line/documents): returns the document checklist for the company's country and person type (kyb/kyc) by reading the json_structure of the matching generic_forms row (form = 'credit_line_request_kyb' | 'credit_line_request_kyc'). Each document includes field_id (string identifier from the JSON), key, type, label (i18n key), required, and sort_order.
  • Create request (via company ticket flow): when a client submits a credit ticket (type_id = 19), a credit_line_requests record is created automatically with initial documents in credit_line_request_variable_values. The submitted documents reference the form via field_id (VARCHAR) — no foreign key to a config table is needed.
  • Request by ticket (GET /credit-line/request/:ticket_id): returns request status, requested amount/days, rejection reason, and document statuses for the client to track.
  • Document resubmission (PATCH /credit-line/documents/:document_id): allows clients to resubmit a previously rejected document (resets status to pending).
  • Pagaré (Mexico): once legal sends the pagaré format, the client downloads pagare_format, prints/signs it, and uploads the signed copy as pagare_signed. This moves the request to pagare_review for legal validation. (Client-side upload is implemented in the queries/envia-clients repos; admon owns the format upload and validation.)

envia-clients — Client Frontend (Vue 3)

Added credit line request support to the client portal. Key additions include a view of the request status with document statuses, ability to resubmit rejected documents, and integration with the ticket flow so that when a credit ticket is opened the credit line request is surfaced inline.

Data Flow

Request Status Lifecycle

The pagare_sent ← → pagare_review ← → pagare_validated segment only exists for countries with a configured pagaré form (currently Mexico). For all other countries the request goes straight from approved_commercial to contract_sent.

Automated Expiration (Auto-Rejection)

An external scheduler triggers the auto-reject flow by calling POST /credit-line-requests/auto-reject (requires the credit-line-requests-manage permission). The endpoint enqueues a job to the credit_line_requests_auto_reject Bull queue and returns { success: true } immediately. A worker process (worker.js) picks up the job and calls autoRejectExpiredPendingCommercialRequests in credit-line-requests.util.js, which auto-rejects any pending_commercial request whose created_at is older than 30 calendar days from UTC_TIMESTAMP().

External scheduler
  └─► POST /credit-line-requests/auto-reject
        └─► credit_line_requests_auto_reject (Bull queue)
              └─► worker.js → auto-reject-credit-line-requests.jobs.js
                    └─► creditLineRequestsUtil.autoRejectExpiredPendingCommercialRequests()
AttributeValue
Target statuspending_commercial only (v1 scope)
Threshold30 calendar days since created_at (CREDIT_LINE_PENDING_COMMERCIAL_AUTO_REJECT_DAYS)
Final statusrejected
resolved_bySYSTEM_USER_ID = 0 — distinguishes auto-reject from manual reject
resolved_atSet at rejection time by updateRequestStatus
TicketSet to DECLINED; localized rejection message inserted as a visible system comment
i18n keycreditLineRequests.autoReject.expired (falls back to English when no translation found)
Queuecredit_line_requests_auto_reject (Bull / Redis). Concurrency: 1. Retries: 3 with 5 s fixed backoff

The auto-reject uses the existing updateRequestStatus method to reuse all side effects (ticket decline, comment insertion). Each request is wrapped in an individual try/catch so a single failure does not abort the batch. The language_id for each candidate is resolved in the initial SQL query (JOIN companies + locales) and cached per language within the job run — no extra DB round-trip per request.

v1 note: The original ticket mentioned "área Legal" as the target, but the business confirmed the scope is exclusively pending_commercial for this version. The intent is documented in the constants file and this section.

Flow: Pagaré Sub-Flow (Mexico)

Flow: Admin Creates DocuSign Contract

Flow: DocuSign Webhook → Contract Signed

The webhook handler returns 200 OK immediately after signature verification and enqueues the processing to a Bull queue. The actual business logic runs asynchronously in a worker process, preventing DocuSign from timing out on slow operations (PDF download, S3 upload).

Queue: credit_line_requests_docusign_webhook (Bull / Redis). Concurrency: 1. Job progress and logs are visible in Bull Arena (/arena).

Flow: Client Submits Credit Request (queries API)

Backend Structure

Files (admon)

backend/
├── routes/
│   ├── credit-line-requests.routes.js                    # All request management endpoints + DocuSign webhook + auto-reject trigger
│   └── docusign-credit-contracts.routes.js               # DocuSign template registry CRUD
├── controllers/
│   ├── credit-line-requests.controller.js                # Thin delegation layer; enqueues DocuSign webhook job
│   ├── crons.controller.js                               # Auto-reject trigger: enqueues job to credit_line_requests_auto_reject
│   └── docusign-credit-contracts.controller.js
├── jobs/
│   ├── auto-reject-credit-line-requests.jobs.js          # Bull processor: calls autoRejectExpiredPendingCommercialRequests
│   └── docusign-webhook-credit-line-requests.jobs.js     # Bull processor: calls processDocuSignWebhook
├── libraries/
│   ├── credit-line-requests.util.js         # All business logic (singleton); pagaré: loadCreditLinePagareFields, uploadPagareFormat, validatePagare, _countryRequiresPagare, _markRequestDocumentRejected
│   └── docusign-credit-contracts.util.js    # DocuSign contract config CRUD
├── services/
│   └── docusign/
│       └── index.js                         # DocuSign service: JWT auth, envelope creation, PDF download
├── constants/
│   └── credit-line-requests.constants.js    # Status enums, i18n keys, fallback strings
└── worker.js                                # Registers Bull processors for credit_line_requests_auto_reject and credit_line_requests_docusign_webhook queues

Files (queries)

controllers/credit-line.controller.js        # Eligibility, documents, request by ticket, resubmit
routes/credit-line.routes.js                 # Route definitions
services/credit-line.service.js              # Business logic (eligibility criteria, document validation, createRequest)

Key Business Rules

RuleImplementation
Only one active DocuSign contract per locale + person_typeassertAtMostOneActivePerCountryAndPersonType in DocusignCreditContractsUtil
Contract can only be created when request is in approved_commercial and all required documents are approved_validateContractPreconditions checks status and document states
Eligibility criteria: antiquity ≥ 90 days, ≥ 300 delivered guides, balance ≥ MXN 100kcreditLineService.checkEligibility in queries
Approved documents cannot be resubmittedresubmitDocument rejects if status === 'approved'
HMAC signature required for all DocuSign webhook callsverifyDocuSignHmacSignature in auths.util.js; throws 401 if missing/invalid
DocuSign webhook route has no JWT authauth: false — secured by HMAC only
Pagaré required only for configured countries (Mexico)_countryRequiresPagare returns true only when an active credit_line_request_pagare form exists for the country
Pagaré format can only be sent in approved_commercial with all required docs approveduploadPagareFormat validates status, country, and pending documents before storing the format and moving to pagare_sent
Pagaré can only be validated in pagare_reviewvalidatePagare rejects any other status; approve → pagare_validated, reject → deletes signed file/row and returns to pagare_sent
For pagaré countries, the signed pagaré must be approved before contract creationcreateContract asserts pagare_signed.status === 'approved' when the country requires a pagaré; allowed starting statuses are approved_commercial or pagare_validated
Rejecting a KYC/KYB document during legal review moves the request to document_rejected_markRequestDocumentRejected (only while in approved_commercial); re-approving all required docs restores approved_commercial via _recoverFromDocumentRejectedIfResolved
Pagaré documents are excluded from the generic document list and bulk actionspagare_format/pagare_signed are filtered out in the frontend and the format is stored as approved so it never appears as pending
Document review uses a dedicated permissionPATCH .../documents/{documentId}, PATCH .../documents/bulk and PATCH .../pagare/validate require credit-line-requests-review-documents (split from credit-line-requests-manage)

Database

Tables

TablePurpose
credit_line_requestsOne record per credit ticket; tracks lifecycle status, DocuSign state, amounts
credit_line_request_variable_valuesSubmitted supporting files and data fields per request; each row carries a field_id (VARCHAR) that points to a field defined in the generic_forms JSON
catalog_credit_line_variablesGlobal catalog of document variables (key, type: file/input/date, description). Acts as the source of field_id values and is referenced via catalogId in the form JSON.
generic_formsPer-country form configurations stored as JSON. The credit line module uses three form values: credit_line_request_kyb, credit_line_request_kyc, and credit_line_request_pagare (Mexico). The json_structure holds the ordered list of fields with their i18n labels, types and validation rules. The presence of an active credit_line_request_pagare row for a country flags it as requiring the pagaré step.
credit_lines_contracts_templatesRegistry of DocuSign Template IDs per locale + person_type

Schema: credit_line_requests

ColumnTypeDescription
idINT (PK)Auto-increment
company_idINT (FK)Requesting company
ticket_idINT (FK)Linked support ticket
statusENUMpending_commercial, approved_commercial, document_rejected, pagare_sent, pagare_review, pagare_validated, contract_sent, legal_review, active, rejected
contract_statusENUMno_contract, sent, created, signed, manual
docusign_envelope_idVARCHAR(100)DocuSign envelope tracking ID
docusign_contract_file_urlVARCHARS3 URL of signed contract PDF
requested_amountDECIMALAmount requested by client
requested_daysINTCredit term days requested
approved_amountDECIMALAmount approved by commercial team
approved_daysINTCredit term approved
rejection_reasonTEXTFree-text rejection reason
created_atDATETIMERequest creation timestamp

Schema: credit_line_request_variable_values

ColumnTypeDescription
idINT (PK)Auto-increment
credit_line_request_idINT (FK)Owning request
field_idVARCHAR(50)Identifier of the form field; matches a fieldId in the generic_forms.json_structure for the request's country/person type
valueTEXTSubmitted value (URL for file types, plain text for input, ISO date for date)
statusENUMpending, approved, rejected
rejection_reasonVARCHAR(255)Free-text rejection reason
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp

The field_id column is the canonical reference into the JSON form definition (generic_forms.json_structure). The deprecated document_config_id column and the credit_line_request_document_config table have been fully removed.

Reserved pagaré field_id values

The pagaré sub-flow reuses this same table with two reserved field_id values (defined in CREDIT_LINE_PAGARE_FIELD):

field_idUploaded bystatus meaning
pagare_formatLegal (admon)Always stored as approved so it is excluded from pending/required checks and bulk updates. value is the S3 URL of the unsigned format
pagare_signedClient (portal)Validation state of the signed copy: pendingapproved (validated) / removed on rejection. value is the S3 URL of the signed PDF

When legal rejects the signed pagaré, the pagare_signed row and its S3 file are deleted (not just flagged), so the client effectively re-uploads a fresh copy.

Schema: generic_forms (credit line form definition)

For the credit line module up to three rows per country are expected (or a fallback row with default_flag = 1): the KYB and KYC document forms, plus an optional credit_line_request_pagare form for countries that require a promissory note (currently Mexico).

ColumnValue (credit line)
country_codeISO country code (MX, BR, etc.)
formcredit_line_request_kyb, credit_line_request_kyc, or credit_line_request_pagare
active1 to be picked by the lookup
default_flag1 only on the catch-all fallback row
json_structureOrdered array of field definitions (see shape below)

Each entry inside json_structure follows this shape:

json
{
  "notes": "tax_status_certificate",
  "rules": { "required": false },
  "fieldId": "tax_status_certificate",
  "fieldName": "tax_status_certificate",
  "fieldType": "file",
  "sortOrder": 2,
  "catalogId": 11,
  "fieldLabelLang": "creditLineRequests.documentConfig.mx.tax_status_certificate"
}

Field semantics:

  • fieldId — Unique identifier used as credit_line_request_variable_values.field_id. Stored as VARCHAR(50). Mirrors the key from catalog_credit_line_variables.
  • fieldName — HTML/form name for the field. Always equal to fieldId for credit line documents (kept distinct from fieldId because other generic_forms consumers expect this convention).
  • fieldTypefile, input or date. Drives how the value is rendered/uploaded in the client portal and how DocuSign tabs are resolved in admon.
  • fieldLabelLang — i18n key consumed by t(doc.label) in the frontends. Translations live in the locale JSON files (see envia-clients/src/locales and admon/frontend/.../locales). If the key has no translation, the consumers fall back to fieldId.
  • sortOrder — Display order in the client form and admin detail.
  • rules.requiredtrue if the document is required for submission. This is currently the only rule consumed by the credit line flow.
  • notes — Optional admin note carried over from the original catalog definition.
  • catalogId — Optional pointer to catalog_credit_line_variables.id for traceability.

Note: Earlier revisions of these rows included fieldLabel (plain-text fallback) and dataType. Both were removed in migration.sql PHASE 1.1 to keep the JSON minimal — the credit line flow renders labels exclusively from fieldLabelLang (with fieldId as fallback) and infers data handling from fieldType.

The lookup performed by both queries/services/credit-line.service.js and admon/backend/libraries/credit-line-requests.util.js is:

sql
SELECT json_structure
FROM generic_forms
WHERE form = ?                          -- credit_line_request_kyb | credit_line_request_kyc
  AND (country_code = ? OR default_flag = 1)
  AND active = 1
ORDER BY (country_code = ?) DESC, default_flag ASC, id ASC
LIMIT 1

The exact-country match wins; default_flag = 1 only kicks in when no country-specific row exists.

loadCreditLinePagareFields(countryCode) uses the same lookup with form = 'credit_line_request_pagare' but without the person-type dimension (one pagaré form is shared by KYB and KYC). A non-empty result is what flags the country as requiring the pagaré step (_countryRequiresPagare).

Schema: credit_lines_contracts_templates

ColumnTypeDescription
idINT (PK)Auto-increment
nameVARCHAR(255)Friendly name for the contract configuration
descriptionTEXTOptional description
docusign_template_idVARCHAR(255)Template ID in DocuSign (from DocuSign account)
locale_idINT (FK)Country scope
person_typeENUMkyb or kyc
is_activeTINYINTOnly one active per locale + person_type
created_byINT (FK)Admin user who created
updated_byINT (FK)Admin user who last updated

Entity Relationships

DocuSign Integration

Authentication

The DocuSignService (backend/services/docusign/index.js) uses JWT Bearer Grant (RFC 7523) with RS256 to obtain access tokens. Tokens are cached in Redis for 3,500 seconds (just under the 3,600 s DocuSign expiry). Account info (account ID + base URI) is cached separately for 24 hours.

Webhook Security

DocuSign webhooks are verified using HMAC-SHA256 (X-DocuSign-Signature-1 header). The webhook route (POST /credit-line-requests/webhooks/docusign) has no JWT auth — it is secured exclusively by the HMAC check. The DOCUSIGN_CONNECT_HMAC_SECRET environment variable must match the Connect Key registered in DocuSign Admin → eSignature Admin → Connect → Connect Keys.

Envelope Payload

Envelopes are created from a DocuSign Template (templateId). The changeRoutingOrder: true option is passed to avoid role mis-mapping when template routing order differs from the send-time configuration.

When DOCUSIGN_CONNECT_HMAC_SECRET is configured, the envelope definition automatically includes an eventNotification block for Completed envelope and recipient events in SIM delivery mode (JSON format, restv2.1).

Environment Variables

These variables were added as part of this feature. All are required in production; the service will not start correctly without them.

VariableRequiredDescription
DOCUSIGN_AUTH_HOSTYesDocuSign OAuth host. Use account-d.docusign.com (demo) or account.docusign.com (production)
DOCUSIGN_INTEGRATION_KEYYesApplication Integration Key (Client ID) from the DocuSign developer account
DOCUSIGN_USER_IDYesAPI Username (GUID) of the DocuSign user that impersonates contract sending
DOCUSIGN_PRIVATE_KEYYesRSA private key (PEM, \\n-encoded for env var) for JWT Bearer Grant. Must match the public key uploaded to DocuSign
DOCUSIGN_CONNECT_HMAC_SECRETYes (production)HMAC secret registered as a Connect Key in DocuSign Admin. Used to verify X-DocuSign-Signature-1 on incoming webhooks. If absent, webhook calls will be rejected with 500
DOCUSIGN_WEBHOOK_PUBLIC_BASEDev/ngrokPublic base URL for the DocuSign webhook receiver. Overrides HOSTNAME for local development with ngrok. Example: https://abc123.ngrok.io

WARNING

DOCUSIGN_WEBHOOK_PUBLIC_BASE is only needed when developing locally and routing DocuSign webhooks through a tunnel (e.g. ngrok). In production, DocuSign uses the HOSTNAME variable automatically.

Key Decisions

DecisionReasoningAlternatives Considered
Webhook route has no JWT authDocuSign calls the webhook server-to-server; no JWT is available. Security is provided by HMAC-SHA256 signature verification insteadIP allowlist (fragile, DocuSign ranges change)
Access token cached in RedisDocuSign tokens last 3,600 s. Caching avoids a JWT round-trip on every envelope creation. 3,500 s TTL provides a safe marginPer-request token (unnecessary overhead), in-memory cache (lost on restart)
Account info cached 24 haccount_id and base_uri are stable. A 24 h cache eliminates the /oauth/userinfo call on warm pathsNo cache (extra HTTP call per request), very long TTL (stale if account changes)
One active DocuSign contract per locale + person_typePrevents ambiguity when resolving which template to use for a given companyMultiple active templates (requires additional selection UI)
changeRoutingOrder: true on envelope creationWithout this, DocuSign can mis-map template roles when routingOrder differs, causing missing tabs for later signersDefault (causes intermittent tab loss)
Documents stored in credit_line_request_variable_values (not S3 on creation)Files are uploaded via the admin interface after review; this allows rejection/resubmission without re-uploading to storageDirect S3 upload on client submission (harder to manage rejected documents)
Pagaré requirement is data-driven via generic_forms (not a hardcoded country list)Adding/removing the pagaré step for a country is a configuration change (insert/deactivate a credit_line_request_pagare row), not a code change. Keeps the flow multi-country ready while only Mexico is liveHardcoded if (country === 'MX') checks (require deploys to change scope)
Pagaré reuses credit_line_request_variable_values with reserved field_idsAvoids a new table; the format/signed documents fit the existing document model and S3 storage patternDedicated credit_line_pagares table (extra schema for two files)
Rejected signed pagaré is hard-deleted (file + row)Forces a clean re-upload and keeps a single source of truth for the current signed copy; the request returns to pagare_sentSoft-flag as rejected (leaves stale files and ambiguous "which copy is current")
Format stored as approved, pagaré documents hidden from the generic listPrevents the legal-provided format and the dedicated pagaré UI from polluting the KYC/KYB document review and bulk actionsShow all documents in one list (confusing review UX, format wrongly counted as pending)
Separate credit-line-requests-review-documents permissionLets the document/pagaré validation responsibility be granted to legal reviewers independently of the broader credit-line-requests-manage actionsSingle manage permission (no separation of duties)

Dependencies

  • Internal: Tickets module (ticket creation triggers request creation), Permissions module (permissionMiddleware.canByName)
  • External: DocuSign eSign REST API, Redis (token cache), AWS S3 (signed contract storage), Mailgun (client notifications)
  • Cross-repo: queries API (client eligibility, document submission, resubmission); envia-clients (client portal UI)

Envia Admin