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
| Concept | Description |
|---|---|
| Credit Line Request | A formal application for a revolving credit line, linked 1:1 to a support ticket of type credit |
| Request Status | Lifecycle stage. Base path: pending_commercial → approved_commercial → contract_sent → legal_review/active/rejected. Pagaré countries insert pagare_sent → pagare_review → pagare_validated between document approval and contract creation. A rejected document moves the request to document_rejected |
| Contract Status | DocuSign envelope state: no_contract → sent → created → signed (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é Form | An 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 Envelope | An electronic signing package sent to the client via DocuSign, tracked by docusign_envelope_id |
| DocuSign Credit Contract | Admin-registered mapping of a DocuSign Template ID to a locale + person type combination |
| Document Config | Per-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 Catalog | Global registry of available document variables (catalog_credit_line_variables); referenced from the form JSON via catalogId |
| SLA | Days 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. Returnseligible, 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 thejson_structureof the matchinggeneric_formsrow (form = 'credit_line_request_kyb' | 'credit_line_request_kyc'). Each document includesfield_id(string identifier from the JSON),key,type,label(i18n key),required, andsort_order. - Create request (via company ticket flow): when a client submits a credit ticket (
type_id = 19), acredit_line_requestsrecord is created automatically with initial documents incredit_line_request_variable_values. The submitted documents reference the form viafield_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 topending). - Pagaré (Mexico): once legal sends the pagaré format, the client downloads
pagare_format, prints/signs it, and uploads the signed copy aspagare_signed. This moves the request topagare_reviewfor legal validation. (Client-side upload is implemented in thequeries/envia-clientsrepos; 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_validatedsegment only exists for countries with a configured pagaré form (currently Mexico). For all other countries the request goes straight fromapproved_commercialtocontract_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()| Attribute | Value |
|---|---|
| Target status | pending_commercial only (v1 scope) |
| Threshold | 30 calendar days since created_at (CREDIT_LINE_PENDING_COMMERCIAL_AUTO_REJECT_DAYS) |
| Final status | rejected |
resolved_by | SYSTEM_USER_ID = 0 — distinguishes auto-reject from manual reject |
resolved_at | Set at rejection time by updateRequestStatus |
| Ticket | Set to DECLINED; localized rejection message inserted as a visible system comment |
| i18n key | creditLineRequests.autoReject.expired (falls back to English when no translation found) |
| Queue | credit_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_commercialfor 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 queuesFiles (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
| Rule | Implementation |
|---|---|
Only one active DocuSign contract per locale + person_type | assertAtMostOneActivePerCountryAndPersonType 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 100k | creditLineService.checkEligibility in queries |
| Approved documents cannot be resubmitted | resubmitDocument rejects if status === 'approved' |
| HMAC signature required for all DocuSign webhook calls | verifyDocuSignHmacSignature in auths.util.js; throws 401 if missing/invalid |
| DocuSign webhook route has no JWT auth | auth: 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 approved | uploadPagareFormat validates status, country, and pending documents before storing the format and moving to pagare_sent |
Pagaré can only be validated in pagare_review | validatePagare 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 creation | createContract 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 actions | pagare_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 permission | PATCH .../documents/{documentId}, PATCH .../documents/bulk and PATCH .../pagare/validate require credit-line-requests-review-documents (split from credit-line-requests-manage) |
Database
Tables
| Table | Purpose |
|---|---|
credit_line_requests | One record per credit ticket; tracks lifecycle status, DocuSign state, amounts |
credit_line_request_variable_values | Submitted 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_variables | Global 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_forms | Per-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_templates | Registry of DocuSign Template IDs per locale + person_type |
Schema: credit_line_requests
| Column | Type | Description |
|---|---|---|
id | INT (PK) | Auto-increment |
company_id | INT (FK) | Requesting company |
ticket_id | INT (FK) | Linked support ticket |
status | ENUM | pending_commercial, approved_commercial, document_rejected, pagare_sent, pagare_review, pagare_validated, contract_sent, legal_review, active, rejected |
contract_status | ENUM | no_contract, sent, created, signed, manual |
docusign_envelope_id | VARCHAR(100) | DocuSign envelope tracking ID |
docusign_contract_file_url | VARCHAR | S3 URL of signed contract PDF |
requested_amount | DECIMAL | Amount requested by client |
requested_days | INT | Credit term days requested |
approved_amount | DECIMAL | Amount approved by commercial team |
approved_days | INT | Credit term approved |
rejection_reason | TEXT | Free-text rejection reason |
created_at | DATETIME | Request creation timestamp |
Schema: credit_line_request_variable_values
| Column | Type | Description |
|---|---|---|
id | INT (PK) | Auto-increment |
credit_line_request_id | INT (FK) | Owning request |
field_id | VARCHAR(50) | Identifier of the form field; matches a fieldId in the generic_forms.json_structure for the request's country/person type |
value | TEXT | Submitted value (URL for file types, plain text for input, ISO date for date) |
status | ENUM | pending, approved, rejected |
rejection_reason | VARCHAR(255) | Free-text rejection reason |
created_at | TIMESTAMP | Creation timestamp |
updated_at | TIMESTAMP | Last 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_id | Uploaded by | status meaning |
|---|---|---|
pagare_format | Legal (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_signed | Client (portal) | Validation state of the signed copy: pending → approved (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).
| Column | Value (credit line) |
|---|---|
country_code | ISO country code (MX, BR, etc.) |
form | credit_line_request_kyb, credit_line_request_kyc, or credit_line_request_pagare |
active | 1 to be picked by the lookup |
default_flag | 1 only on the catch-all fallback row |
json_structure | Ordered array of field definitions (see shape below) |
Each entry inside json_structure follows this shape:
{
"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 ascredit_line_request_variable_values.field_id. Stored as VARCHAR(50). Mirrors thekeyfromcatalog_credit_line_variables.fieldName— HTML/form name for the field. Always equal tofieldIdfor credit line documents (kept distinct fromfieldIdbecause othergeneric_formsconsumers expect this convention).fieldType—file,inputordate. Drives how the value is rendered/uploaded in the client portal and how DocuSign tabs are resolved in admon.fieldLabelLang— i18n key consumed byt(doc.label)in the frontends. Translations live in the locale JSON files (seeenvia-clients/src/localesandadmon/frontend/.../locales). If the key has no translation, the consumers fall back tofieldId.sortOrder— Display order in the client form and admin detail.rules.required—trueif 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 tocatalog_credit_line_variables.idfor traceability.
Note: Earlier revisions of these rows included
fieldLabel(plain-text fallback) anddataType. Both were removed inmigration.sqlPHASE 1.1 to keep the JSON minimal — the credit line flow renders labels exclusively fromfieldLabelLang(withfieldIdas fallback) and infers data handling fromfieldType.
The lookup performed by both queries/services/credit-line.service.js and admon/backend/libraries/credit-line-requests.util.js is:
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 1The 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
| Column | Type | Description |
|---|---|---|
id | INT (PK) | Auto-increment |
name | VARCHAR(255) | Friendly name for the contract configuration |
description | TEXT | Optional description |
docusign_template_id | VARCHAR(255) | Template ID in DocuSign (from DocuSign account) |
locale_id | INT (FK) | Country scope |
person_type | ENUM | kyb or kyc |
is_active | TINYINT | Only one active per locale + person_type |
created_by | INT (FK) | Admin user who created |
updated_by | INT (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.
| Variable | Required | Description |
|---|---|---|
DOCUSIGN_AUTH_HOST | Yes | DocuSign OAuth host. Use account-d.docusign.com (demo) or account.docusign.com (production) |
DOCUSIGN_INTEGRATION_KEY | Yes | Application Integration Key (Client ID) from the DocuSign developer account |
DOCUSIGN_USER_ID | Yes | API Username (GUID) of the DocuSign user that impersonates contract sending |
DOCUSIGN_PRIVATE_KEY | Yes | RSA private key (PEM, \\n-encoded for env var) for JWT Bearer Grant. Must match the public key uploaded to DocuSign |
DOCUSIGN_CONNECT_HMAC_SECRET | Yes (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_BASE | Dev/ngrok | Public 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
| Decision | Reasoning | Alternatives Considered |
|---|---|---|
| Webhook route has no JWT auth | DocuSign calls the webhook server-to-server; no JWT is available. Security is provided by HMAC-SHA256 signature verification instead | IP allowlist (fragile, DocuSign ranges change) |
| Access token cached in Redis | DocuSign tokens last 3,600 s. Caching avoids a JWT round-trip on every envelope creation. 3,500 s TTL provides a safe margin | Per-request token (unnecessary overhead), in-memory cache (lost on restart) |
| Account info cached 24 h | account_id and base_uri are stable. A 24 h cache eliminates the /oauth/userinfo call on warm paths | No cache (extra HTTP call per request), very long TTL (stale if account changes) |
| One active DocuSign contract per locale + person_type | Prevents ambiguity when resolving which template to use for a given company | Multiple active templates (requires additional selection UI) |
changeRoutingOrder: true on envelope creation | Without this, DocuSign can mis-map template roles when routingOrder differs, causing missing tabs for later signers | Default (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 storage | Direct 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 live | Hardcoded if (country === 'MX') checks (require deploys to change scope) |
Pagaré reuses credit_line_request_variable_values with reserved field_ids | Avoids a new table; the format/signed documents fit the existing document model and S3 storage pattern | Dedicated 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_sent | Soft-flag as rejected (leaves stale files and ambiguous "which copy is current") |
Format stored as approved, pagaré documents hidden from the generic list | Prevents the legal-provided format and the dedicated pagaré UI from polluting the KYC/KYB document review and bulk actions | Show all documents in one list (confusing review UX, format wrongly counted as pending) |
Separate credit-line-requests-review-documents permission | Lets the document/pagaré validation responsibility be granted to legal reviewers independently of the broader credit-line-requests-manage actions | Single 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:
queriesAPI (client eligibility, document submission, resubmission);envia-clients(client portal UI)
Related Documentation
- API Endpoints — Complete endpoint reference for this module
- UI Screens & Flows — Frontend screens and user interactions
- User Guide — Step-by-step admin workflow
