{"id":"69ecef9a60bb8b34fce600a5","title":"Loading Member Data","path":"how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/loading-member-data","contentMarkdown":"# 21-Apr-2026 — Loading member data\n\n_____\n\nTwo ways to populate a tenant: upload a real Insight Hub `ExportAll.xlsx`, or generate synthetic rows from a configurable template. Either replaces the tenant's current dataset — existing members not present in the new batch are soft-removed (their `removed` flag is set so consumers polling with `?since=` can see the removal).\n\n[← Back to the overview](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock)\n\n## Members tab layout\n\nOpen a tenant and stay on the **Members** tab. The top panel shows the first 20 members currently in the dataset. Below it are two sub-tabs:\n\n- **Upload Insight Hub xlsx** — push a real `ExportAll.xlsx` in\n- **Insight Hub export** — pull the current dataset out as a 36-column xlsx\n\nSynthetic generation lives on its own **Generate** tab, covered further down.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/7fec8394-2f61-4157-925b-4aae3160e577.png)\n\n*The Members tab shows the first 20 members. Below it, two sub-tabs — Upload Insight Hub xlsx (push data in) and Insight Hub export (pull data out).*\n\n## Upload Insight Hub xlsx\n\nGet an `ExportAll.xlsx` from Insight Hub's bulk download. The mock expects the standard 36-column schema documented in the [#209 field table](https://github.com/nbarrett/ngx-ramblers/issues/209).\n\n- Click **Choose File**, pick your `.xlsx`\n- Hit **Upload**\n\nWhile the file streams, a busy indicator runs and every action button is disabled. When the upload finishes, the page refreshes the Members preview with the first 20 rows of the newly loaded dataset and the tenant header updates to show the new last-import time and row count. There is no JSON summary surfaced in the UI; if you want the parse-and-ingest detail you can hit `POST /admin/api/tenants/:code/upload` directly with curl and inspect the response.\n\nThe detail response (curl path) returns four fields worth knowing about:\n\n| Field | Meaning |\n|---|---|\n| `unmatchedHeaders` | Columns in your file that didn't match any of the 36 expected names. Typos and extras land here. Rename and re-upload if anything important is listed. |\n| `missingHeaders` | Expected columns absent from your file. Not fatal — they come through as undefined on member records. |\n| `warnings` | Rows skipped because they had no `Mem No.` or no `Last Name`. |\n| `softRemoved` | Members that existed before the upload but are absent from this one — flagged with `removed: true`, not hard-deleted, so incremental-sync consumers can see the removal. |\n\nHeader text is matched case-insensitively with whitespace collapsed; the spelling has to be right. The parser reads the first worksheet of the workbook (the writer side, used by export and download, names the worksheet `Full List` to match what consumers like ngx-ramblers' `member-bulk-load.ts` look up by name).\n\n## Insight Hub export — download the current dataset\n\nThe second sub-tab on the Members tab. Click **Download Insight Hub xlsx** and the mock streams the tenant's current dataset back as `InsightHub-<CODE>-<n>.xlsx`, in exactly the 36-column shape Insight Hub itself produces.\n\nUseful for:\n\n- Round-tripping data through a third-party tool that reads `ExportAll.xlsx`\n- Handing a tenant's snapshot to a consumer who hasn't wired up the public API yet\n- Comparing two operators' datasets side by side without API access\n\nNote: granular consent flags (`groupMarketingConsent`, `areaMarketingConsent`, `otherMarketingConsent`) and role assignments are stored on the member record but **are not part of the 36-column Insight Hub schema**, so they don't appear in the downloaded xlsx. They are preserved on the database side and continue to be served by the public API.\n\n## Generate synthetic data\n\nIf you don't have a real spreadsheet — or you want a controlled, reproducible scenario — open the **Generate** tab. The form has six sections.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/ccb62af1-5536-4982-ac48-91e2d1dfc7b2.png)\n\n*The full Generate form — size, geography, email pattern, consent distribution (independent or joint), role proportions, and the two submit modes (import directly into the database, or download as Insight Hub xlsx).*\n\n### Size and reproducibility\n\n- **Row count** — 1 to 10,000. Start with a few hundred for quick iteration; 10,000 is enough to stress-test paging.\n- **Seed** (optional) — same seed plus same options gives the same output every time. Leave blank for fresh randomness on each run.\n\n### Geography\n\nPool of towns and postcode prefixes that addresses are drawn from.\n\n| Region | Towns | Postcode areas |\n|---|---|---|\n| Mixed UK (default) | every region's pool combined | every region's pool combined |\n| Kent | Canterbury, Faversham, Maidstone, Tunbridge Wells, … | CT, ME, TN, BR, DA |\n| Staffordshire | Stoke-on-Trent, Newcastle-under-Lyme, Stafford, Lichfield, … | ST, DE, WS |\n| Newcastle upon Tyne and the North East | Newcastle upon Tyne, Gateshead, Sunderland, Durham, … | NE, SR, DH |\n| Hampshire | Winchester, Southampton, Portsmouth, Basingstoke, … | SO, PO, RG, GU, SP |\n\nPicking a single region is useful when you want a tenant whose addresses look believable for a specific group's catchment.\n\n### Email pattern\n\nFour presets, plus custom. Available placeholders: `{firstname}`, `{surname}`, `{nn}` (a small integer disambiguator), `{membershipNumber}`, `{groupCode}`, `{domain}`, `{base}`.\n\n| Preset | Template | Use when |\n|---|---|---|\n| Distinct | `{firstname}.{surname}{nn}@{domain}` | You want addresses that look like real members. The domain needs a catch-all to actually receive mail. |\n| Plus-addressing | `{base}+m{membershipNumber}@{domain}` | You want every test member's mail to land in one shared inbox (e.g. `test+m3300001@…`). |\n| Minimal | `member{membershipNumber}@{domain}` | You want fully synthetic, obviously-fake addresses (no real names involved). |\n| Custom | your own template | You need a different shape — e.g. `{firstname}-{surname}@{domain}`. |\n\nThe default domain is `ngx-ramblers.org.uk`. It's chosen because it's an unreachable address from real Brevo lists — which means accidentally pushing synthetic data into a real outbound campaign won't deliver to anyone.\n\n### Consent distribution\n\nTwo modes for the six consent flags:\n\n**Independent** — each flag is rolled separately against its own probability. Two tables of sliders, grouped by where each flag rides:\n\n- **xlsx + API** — the standard Insight Hub flags: `emailMarketingConsent`, `postDirectMarketing`, `telephoneDirectMarketing`. These round-trip through both the public API and the Insight Hub xlsx download.\n- **API only** — granular flags: `groupMarketingConsent`, `areaMarketingConsent`, `otherMarketingConsent`. Stored on the member record and returned by the public API; not in the 36-column Insight Hub schema, so absent from the downloaded xlsx.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/877fd26d-7576-4e5f-b8c8-57db03d85edb.png)\n\n*Switching to Joint mode reveals a probability table over flag combinations. Add rows, set weights, and the running sum tells you when you've reached 100.*\n\n**Joint** — a probability table over flag combinations, weights summing to 100. Useful for correlated scenarios that independent rolls can't express (\"give me a population where members who consent to group email always also consent to area email\"). Add rows with **+ Add row**; the running sum shows live so you can see when you've reached 100.\n\n### Role proportions (API only)\n\nThree flags set on each member's primary `groupMemberships[].roles`: `walkLeader`, `emailSender`, `viewMembershipData`. Defaults are 8% / 5% / 3%. Like the granular consent flags, roles ride only through the public API — they are not part of the Insight Hub schema, so they don't appear in the downloaded xlsx.\n\n### Two ways to take the data away\n\nThe form has two submit buttons:\n\n- **Generate & import** — generate the rows *and* save them into this server's database. Replaces the tenant's current dataset; the public API will now return them. Granular consent and roles survive into the DB because there's no xlsx round-trip on this path.\n- **Generate & download xlsx** — generate the rows *and* stream them as a 36-column Insight Hub xlsx (`ExportAll-<tenant>-<count>.xlsx`) without touching the database. Useful when a downstream consumer reads `ExportAll.xlsx` directly and you just want a synthetic file to feed them.\n\n### Surnames are double-barrelled on collision\n\nA subtlety worth knowing: synthetic data is generated against a fixed pool of first names and surnames. If your row count is high enough that the same `(firstName, lastName)` pair would otherwise repeat, the generator double-barrels the surname (`Smith` → `Smith-Adams`) until uniqueness is restored. This matters because consumers like ngx-ramblers enforce a unique `(lastName, firstName, nameAlias)` index on import; without the double-barrelling, large synthetic batches would be rejected mid-load.\n\n## Next\n\n[**Calling the API →**](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/calling-the-api)","contentHtml":"<h1>21-Apr-2026 — Loading member data</h1>\n<hr>\n<p>Two ways to populate a tenant: upload a real Insight Hub <code>ExportAll.xlsx</code>, or generate synthetic rows from a configurable template. Either replaces the tenant&#39;s current dataset — existing members not present in the new batch are soft-removed (their <code>removed</code> flag is set so consumers polling with <code>?since=</code> can see the removal).</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>Members tab layout</h2>\n<p>Open a tenant and stay on the <strong>Members</strong> tab. The top panel shows the first 20 members currently in the dataset. Below it are two sub-tabs:</p>\n<ul>\n<li><strong>Upload Insight Hub xlsx</strong> — push a real <code>ExportAll.xlsx</code> in</li>\n<li><strong>Insight Hub export</strong> — pull the current dataset out as a 36-column xlsx</li>\n</ul>\n<p>Synthetic generation lives on its own <strong>Generate</strong> tab, covered further down.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/7fec8394-2f61-4157-925b-4aae3160e577.png\" alt=\"\"></p>\n<p><em>The Members tab shows the first 20 members. Below it, two sub-tabs — Upload Insight Hub xlsx (push data in) and Insight Hub export (pull data out).</em></p>\n<h2>Upload Insight Hub xlsx</h2>\n<p>Get an <code>ExportAll.xlsx</code> from Insight Hub&#39;s bulk download. The mock expects the standard 36-column schema documented in the <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/209\">#209 field table</a>.</p>\n<ul>\n<li>Click <strong>Choose File</strong>, pick your <code>.xlsx</code></li>\n<li>Hit <strong>Upload</strong></li>\n</ul>\n<p>While the file streams, a busy indicator runs and every action button is disabled. When the upload finishes, the page refreshes the Members preview with the first 20 rows of the newly loaded dataset and the tenant header updates to show the new last-import time and row count. There is no JSON summary surfaced in the UI; if you want the parse-and-ingest detail you can hit <code>POST /admin/api/tenants/:code/upload</code> directly with curl and inspect the response.</p>\n<p>The detail response (curl path) returns four fields worth knowing about:</p>\n<table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>unmatchedHeaders</code></td>\n<td>Columns in your file that didn&#39;t match any of the 36 expected names. Typos and extras land here. Rename and re-upload if anything important is listed.</td>\n</tr>\n<tr>\n<td><code>missingHeaders</code></td>\n<td>Expected columns absent from your file. Not fatal — they come through as undefined on member records.</td>\n</tr>\n<tr>\n<td><code>warnings</code></td>\n<td>Rows skipped because they had no <code>Mem No.</code> or no <code>Last Name</code>.</td>\n</tr>\n<tr>\n<td><code>softRemoved</code></td>\n<td>Members that existed before the upload but are absent from this one — flagged with <code>removed: true</code>, not hard-deleted, so incremental-sync consumers can see the removal.</td>\n</tr>\n</tbody></table>\n<p>Header text is matched case-insensitively with whitespace collapsed; the spelling has to be right. The parser reads the first worksheet of the workbook (the writer side, used by export and download, names the worksheet <code>Full List</code> to match what consumers like ngx-ramblers&#39; <code>member-bulk-load.ts</code> look up by name).</p>\n<h2>Insight Hub export — download the current dataset</h2>\n<p>The second sub-tab on the Members tab. Click <strong>Download Insight Hub xlsx</strong> and the mock streams the tenant&#39;s current dataset back as <code>InsightHub-&lt;CODE&gt;-&lt;n&gt;.xlsx</code>, in exactly the 36-column shape Insight Hub itself produces.</p>\n<p>Useful for:</p>\n<ul>\n<li>Round-tripping data through a third-party tool that reads <code>ExportAll.xlsx</code></li>\n<li>Handing a tenant&#39;s snapshot to a consumer who hasn&#39;t wired up the public API yet</li>\n<li>Comparing two operators&#39; datasets side by side without API access</li>\n</ul>\n<p>Note: granular consent flags (<code>groupMarketingConsent</code>, <code>areaMarketingConsent</code>, <code>otherMarketingConsent</code>) and role assignments are stored on the member record but <strong>are not part of the 36-column Insight Hub schema</strong>, so they don&#39;t appear in the downloaded xlsx. They are preserved on the database side and continue to be served by the public API.</p>\n<h2>Generate synthetic data</h2>\n<p>If you don&#39;t have a real spreadsheet — or you want a controlled, reproducible scenario — open the <strong>Generate</strong> tab. The form has six sections.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/ccb62af1-5536-4982-ac48-91e2d1dfc7b2.png\" alt=\"\"></p>\n<p><em>The full Generate form — size, geography, email pattern, consent distribution (independent or joint), role proportions, and the two submit modes (import directly into the database, or download as Insight Hub xlsx).</em></p>\n<h3>Size and reproducibility</h3>\n<ul>\n<li><strong>Row count</strong> — 1 to 10,000. Start with a few hundred for quick iteration; 10,000 is enough to stress-test paging.</li>\n<li><strong>Seed</strong> (optional) — same seed plus same options gives the same output every time. Leave blank for fresh randomness on each run.</li>\n</ul>\n<h3>Geography</h3>\n<p>Pool of towns and postcode prefixes that addresses are drawn from.</p>\n<table>\n<thead>\n<tr>\n<th>Region</th>\n<th>Towns</th>\n<th>Postcode areas</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Mixed UK (default)</td>\n<td>every region&#39;s pool combined</td>\n<td>every region&#39;s pool combined</td>\n</tr>\n<tr>\n<td>Kent</td>\n<td>Canterbury, Faversham, Maidstone, Tunbridge Wells, …</td>\n<td>CT, ME, TN, BR, DA</td>\n</tr>\n<tr>\n<td>Staffordshire</td>\n<td>Stoke-on-Trent, Newcastle-under-Lyme, Stafford, Lichfield, …</td>\n<td>ST, DE, WS</td>\n</tr>\n<tr>\n<td>Newcastle upon Tyne and the North East</td>\n<td>Newcastle upon Tyne, Gateshead, Sunderland, Durham, …</td>\n<td>NE, SR, DH</td>\n</tr>\n<tr>\n<td>Hampshire</td>\n<td>Winchester, Southampton, Portsmouth, Basingstoke, …</td>\n<td>SO, PO, RG, GU, SP</td>\n</tr>\n</tbody></table>\n<p>Picking a single region is useful when you want a tenant whose addresses look believable for a specific group&#39;s catchment.</p>\n<h3>Email pattern</h3>\n<p>Four presets, plus custom. Available placeholders: <code>{firstname}</code>, <code>{surname}</code>, <code>{nn}</code> (a small integer disambiguator), <code>{membershipNumber}</code>, <code>{groupCode}</code>, <code>{domain}</code>, <code>{base}</code>.</p>\n<table>\n<thead>\n<tr>\n<th>Preset</th>\n<th>Template</th>\n<th>Use when</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>Distinct</td>\n<td><code>{firstname}.{surname}{nn}@{domain}</code></td>\n<td>You want addresses that look like real members. The domain needs a catch-all to actually receive mail.</td>\n</tr>\n<tr>\n<td>Plus-addressing</td>\n<td><code>{base}+m{membershipNumber}@{domain}</code></td>\n<td>You want every test member&#39;s mail to land in one shared inbox (e.g. <code>test+m3300001@…</code>).</td>\n</tr>\n<tr>\n<td>Minimal</td>\n<td><code>member{membershipNumber}@{domain}</code></td>\n<td>You want fully synthetic, obviously-fake addresses (no real names involved).</td>\n</tr>\n<tr>\n<td>Custom</td>\n<td>your own template</td>\n<td>You need a different shape — e.g. <code>{firstname}-{surname}@{domain}</code>.</td>\n</tr>\n</tbody></table>\n<p>The default domain is <code>ngx-ramblers.org.uk</code>. It&#39;s chosen because it&#39;s an unreachable address from real Brevo lists — which means accidentally pushing synthetic data into a real outbound campaign won&#39;t deliver to anyone.</p>\n<h3>Consent distribution</h3>\n<p>Two modes for the six consent flags:</p>\n<p><strong>Independent</strong> — each flag is rolled separately against its own probability. Two tables of sliders, grouped by where each flag rides:</p>\n<ul>\n<li><strong>xlsx + API</strong> — the standard Insight Hub flags: <code>emailMarketingConsent</code>, <code>postDirectMarketing</code>, <code>telephoneDirectMarketing</code>. These round-trip through both the public API and the Insight Hub xlsx download.</li>\n<li><strong>API only</strong> — granular flags: <code>groupMarketingConsent</code>, <code>areaMarketingConsent</code>, <code>otherMarketingConsent</code>. Stored on the member record and returned by the public API; not in the 36-column Insight Hub schema, so absent from the downloaded xlsx.</li>\n</ul>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/877fd26d-7576-4e5f-b8c8-57db03d85edb.png\" alt=\"\"></p>\n<p><em>Switching to Joint mode reveals a probability table over flag combinations. Add rows, set weights, and the running sum tells you when you&#39;ve reached 100.</em></p>\n<p><strong>Joint</strong> — a probability table over flag combinations, weights summing to 100. Useful for correlated scenarios that independent rolls can&#39;t express (&quot;give me a population where members who consent to group email always also consent to area email&quot;). Add rows with <strong>+ Add row</strong>; the running sum shows live so you can see when you&#39;ve reached 100.</p>\n<h3>Role proportions (API only)</h3>\n<p>Three flags set on each member&#39;s primary <code>groupMemberships[].roles</code>: <code>walkLeader</code>, <code>emailSender</code>, <code>viewMembershipData</code>. Defaults are 8% / 5% / 3%. Like the granular consent flags, roles ride only through the public API — they are not part of the Insight Hub schema, so they don&#39;t appear in the downloaded xlsx.</p>\n<h3>Two ways to take the data away</h3>\n<p>The form has two submit buttons:</p>\n<ul>\n<li><strong>Generate &amp; import</strong> — generate the rows <em>and</em> save them into this server&#39;s database. Replaces the tenant&#39;s current dataset; the public API will now return them. Granular consent and roles survive into the DB because there&#39;s no xlsx round-trip on this path.</li>\n<li><strong>Generate &amp; download xlsx</strong> — generate the rows <em>and</em> stream them as a 36-column Insight Hub xlsx (<code>ExportAll-&lt;tenant&gt;-&lt;count&gt;.xlsx</code>) without touching the database. Useful when a downstream consumer reads <code>ExportAll.xlsx</code> directly and you just want a synthetic file to feed them.</li>\n</ul>\n<h3>Surnames are double-barrelled on collision</h3>\n<p>A subtlety worth knowing: synthetic data is generated against a fixed pool of first names and surnames. If your row count is high enough that the same <code>(firstName, lastName)</code> pair would otherwise repeat, the generator double-barrels the surname (<code>Smith</code> → <code>Smith-Adams</code>) until uniqueness is restored. This matters because consumers like ngx-ramblers enforce a unique <code>(lastName, firstName, nameAlias)</code> index on import; without the double-barrelling, large synthetic batches would be rejected mid-load.</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/calling-the-api\"><strong>Calling the API →</strong></a></p>\n"}