239 lines
8.5 KiB
Markdown
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.
|