{"id":"6a175b87cf76069458683ab7","title":"Lifecycle Scenarios","path":"how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/lifecycle-scenarios","contentMarkdown":"# 27-May-2026 — Lifecycle scenarios\n\n_____\n\nOnce a tenant has data, the next thing an end-to-end test usually needs is *change*: a member who left, a couple who joined, a handful whose details were edited. The **Scenarios** tab manufactures exactly that. It mutates the tenant's current dataset so that a follow-up `GET /api/groups/{code}/members?since=<baseline>` call returns a shaped `added` / `updated` / `removed` delta, with no hand-editing of Mongo and no wire-format change. Any conforming consumer reads the result through the ordinary public endpoint.\n\n[← Back to the overview](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock)\n\n## The mental model\n\nIncremental sync (covered in [Calling the API](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/calling-the-api)) hangs off a `since` timestamp: the consumer remembers when it last pulled and asks for everything changed after that. A scenario works backwards from the same idea:\n\n1. Your consumer does a first pull and records the moment it synced (the **baseline**).\n2. You open the **Scenarios** tab, set that baseline as `since`, and ask for *N removed, M added, K amended*.\n3. The mock soft-removes, inserts and edits members so that every change is stamped `updatedAt` strictly after the baseline (added members also get `ingestedAt` after it).\n4. Your consumer pulls again with `?since=<baseline>` and gets back precisely the delta you asked for, split into `added` / `updated` / `removed`.\n\nBecause each apply records the timestamp it produced (`nextSince`), you can chain runs: the `nextSince` of one scenario becomes the `since` of the next, so each consumer pull sees exactly the changes between two checkpoints.\n\n## Where it lives\n\nOpen a tenant in [`/admin`](https://salesforce-mock.ngx-ramblers.org.uk/admin) and pick the **Scenarios** tab. There are two panels: **Simulate a change** (the form) and **Recent scenarios** (the last 20 applies on this tenant).\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/cec9819c-24e5-478b-9768-d045bf8ba26b.png)\n\n*The Scenarios tab's \"Simulate a change\" form: set a since baseline (and an optional seed), then choose how many members to remove, add or amend, plus which fields each amendment should touch. Apply, and the next ?since= pull returns exactly that delta.*\n\n## The form\n\n### Baseline\n\n| Field | What it does |\n|---|---|\n| Since baseline | The `since` your consumer will pass on its next pull. Defaults to **Now**; switch to **Custom date/time** if you already have a recorded checkpoint from an earlier sync. Every changed member is stamped `updatedAt` strictly after this moment. |\n| Seed (optional) | Makes the run reproducible. The same seed against the same dataset picks the same members and produces the same field values. Leave blank for fresh randomness on each run. |\n\n### Removed\n\nSoft-removes N existing members: their `removed` flag is set (they are not hard-deleted), and they appear in the next delta as `changeType: removed`. You can tag them with a removal reason (`other` is the default, plus `expired`, `transferred`, and `deceased`), carried through as `removalReason` so a consumer can tell why someone dropped off.\n\n### Added\n\nInserts N brand-new synthetic members with fresh membership numbers, using the same generator as the **Generate** tab, so their consent and role values follow the generator's defaults. They surface in the next delta as `changeType: added`.\n\n### Amended\n\nEdits N existing members and stamps `updatedAt`, so they come back as `changeType: updated`. You must tick at least one field to mutate. This is where the detail matters, because \"amend\" does not behave the same way for every field:\n\n| Field(s) | How the amend behaves |\n|---|---|\n| `firstName`, `lastName`, `email`, `postcode`, `mobileNumber`, `landlineTelephone`, `membershipExpiryDate` | **Replaced** with a freshly generated synthetic value. The member keeps their membership number and identity; the chosen field just gets a new plausible value. |\n| `emailMarketingConsent`, `groupMarketingConsent` | **Toggled**: the existing boolean is flipped. Amending `emailMarketingConsent` also updates `emailPermissionLastUpdated`. This is deliberate, because a consent test usually wants the value to *change* relative to what the consumer last saw, rather than be set to a random one. |\n\nOne consequence is worth calling out: amending `membershipExpiryDate` sets a **future** date (one to three years out, exactly like the generator). It is good for testing that a changed expiry propagates through `?since=`, but it does **not** turn a member into a lapsed one. See the note on lapsed members at the end of this page.\n\n## How an apply is built\n\nThe order is fixed: **remove first, then amend from what remains, then add.** All three draw from the tenant's currently-active (non-removed) members, shuffled deterministically by the seed. If you ask for more than the dataset can supply (say 50 removals on a 30-member tenant), the mock applies as many as it can and returns a warning rather than failing.\n\n## What you get back\n\nThe apply returns a JSON summary, also shown inline in the UI:\n\n- `counts` — both what you **requested** and what was actually **applied**. These differ when a warning has fired.\n- `changes[]` — one entry per affected member: `membershipNumber`, `salesforceId`, `changeType`, and for amendments the list of `fields` that were touched.\n- `nextSince` — the timestamp this apply produced. Hand it to your consumer as the `since` for the next pull, or use the **Recent scenarios** panel's **Use as since** button to chain straight into another apply.\n- `seed` — the seed that was used (the one you supplied, or the random one the mock chose), so the run can be reproduced.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/f0daa209-5818-435c-9e3c-81d1574b806c.png)\n\n*A scenario apply returns (and shows inline) a JSON summary: requested vs applied counts, a per-member changes array listing the fields touched on each amendment, the nextSince to feed the next pull, and the seed used. This run was remove 1, amend 2 (firstName + email), add 3, with no warnings.*\n\n## Recent scenarios\n\nEvery apply is saved against the tenant. The **Recent scenarios** panel lists the latest 20 with who ran them, the seed, the removed / amended / added counts, and the `nextSince`. **Use as since** copies a row's `nextSince` into the baseline above, which is the quickest way to walk a consumer through several checkpoints in sequence.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/21f8b5fc-fbee-4825-ad7b-4246f18ee572.png)\n\n*The Recent scenarios panel: each prior apply on this tenant with its seed and removed / amended / added counts. Use as since copies that row's nextSince into the baseline above, so the next apply (and the consumer's next pull) sees exactly the deltas between the two checkpoints.*\n\n## Driving it from curl\n\nThe form posts to a per-tenant admin endpoint. It authenticates with your **operator session cookie**, not a Bearer token (Bearer tokens are for the public member API, not the admin surface):\n\n```sh\n# Apply: remove 2 (reason transferred), add 5, amend 3 (firstName + email), reproducibly\ncurl -sS -X POST \\\n  -H \"Cookie: <your admin session cookie>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"since\": \"2026-05-01T00:00:00Z\",\n        \"removed\": 2,\n        \"added\": 5,\n        \"amended\": 3,\n        \"amendFields\": [\"firstName\", \"email\"],\n        \"removalReason\": \"transferred\",\n        \"seed\": 42\n      }' \\\n  https://salesforce-mock.ngx-ramblers.org.uk/admin/api/tenants/KT50/scenarios/delta | jq .\n\n# Then read the delta back through the ordinary public endpoint:\ncurl -sS \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  \"https://salesforce-mock.ngx-ramblers.org.uk/api/groups/KT50/members?since=2026-05-01T00:00:00Z\" | jq .changes\n```\n\nRequest fields: `since` (ISO-8601, defaults to now), `removed` / `added` / `amended` (0 to 10,000, default 0), `amendFields` (required when `amended > 0`, at least one of the allow-list), `removalReason` (`expired` / `transferred` / `deceased` / `other`), and `seed` (optional integer). The amend allow-list is `firstName`, `lastName`, `email`, `postcode`, `mobileNumber`, `landlineTelephone`, `membershipExpiryDate`, `emailMarketingConsent`, `groupMarketingConsent`. Earlier applies on a tenant can be listed with `GET /admin/api/tenants/{code}/scenarios?limit=20`.\n\n## Lapsed members: a deliberate gap\n\nA natural question is \"how do I get a tenant with some lapsed members, to test `?includeExpired=true`?\" The answer is that **scenarios cannot manufacture those, and neither can the generator.** Both the **Generate** tab and the scenario `membershipExpiryDate` amend always set an expiry one to three years in the future, so every synthetic member is active. The scenario tool's `expired` removal reason is a different thing again: it soft-*removes* the member, so they leave the list entirely and show up as `removed` in the delta, rather than staying present with a past expiry date.\n\nThe only way to get members that are **present but lapsed** is to upload a real Insight Hub `ExportAll.xlsx` that already contains past expiry dates. So if you need to exercise the lapsed-member path, keep a real export with lapsed rows in it; if you want a clean, all-active dataset, regenerate from the **Generate** tab.\n\n## Next\n\n[**Release notes and versioning →**](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/release-notes-and-versioning)","contentHtml":"<h1>27-May-2026 — Lifecycle scenarios</h1>\n<hr>\n<p>Once a tenant has data, the next thing an end-to-end test usually needs is <em>change</em>: a member who left, a couple who joined, a handful whose details were edited. The <strong>Scenarios</strong> tab manufactures exactly that. It mutates the tenant&#39;s current dataset so that a follow-up <code>GET /api/groups/{code}/members?since=&lt;baseline&gt;</code> call returns a shaped <code>added</code> / <code>updated</code> / <code>removed</code> delta, with no hand-editing of Mongo and no wire-format change. Any conforming consumer reads the result through the ordinary public endpoint.</p>\n<p><a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock\">← Back to the overview</a></p>\n<h2>The mental model</h2>\n<p>Incremental sync (covered in <a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/calling-the-api\">Calling the API</a>) hangs off a <code>since</code> timestamp: the consumer remembers when it last pulled and asks for everything changed after that. A scenario works backwards from the same idea:</p>\n<ol>\n<li>Your consumer does a first pull and records the moment it synced (the <strong>baseline</strong>).</li>\n<li>You open the <strong>Scenarios</strong> tab, set that baseline as <code>since</code>, and ask for <em>N removed, M added, K amended</em>.</li>\n<li>The mock soft-removes, inserts and edits members so that every change is stamped <code>updatedAt</code> strictly after the baseline (added members also get <code>ingestedAt</code> after it).</li>\n<li>Your consumer pulls again with <code>?since=&lt;baseline&gt;</code> and gets back precisely the delta you asked for, split into <code>added</code> / <code>updated</code> / <code>removed</code>.</li>\n</ol>\n<p>Because each apply records the timestamp it produced (<code>nextSince</code>), you can chain runs: the <code>nextSince</code> of one scenario becomes the <code>since</code> of the next, so each consumer pull sees exactly the changes between two checkpoints.</p>\n<h2>Where it lives</h2>\n<p>Open a tenant in <a href=\"https://salesforce-mock.ngx-ramblers.org.uk/admin\"><code>/admin</code></a> and pick the <strong>Scenarios</strong> tab. There are two panels: <strong>Simulate a change</strong> (the form) and <strong>Recent scenarios</strong> (the last 20 applies on this tenant).</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/cec9819c-24e5-478b-9768-d045bf8ba26b.png\" alt=\"\"></p>\n<p><em>The Scenarios tab&#39;s &quot;Simulate a change&quot; form: set a since baseline (and an optional seed), then choose how many members to remove, add or amend, plus which fields each amendment should touch. Apply, and the next ?since= pull returns exactly that delta.</em></p>\n<h2>The form</h2>\n<h3>Baseline</h3>\n<table>\n<thead>\n<tr>\n<th>Field</th>\n<th>What it does</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Since baseline</td>\n<td>The <code>since</code> your consumer will pass on its next pull. Defaults to <strong>Now</strong>; switch to <strong>Custom date/time</strong> if you already have a recorded checkpoint from an earlier sync. Every changed member is stamped <code>updatedAt</code> strictly after this moment.</td>\n</tr>\n<tr>\n<td>Seed (optional)</td>\n<td>Makes the run reproducible. The same seed against the same dataset picks the same members and produces the same field values. Leave blank for fresh randomness on each run.</td>\n</tr>\n</tbody></table>\n<h3>Removed</h3>\n<p>Soft-removes N existing members: their <code>removed</code> flag is set (they are not hard-deleted), and they appear in the next delta as <code>changeType: removed</code>. You can tag them with a removal reason (<code>other</code> is the default, plus <code>expired</code>, <code>transferred</code>, and <code>deceased</code>), carried through as <code>removalReason</code> so a consumer can tell why someone dropped off.</p>\n<h3>Added</h3>\n<p>Inserts N brand-new synthetic members with fresh membership numbers, using the same generator as the <strong>Generate</strong> tab, so their consent and role values follow the generator&#39;s defaults. They surface in the next delta as <code>changeType: added</code>.</p>\n<h3>Amended</h3>\n<p>Edits N existing members and stamps <code>updatedAt</code>, so they come back as <code>changeType: updated</code>. You must tick at least one field to mutate. This is where the detail matters, because &quot;amend&quot; does not behave the same way for every field:</p>\n<table>\n<thead>\n<tr>\n<th>Field(s)</th>\n<th>How the amend behaves</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>firstName</code>, <code>lastName</code>, <code>email</code>, <code>postcode</code>, <code>mobileNumber</code>, <code>landlineTelephone</code>, <code>membershipExpiryDate</code></td>\n<td><strong>Replaced</strong> with a freshly generated synthetic value. The member keeps their membership number and identity; the chosen field just gets a new plausible value.</td>\n</tr>\n<tr>\n<td><code>emailMarketingConsent</code>, <code>groupMarketingConsent</code></td>\n<td><strong>Toggled</strong>: the existing boolean is flipped. Amending <code>emailMarketingConsent</code> also updates <code>emailPermissionLastUpdated</code>. This is deliberate, because a consent test usually wants the value to <em>change</em> relative to what the consumer last saw, rather than be set to a random one.</td>\n</tr>\n</tbody></table>\n<p>One consequence is worth calling out: amending <code>membershipExpiryDate</code> sets a <strong>future</strong> date (one to three years out, exactly like the generator). It is good for testing that a changed expiry propagates through <code>?since=</code>, but it does <strong>not</strong> turn a member into a lapsed one. See the note on lapsed members at the end of this page.</p>\n<h2>How an apply is built</h2>\n<p>The order is fixed: <strong>remove first, then amend from what remains, then add.</strong> All three draw from the tenant&#39;s currently-active (non-removed) members, shuffled deterministically by the seed. If you ask for more than the dataset can supply (say 50 removals on a 30-member tenant), the mock applies as many as it can and returns a warning rather than failing.</p>\n<h2>What you get back</h2>\n<p>The apply returns a JSON summary, also shown inline in the UI:</p>\n<ul>\n<li><code>counts</code> — both what you <strong>requested</strong> and what was actually <strong>applied</strong>. These differ when a warning has fired.</li>\n<li><code>changes[]</code> — one entry per affected member: <code>membershipNumber</code>, <code>salesforceId</code>, <code>changeType</code>, and for amendments the list of <code>fields</code> that were touched.</li>\n<li><code>nextSince</code> — the timestamp this apply produced. Hand it to your consumer as the <code>since</code> for the next pull, or use the <strong>Recent scenarios</strong> panel&#39;s <strong>Use as since</strong> button to chain straight into another apply.</li>\n<li><code>seed</code> — the seed that was used (the one you supplied, or the random one the mock chose), so the run can be reproduced.</li>\n</ul>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/f0daa209-5818-435c-9e3c-81d1574b806c.png\" alt=\"\"></p>\n<p><em>A scenario apply returns (and shows inline) a JSON summary: requested vs applied counts, a per-member changes array listing the fields touched on each amendment, the nextSince to feed the next pull, and the seed used. This run was remove 1, amend 2 (firstName + email), add 3, with no warnings.</em></p>\n<h2>Recent scenarios</h2>\n<p>Every apply is saved against the tenant. The <strong>Recent scenarios</strong> panel lists the latest 20 with who ran them, the seed, the removed / amended / added counts, and the <code>nextSince</code>. <strong>Use as since</strong> copies a row&#39;s <code>nextSince</code> into the baseline above, which is the quickest way to walk a consumer through several checkpoints in sequence.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/21f8b5fc-fbee-4825-ad7b-4246f18ee572.png\" alt=\"\"></p>\n<p><em>The Recent scenarios panel: each prior apply on this tenant with its seed and removed / amended / added counts. Use as since copies that row&#39;s nextSince into the baseline above, so the next apply (and the consumer&#39;s next pull) sees exactly the deltas between the two checkpoints.</em></p>\n<h2>Driving it from curl</h2>\n<p>The form posts to a per-tenant admin endpoint. It authenticates with your <strong>operator session cookie</strong>, not a Bearer token (Bearer tokens are for the public member API, not the admin surface):</p>\n<pre><code class=\"language-sh\"># Apply: remove 2 (reason transferred), add 5, amend 3 (firstName + email), reproducibly\ncurl -sS -X POST \\\n  -H &quot;Cookie: &lt;your admin session cookie&gt;&quot; \\\n  -H &quot;Content-Type: application/json&quot; \\\n  -d &#39;{\n        &quot;since&quot;: &quot;2026-05-01T00:00:00Z&quot;,\n        &quot;removed&quot;: 2,\n        &quot;added&quot;: 5,\n        &quot;amended&quot;: 3,\n        &quot;amendFields&quot;: [&quot;firstName&quot;, &quot;email&quot;],\n        &quot;removalReason&quot;: &quot;transferred&quot;,\n        &quot;seed&quot;: 42\n      }&#39; \\\n  https://salesforce-mock.ngx-ramblers.org.uk/admin/api/tenants/KT50/scenarios/delta | jq .\n\n# Then read the delta back through the ordinary public endpoint:\ncurl -sS \\\n  -H &quot;Authorization: Bearer $TOKEN&quot; \\\n  &quot;https://salesforce-mock.ngx-ramblers.org.uk/api/groups/KT50/members?since=2026-05-01T00:00:00Z&quot; | jq .changes\n</code></pre>\n<p>Request fields: <code>since</code> (ISO-8601, defaults to now), <code>removed</code> / <code>added</code> / <code>amended</code> (0 to 10,000, default 0), <code>amendFields</code> (required when <code>amended &gt; 0</code>, at least one of the allow-list), <code>removalReason</code> (<code>expired</code> / <code>transferred</code> / <code>deceased</code> / <code>other</code>), and <code>seed</code> (optional integer). The amend allow-list is <code>firstName</code>, <code>lastName</code>, <code>email</code>, <code>postcode</code>, <code>mobileNumber</code>, <code>landlineTelephone</code>, <code>membershipExpiryDate</code>, <code>emailMarketingConsent</code>, <code>groupMarketingConsent</code>. Earlier applies on a tenant can be listed with <code>GET /admin/api/tenants/{code}/scenarios?limit=20</code>.</p>\n<h2>Lapsed members: a deliberate gap</h2>\n<p>A natural question is &quot;how do I get a tenant with some lapsed members, to test <code>?includeExpired=true</code>?&quot; The answer is that <strong>scenarios cannot manufacture those, and neither can the generator.</strong> Both the <strong>Generate</strong> tab and the scenario <code>membershipExpiryDate</code> amend always set an expiry one to three years in the future, so every synthetic member is active. The scenario tool&#39;s <code>expired</code> removal reason is a different thing again: it soft-<em>removes</em> the member, so they leave the list entirely and show up as <code>removed</code> in the delta, rather than staying present with a past expiry date.</p>\n<p>The only way to get members that are <strong>present but lapsed</strong> is to upload a real Insight Hub <code>ExportAll.xlsx</code> that already contains past expiry dates. So if you need to exercise the lapsed-member path, keep a real export with lapsed rows in it; if you want a clean, all-active dataset, regenerate from the <strong>Generate</strong> tab.</p>\n<h2>Next</h2>\n<p><a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/release-notes-and-versioning\"><strong>Release notes and versioning →</strong></a></p>\n"}