User Response Controller
π Overviewβ
Controllers/userresponse.js receives every form submissionβwhether submitted publicly via share link or via a tracked requestβand fans the data out to downstream systems. Beyond storing the raw payload, it updates invite statuses, kicks off onboarding automations (Instasites/Instareports builds, CRM contact creation, support conversations), updates Store orders, and exposes rich listings for agency dashboards (including cross-account views).
ποΈ Collections touchedβ
| Collection | Purpose |
|---|---|
forms.userresponse | Persists submission payloads, analytics metadata, and soft-delete flags. |
forms.sent.requests | Lookup + update request status, link responses to invites, compute pending queues. |
forms | Resolves ownership and ensures delete permissions before mutating responses. |
campaign.data | Indirect via the inbound utility (determines automation recipes). |
crm.contacts | Hydrates recipient info, builds pending request payloads, and enriches in-bound submissions. |
support.* collections | Created during inbound automation to open support rooms/messages for new leads. |
_store.orders | Updates onboarding status when a response matches a stored request. |
funnels / funnel.analytics | When responses originate from funnel steps, analytics are logged. |
queues, leads-data, instasites, instareports | All triggered inside utilities/inbound.js for automation side effects. |
π Middleware & validationβ
Routes under /forms reuse the module validators (validators/index.js) with the following highlights:
- Most list endpoints require
forms.read(optionallyforms.templatesfor template context) and usevalidateLimitfor pagination bounds. - Sub-account accessors (
/forms/:subaccountid/subaccountresponseetc.) verify the requested account is a child of the authenticated parent, returningRESOURCE_NOT_FOUNDotherwise. - Public submissions (
POST /forms/:formid/userresponse) run the Joi guard but skip authorization to keep share links open. - Update/delete paths ensure
formidandresponseidare valid ObjectIds.
π£οΈ Route mapβ
| Method | Path (internal service) | Controller method | Notes |
|---|---|---|---|
POST | /forms/:formid/userresponse | saveResponse | Accepts public submissions or invite completions; orchestrates automation. |
GET | /forms/:formid/userresponse | getUserResponses | Lists submissions (default) or invite-based type=requests; supports export mode. |
PUT | /forms/:formid/userresponse/:responseid | updateUserResponse | PATCH-style update that wraps payload under data. |
PUT | /forms/:formid/formrequest/:requestid | updateFormRequest | Mutate invite metadata (status, reminder flags, etc.). |
GET | /forms/:formid/getpendinguserresponse | getPendingResponses | Pending invites (status: 'sent') for a single form. |
GET | /forms/:subaccountid/subaccountresponse | getSubAccountsResponses | Parent view across a child account's responses. |
GET | /forms/:subaccountid/subaccountpendingrequests | getSubAccountPendingRequests | Pending invite view for a child account. |
GET | /forms/mainaccount/responses | getMainAccountsResponses | Owner-level roll-up of responses. |
GET | /forms/mainaccount/pendingrequests | getmainAccountPendingRequests | Pending invites across the main account. |
GET | /forms/:formid/userresponse/:responseid | getUserResponsesById | Fetches a single response (guarded by scope). |
DELETE | /forms/:formid/userresponse/:responseid | deleteUserResponse | Marks a response is_deleted=true after owner check. |
DELETE | /forms/:formid/userresponse | deleteUserResponses | Bulk soft-delete (comma-separated responses query param). |
βοΈ Submission ingest (saveResponse)β
Two flows share the same method:
-
Invite completion (
rid+rqidpresent)- Validates the invite via
forms.sent.requests. Rejects with descriptive errors if alreadysubmittedorcancelled. - Copies invite metadata onto the response (
form_id,account_id,request_id) and updates the invite status tosubmitted. - Checks the form's owning account status via
checkAccountStatus; returns a success envelope with a failure message if the account is inactive. - Persists the submission (
userResponse.is_deleted=falseby default). - Calls
inbound.inHouseForms, which handles CRM contact upsert, lead creation, notifications, Instasite/Instareport builds, support room creation, and deal creation (depending on campaign config). - Updates
StoreOrderonboarding milestones: when a request matches an onboarding order without a response, the order's status flips toreceived, and response/form IDs are recorded.
- Validates the invite via
-
Public submission (
ridabsent)- Finds the form via
form_secret_idand stamps the plain response withtype: 'public'plus the resolvedform_id. - Performs the same account status check and inbound automation pipeline as above.
- Finds the form via
Common behaviour:
- Accepts optional
analyticspayload to store visitor/session data alongside the response. - Returns
{ success: true, data: <response>, message: 'SUCCESS' }and appendscontact_idfrom the inbound helper when a CRM record is created.
π Listings & exportsβ
getUserResponsesβ
- Query params:
page,limit,type,sort,order,export. - When
type=requests, aggregates againstforms.sent.requests, joining the linked response (status: 'submitted') and contact info. The result object containssubmittedrequestsplus acount; pagination still applies whenlimit > 0. - Default (
typeomitted) reads fromforms.userresponse, filters outis_deleted=true, and returns{ data: [...], total: [...] }from the aggregation facet. export=truebypasses pagination (limit=0), strips Mongo metadata fields (_id,__v, timestamps, control flags), and delivers raw submission content.
getSubAccountsResponses / getMainAccountsResponsesβ
- Parent view ensures the requested sub-account belongs to the current parent; otherwise returns
RESOURCE_NOT_FOUND. - Aggregations enrich responses with contact + business details via
$lookupintocrm.contacts, include funnel metadata, and preserve pagination semantics. - Search applies a regex to
forms.form_name.
Pending invite listingsβ
getSubAccountPendingRequests,getmainAccountPendingRequests, andgetPendingResponsesall delegate toUserResponseModel.getAllPendingResponseswith slightly different conditions (recipient IDs vs account IDs vs form ID).- Each entry includes the form metadata and the recipient contact or user details.
- Sub-account variant appends fully qualified onboarding links using
utilities.getActiveDomainfor the parent account.
π Mutationsβ
updateUserResponsewraps the providedupdateDatainside{ data: { ... } }and runsupdateMany, enabling partial updates to stored payload JSON.updateFormRequestmutates invite records (e.g., settingstatus, adding reminder timestamps) while enforcing account ownership.deleteUserResponse&deleteUserResponsesperform soft deletes (is_deleted: true) after confirming the form belongs to the authenticated account.
β οΈ Edge cases & safeguardsβ
- Account inactivity β Submissions are accepted but respond with
status: falseand a message informing the user the form is inactive whencheckAccountStatusfails. - Invalid request IDs β
saveResponselogs failures updatingStoreOrderwhenrequestIdis malformed to avoid crashing the overall submission pipeline. - Duplicate contacts β Inbound helper performs extensive matching/merging to avoid duplicate CRM contacts; errors are recorded on the response document (
errorsarray) for later inspection. - Bulk deletion β For bulk soft-delete, the controller requires a comma-separated
responseslist and validates owner access before updating. Missing query param returns a 400 error. - Sub-account checks β
getSubAccountsResponsesandgetSubAccountPendingRequestsensure both the sub-account relationship and the presence of a business record; missing context yieldsRESOURCE_NOT_FOUND.
π Related docsβ
- Forms controller β form lifecycle, tag/category management, and request distribution.
- Templates controller β template catalogue used before responses exist.
- Forms module index β module architecture and dependency map.