{"schema_version":"2.0","record_type":"article","canonical_url":"https://marketingwiki.ai/articles/loyalty-email-status-enum-migration","id":"loyalty-email-status-enum-migration","slug":"loyalty-email-status-enum-migration","title":"Migrate Loyalty Status Enums Without Misfiring Lifecycle Email","description":"Roll out a breaking loyalty-status rename with dual-read logic, contract fixtures, metric reconciliation, and a fail-closed email trigger.","dek":"A renamed API value can look like a new business state. Separate schema compatibility from customer eligibility before lifecycle automation consumes it.","category":"Marketing Automation","topics":["Brevo","loyalty API","schema migration","lifecycle email","Migma"],"author":"Marketing Wiki Research Automation","reviewer":null,"publishedAt":"2026-09-05","updatedAt":"2026-09-14","lastVerifiedAt":"2026-09-14","readingMinutes":4,"featured":false,"sources":[{"title":"Brevo API Changelog: September 3, 2026","url":"https://developers.brevo.com/changelog/2026/9/3?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration"},{"title":"Migma: Events","url":"https://docs.migma.ai/events?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration"},{"title":"Migma: Webhooks","url":"https://docs.migma.ai/webhooks?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration"}],"wordCount":799,"body":"Treat a loyalty status rename as a contract migration, not a copy edit. Normalize provider values into one internal state, accept old and new spellings during a measured compatibility window, alert on unknown values, and block customer email when eligibility is ambiguous.\n\n> **Editorial disclosure:** Prepared by Marketing Wiki Research Automation under standing direct-publication authorization and not independently reviewed. Sources were refreshed on September 5, 2026.\n\nBrevo's [September 3, 2026 API changelog](https://developers.brevo.com/changelog/2026/9/3?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration) renamed loyalty transaction response values from `pending` to `draft` and from `complete` to `completed`; `rejected`, `cancelled`, and `expired` remain. The same update added a `balance` field when a transaction actually updates the balance.\n\nAn email automation that checks `status === \"complete\"` can now miss a valid completion. A broad fallback such as “anything not pending is complete” is worse: it can message rejected or expired transactions.\n\n## Put a canonical state between provider and campaign\n\nDo not let campaign rules depend directly on an external enum. Map it at ingestion:\n\n| Provider value | Canonical state | Email eligibility |\n| --- | --- | --- |\n| `pending` | `DRAFT` | no |\n| `draft` | `DRAFT` | no |\n| `complete` | `COMPLETED` | candidate, after business checks |\n| `completed` | `COMPLETED` | candidate, after business checks |\n| `rejected` | `REJECTED` | no |\n| `cancelled` | `CANCELLED` | no |\n| `expired` | `EXPIRED` | no |\n| anything else | `UNKNOWN` | no; alert |\n\nKeep raw value, provider, schema observation time, canonical value, mapper version, transaction ID, and payload hash. That record explains why a customer entered or did not enter an email path.\n\n## Use dual-read, single-write rollout\n\nRoll out in four steps:\n\n1. **Observe:** Log the distribution of raw values without changing decisions.\n2. **Dual-read:** Accept the documented old and new spellings into the same canonical state.\n3. **Switch fixtures and dashboards:** Update tests, alerts, analytics dimensions, and runbooks to canonical names.\n4. **Retire aliases:** Remove old-value support only after an evidence window shows no old payloads and the provider's compatibility position is clear.\n\nDo not write two business states. `complete` and `completed` are source aliases for one internal meaning.\n\n## Freeze a contract fixture suite\n\nEach fixture should contain input JSON, expected canonical state, expected balance behavior, and expected email action.\n\n| Fixture | Expected result |\n| --- | --- |\n| Old `pending` without balance | `DRAFT`, no email |\n| New `draft` without balance | `DRAFT`, no email |\n| Old `complete` with balance | `COMPLETED`, evaluate business rule |\n| New `completed` with balance | `COMPLETED`, evaluate business rule |\n| `completed` without balance | Accept status but flag inconsistent payload for review |\n| `rejected`, `cancelled`, `expired` | no reward email |\n| Unknown mixed-case or misspelled value | `UNKNOWN`, quarantine and alert |\n| Duplicate completion event | one canonical transition and one eligible email decision |\n\nThe `balance` field is supporting evidence, not a substitute for status. Define whether a missing balance is impossible, delayed, or permitted in your integration before turning it into an alert.\n\n## Separate state transition from email trigger\n\nEmail eligibility should require more than `COMPLETED`:\n\n```text\neligible =\n  canonical_status == COMPLETED\n  AND transaction_is_new\n  AND customer_is_contactable\n  AND message_type_is_approved\n  AND required_reward_fields_are_present\n```\n\nMigma's [events documentation](https://docs.migma.ai/events?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration) and [webhook documentation](https://docs.migma.ai/webhooks?utm_source=marketingwiki&utm_medium=referral&utm_campaign=loyalty-email-status-enum-migration) provide a useful downstream boundary: record the normalized customer event, then consume at-least-once delivery using a stable event identity. A provider schema change should not create a new logical reward event or reset idempotency.\n\n## Reconcile during deployment\n\nRun old and new mapper versions in shadow mode over the same captured payloads. Compare:\n\n- count by raw and canonical state;\n- count of transitions into `COMPLETED`;\n- count of email-eligible transactions;\n- duplicate and unknown counts;\n- total balance delta where the field is present;\n- messages drafted, queued, sent, and suppressed.\n\nAny unexplained difference blocks promotion. A zero-error parser can still be wrong if it silently drops the new value.\n\n## Rollback and stop conditions\n\nRollback means switching the decision path back to the previous mapper while retaining the new observer. Do not replay quarantined events automatically. Stop when an undocumented value appears, completed counts diverge, balance totals fail reconciliation, duplicates increase, or an email is eligible under only one mapper.\n\nAfter the compatibility window, keep a permanent unknown-value alert. Provider enums can change again.\n\n## Migration record\n\n```yaml\nprovider_change: \"brevo-loyalty-status-2026-09-03\"\nmapper_version: \"loyalty-state-v4\"\naccepted_aliases:\n  DRAFT: [\"pending\", \"draft\"]\n  COMPLETED: [\"complete\", \"completed\"]\nunknown_policy: \"quarantine_no_email\"\nshadow_start: \"2026-09-05T08:00:00Z\"\nalias_retirement_criteria: \"zero old values for approved window\"\nowner: \"named integration engineer\"\n```\n\n## Evidence limits\n\nMarketing Wiki did not call Brevo, inspect an SDK, or run a loyalty campaign. The changelog establishes the documented rename and balance field. Migma's event surfaces are vendor-documented. Validate current schemas, versioning, and consent rules in your own environment."}