feca-mailshots-plugin/requirements/renewal_views.md

239 lines
8.5 KiB
Markdown

# 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_audience`
* `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`
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
* Mobile
Contact 2 details (`contacts.is_contact_2 = 1`):
* Name
* Position/title
* Email
* Phone
* Mobile
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_audience` -> `accounts.account_audience`
* `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`)
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.middle_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` -> `contacts.home_phone`
* `..._mobile` -> `contacts.mobile`
### `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.AccountAudience` -> `NON-EXISTENT` (use `accounts.account_audience`)
* `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`/`middle_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.home_phone`)
* `Mobile` -> `NON-EXISTENT` (use `contacts.mobile`)
* `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_audience` and `account_no_of_members` are 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).
* Member-status check is based on `picklist_account_type.slug`, not the label text.
* No remote cross-system query is used by these views.