# Application Form Copies New table `copies` lives in partner's database. Possible attributes: - `id` (uuid) - `application_form_id` (uuid) - `title` (string) - `owner_id` (uuid) - `locked_at` (datetime) - `soft_deleted_at` (datetime) - `schema_version` (integer) - `document` (jsonb/protobuf) - protobuf cans be [~3.5x faster](https://hackernoon.com/protobuf-vs-json-in-the-ruby-world) in serializing/deserializing compared to Oj - spike on performance here on writing answers - spike on breaking this document up into multiple columns and performance benefits - `last_touched_by_user_at` (datetime) - in lieu of app form having active copy id - `created_at` (datetime) - `updated_at` (datetime) Possible document... :::info This removes the need for the following tables: - `answers` - `answer_choices` - `answer_pools` - `application_forms_question_sets` - `forced_exclusions` ::: ```ruby { afqs_list: [{ id: '<UUID>', question_set_id: '...', remove_if_ineligible: true, completed_at: '...', added_by_type: '...', added_by_id: '...' created_at: '...', updated_at: '...' }, { id: '<UUID>', question_set_id: '...' remove_if_ineligible: true, completed_at: nil, added_by_type: '...', added_by_id: '...' created_at: '...', updated_at: '...' }], # Spiking on flat answers vs answers nested under pools # Spike on answers being a hash with { id => { ... } } answers: [{ id: '<UUID>', question_id: '...', # mqs_first_name (text) value: 'John', active: true, required: true, is_readonly: false, display_type: 'public', answered_by_type: '...', answered_by_id: '...', answered_by_source: '...', created_at: '...', updated_at: '...' }, { id: '<UUID>', question_id: '...', # mqs_fein (text / sensitive) encrypted_value: '...', encrypted_value_iv: '...', # ... }], answer_pools: [{ id: '<UUID>', question_pool_id: '...', # locations soft_deleted: false, created_at: '...', updated_at: '...', answers: [{ id: '<UUID>', question_id: '...', # mqs_loc_state (dropdown) choice_id: '...', # ... }], answer_pools: [{ id: '<UUID>', question_pool_id: '...', # buildings (nested under locations) soft_deleted: false, created_at: '...', updated_at: '...', answers: [{ id: '<UUID>', question_id: '...', # multi-select question choice_ids: ['...', '...'] # ... }], # ... }], forced_excluded_carrier_product_ids: ['...', '...'], # These are moved from app forms table. Please review the # app forms table and verify more don't need to be moved over... agent_bypassed_carrier_product_ids: ['...', '...'], } ``` By nesting pools under each other we remove the need for `:parent_pool_id`. It also makes deleting pools simpler. It also better matches our quote start API. Tables that need a `copy_id` column: - `application_form_processes` - These are tied to answers, so need to know the copy - `quote_requests` - This is the whole reason we are doing copies - `eligibility_results` - These are sometimes tied to an AFQS, so need to know the copy - `messages` Types include: `:application_form`, `:answer`, `:real_time_eligibility`, `:quote_request`. But the `metadata` column can contain any identifying information for the given type. - Add `copy_id` column - `application_form_disposition` ??? - Adi will follow-up with product on this - `events` ??? - Types include: - `:quote_request_selected` - `:application_form_retrieved` - `:external_transfer` - `:guest_flow_email_sent` - `:agent_needs_bind_assistance_email_sent` - `:bridged_to_off_platform_carrier` - These can be created everytime a user clicks on something - Do we allow more than one email to be sent to an agent (one for each copy? or one for each app form?) - There is a discussion about removing `:application_form_id` and making a polymorphic column that points to what object it is applied on - `naics_searches` ??? - Adi will follow-up with BI on this - But sounds like we might be able to keep this on the app form - `payment_methods` ??? - Keep on app form for now - `tokens` ??? - Only ever used on guest flow to access their app form - Jared thinks this should live on the copy now - Jared thinks this might also be used in our websocket cable - So maybe add `copy_id` column Do we slowly deprecate the `:application_form_id` columns for the above? ### Notes: - Must live behind a feature flag. Feature flag is controlled by owner. - We will need to detect if an app form is a "copy"-type app form dynamically and progress forward with that regardless whether the feature flag is on or off. - On app form creation when the feature flag is enabled we will create a default copy for the app form. Do we want the default copy id to match the app form id? This will make it more backwards compatible. - No two instance can work on a copy at any given time. So we will want to lock on copy id. - Or we let it raise an error if the nonce is not what is expected - When no copy id can be inferred then we use the copy with the latest `last_touched_by_user_at` value. - As above we would then obtain a lock on this copy. - Front-end is serial but app form processes and quote request imports are async. - Need to speed up quote request import - Need to get smart about when we check out the copy lock for app form processes, e.g., 1. Create context and checkout lock before submitting to third-party API 2. Release lock and submit to third-party API 3. Create new context and checkout lock again - For long-lived processes (e.g., app form processes, quote requests, ...) we may need a more long-lived lock that blocks copying of a copy (~5 min) - This could be a short-term solution and after we clean these up we could support them updating multiple copies at once - Cloning... - Within tenant: We clone all copies and re-run rules, but only re-quote last touched copy - Across tenants: We clone ONLY the requested copy (if no copy id supplied then we assume last touched copy) - What about locked app forms? - Only mark quotes state on other copies for specific carriers (e.g., NW). Should this property live on the tenant in Authenticator? - Maybe for day 0 we mark all carrier quotes as stale when quoting in another copy - App form state (and copy state) are dynamically derived. - Perform fuzzy lookup of app form id and check if it is really a copy id? - First lookup id in copies table, if not found then look in app forms table - Great for backwards compatibility. - Doesn't stop us from creating new copy-only endpoints as we slowly migrate over. - Explore how referrals work in the future. - Explore putting the KMS key on the copy and copying that key to all copies. Even copying the KMS key during cloning. ### Endpoints Create a new copy on an app form: ``` POST /application_forms/:application_form_id/copies ``` Do we want to make all other endpoints "shallow" (i.e., only `/copies/:id`)? It seems in cases like `answers`, `answer_pools`, AFQS's, ... we put all of these under the app form (i.e., `/application_forms/:app_form_id/*`). So for example, if we wanted to soft delete a copy on an app form: ``` DELETE /application_forms/:application_form_id/copies/:id ``` or we could have... ``` DELETE /copies/:id ``` Maybe we should slowly replace the relevant `/application_forms/:app_form_id/*` endpoints with `/copies/:copy_id/*`, but do fuzzy matching on `:app_form_id` in the meantime until a cutover. After discussion... - Only keep it under the `/copies/*` namespace - Utilize current endpoints as much as possible for backwards compatibility - Spin off new endpoints as we notice a discrepancy - Look into deprecating old endpoints ### User Stories Notes -