# Renewal Views ## Scope Define SQL views in `fenedgec_members` to support renewal-letter generation. These views must read only from the existing local tables: * `fenedgec_members.accounts` * `fenedgec_members.contacts` * `fenedgec_members.renewals` * `fenedgec_members.picklist_account_type` * `fenedgec_members.picklist_sector` * `fenedgec_members.picklist_public_location` Per `requirements/environment.md`: * Recipient/source reads are in `MEMBERS_REMOTE_MYSQL_DB` (here: `fenedgec_members`). * No fallback behavior is defined in this requirement. ## Source Filters (Current Schema) Use only active rows: * Accounts: `a.is_deleted = 0` * Contacts: `c.is_deleted = 0` * Renewals: no delete flag exists in `renewals`; include all renewal rows unless other filter criteria apply. ## Required Output Data Account details: * `accounts.id` * `accounts.name` * `accounts.account_no_of_members` * `accounts.account_type_id` plus joined `picklist_account_type.value` exposed as `accounts.type` * `accounts.public_location_id` plus joined `picklist_public_location.value` exposed as `accounts.public_location` * `accounts.sector_id` plus joined `picklist_sector.value` exposed as `accounts.account_sector` * `accounts.facebook` * `accounts.twitter` * `accounts.other_social` Renewal details: * `renewals.id` * `renewals.account_id` * `renewals.renewal_year` * `renewals.status` * `renewals.payment_method` * `renewals.payment_amount` * `renewals.payment_date` * `renewals.selected` * `renewals.created_at` * `renewals.updated_at` Contact 1 details (`contacts.is_contact_1 = 1`): * Name * Position/title * Address * Email * Phone 1 * Phone 2 Contact 2 details (`contacts.is_contact_2 = 1`): * Name * Position/title * Email * Phone 1 * Phone 2 Fen Edge News contact details (`contacts.is_fen_1 = 1`): * Name * Email Public contact details (contacts.is_public_contact = 1): * Public Phone * Public Email ## View Hierarchy Create the following views: * `renewal_members` (accounts whose account type picklist slug is `member`) * `renewal_pending` (renewals with `status = 'pending'`) * `renewal_contact_1` (contacts where `is_contact_1 = 1`) * `renewal_contact_2` (contacts where `is_contact_2 = 1`) * `renewal_fen_contact` (contacts where `is_fen_1 = 1`) * `renewal_public_contact` (contacts where `is_public_contact = 1`) * `renewal_accounts_with_contacts` (final denormalized export view joining the above plus `renewal_pending`) ## Column Rules ### `renewal_members` Required columns: * `account_id` -> `accounts.id` * `account_name` -> `accounts.name` * `account_no_of_members` -> `accounts.account_no_of_members` * `account_type_id` -> `accounts.account_type_id` * `account_type` -> `picklist_account_type.value` (equivalent to `accounts.type`) * `sector_id` -> `accounts.sector_id` * `account_sector` -> `picklist_sector.value` (equivalent to `accounts.account_sector`) * `public_location_id` -> `accounts.public_location_id` * `public_location` -> `picklist_public_location.value` (equivalent to `accounts.public_location`) * `account_facebook` -> `accounts.facebook` * `account_twitter` -> `accounts.twitter` * `account_other_social` -> `accounts.other_social` Filter: * Member-status check must use account type picklist `slug`. * `LOWER(TRIM(COALESCE(picklist_account_type.slug, ''))) = 'member'` ### Contact views Each contact view must expose all contact fields needed for renewals, prefixed by view role: * `renewal_contact_1`: prefix `contact_1_` * `renewal_contact_2`: prefix `contact_2_` * `renewal_fen_contact`: prefix `fen_contact_` * `renewal_public_contact`: prefix `public_contact_` Minimum required prefixed columns: * `..._contact_id` -> `contacts.id` * `..._account_id` -> `contacts.account_id` * `..._name` -> `TRIM(CONCAT_WS(' ', contacts.first_name, contacts.last_name))` * `..._position` -> `contacts.title` * `..._address_1` -> `contacts.contact_address_1` * `..._address_2` -> `contacts.contact_address_2` * `..._town` -> `contacts.contact_town` * `..._county` -> `contacts.contact_county` * `..._postcode` -> `contacts.contact_postcode` * `..._email` -> `contacts.contact_email_1` * `..._phone_1` -> `contacts.phone_1` * `..._phone_2` -> `contacts.phone_2` ### `renewal_public_contact` Required columns: * `public_contact_contact_id` -> `contacts.id` * `public_contact_account_id` -> `contacts.account_id` * `public_contact_public_phone` -> `contacts.public_phone` * `public_contact_public_email` -> `contacts.public_email` Filter: * `contacts.is_public_contact = 1` ### `renewal_pending` Required columns: * `renewal_id` -> `renewals.id` * `renewal_account_id` -> `renewals.account_id` * `renewal_year` -> `renewals.renewal_year` * `renewal_status` -> `renewals.status` * `renewal_selected` -> `renewals.selected` * `renewal_payment_method` -> `renewals.payment_method` * `renewal_payment_amount` -> `renewals.payment_amount` * `renewal_payment_date` -> `renewals.payment_date` * `renewal_created_at` -> `renewals.created_at` * `renewal_updated_at` -> `renewals.updated_at` Filter: * `LOWER(TRIM(COALESCE(renewals.status, ''))) = 'pending'` ### `renewal_accounts_with_contacts` Join key: * `renewal_members.account_id = renewal_pending.renewal_account_id` * `renewal_members.account_id = renewal_contact_X.contact_X_account_id` * `renewal_members.account_id = renewal_public_contact.public_contact_account_id` Join type: * Left join all contact views so an account still appears when some contact roles are missing. Output: * All `renewal_members` columns * All `renewal_pending` columns * Selected prefixed columns from each contact view ## Multiple-Match Tie-Break Rules If multiple contacts match the same role for one account: * Pick the lowest `contacts.id`. * This rule must be deterministic and documented in view SQL (for example via grouped subquery/min id). ## Null and Empty Handling * Preserve NULL values from source fields. * Computed `..._name` may be empty string when all name parts are blank. * Do not convert NULL to placeholder text such as `N/A`. ## Legacy Name Review (Flagged) The following names in prior drafts do not match current schema and are explicitly flagged: * `members.Account` -> `NON-EXISTENT` (use `fenedgec_members.accounts`) * `members.Contact` -> `NON-EXISTENT` (use `fenedgec_members.contacts`) * `members.renewals` -> `NON-EXISTENT` in that schema name (use `fenedgec_members.renewals`) * `Account.Name` -> `NON-EXISTENT` (use `accounts.name`) * `Account.Type` -> `NON-EXISTENT` (use `accounts.account_type_id` + `picklist_account_type.slug` for member-status checks, and `picklist_account_type.value` for display) * `Account.AccountNoOfMembers` -> `NON-EXISTENT` (use `accounts.account_no_of_members`) * `Contact.Accountid` -> `NON-EXISTENT` (use `contacts.account_id`) * `First`/`Middle`/`Last` -> `NON-EXISTENT` (use `first_name`/`last_name`) * `JobTitle` -> `NON-EXISTENT`; mapped to `contacts.title` (`AMBIGUOUS: verify business meaning`) * `Address1`/`Address2`/`Town`/`County`/`Postcode` -> `NON-EXISTENT` (use `contact_address_1`/`contact_address_2`/`contact_town`/`contact_county`/`contact_postcode`) * `Email` -> `NON-EXISTENT`; mapped to `contacts.contact_email_1` (`AMBIGUOUS: confirm whether `contact_email_2 ` should also be included`) * `Home` -> `NON-EXISTENT` (use `contacts.phone_1`) * `Mobile` -> `NON-EXISTENT` (use `contacts.phone_2`) * `Contact1` -> `NON-EXISTENT` (use `contacts.is_contact_1`) * `Contact2` -> `NON-EXISTENT` (use `contacts.is_contact_2`) * `FENContact1` -> `NON-EXISTENT` (use `contacts.is_fen_1`) * `deleted` -> `NON-EXISTENT` on accounts/contacts (use `is_deleted`) ## Acceptance Criteria * All views compile in `fenedgec_members`. * `renewal_members` contains only active member accounts. * `renewal_pending` contains only rows where status is `pending`. * `renewal_contact_1` only includes contacts flagged `is_contact_1 = 1`. * `renewal_contact_2` only includes contacts flagged `is_contact_2 = 1`. * `renewal_fen_contact` only includes contacts flagged `is_fen_1 = 1`. * `renewal_public_contact` only includes contacts flagged `is_public_contact = 1`. * `renewal_accounts_with_contacts` returns one row per pending renewal row (no duplicated `renewal_id`). * `account_no_of_members` is present in final output. * Renewal fields listed above are present in final output. * Account picklist-backed values are present in final output as `account_type`/`account_sector`/`public_location` (or equivalent `accounts.type`/`accounts.account_sector`/`accounts.public_location` aliases). * Account social fields are present in final output as `account_facebook`, `account_twitter`, and `account_other_social`. * Member-status check is based on `picklist_account_type.slug`, not the label text. * No remote cross-system query is used by these views.