feca-mailshots-plugin/requirements/renewal_views.md

8.5 KiB

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.