# 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.
