Customer-data deep-dive
Cannabis loyalty data — without losing a single point
Cannabis customers don’t check most of the data the operator obsesses over. They notice loyalty. Points balance off by 50, tier-history vanished, sign-up-date showing yesterday on a 4-year customer — they hit the budtender with it before the till opens. The fix is structural, not a one-time migration heroic. Append-only ledger, every earn + redeem + adjust as a row, never a counter.
Why loyalty data fails on migrations
Most cannabis-POS loyalty implementations store points as a single counter on the customer row. When you migrate, you SELECT the counter, INSERT into the new system, and trust the math. That works exactly until: (a) one POS computes points slightly differently than the other, (b) tier-history isn’t exported, (c) the sign-up-date field is named differently, or (d) a sync hiccup mid-migration leaves rows inconsistent. Then the customer notices. Then the budtender apologizes. Then operator-side trust erodes one customer at a time.
- Counter-only storage — single integer on the customer row. No history. Reconstruction impossible once a discrepancy is reported.
- Tier-history flat-discarded — Gold tier earned in 2022 turns into Silver after migration because nobody exported the tier-history table.
- Sign-up date overwritten — new POS uses createdAt for the customer record itself, not the original loyalty-enrollment date. 4-year customers look like new signups.
- Mid-flight sync breaks — POS-A is dual-running with POS-B for a week, points earn in both, neither knows about the other. Reconciliation is manual.
The append-only ledger pattern
Instead of a counter, store every loyalty event as a row: earn / redeem / adjust / migrate-in. The customer’s current balance is computed from the ledger, not stored on the customer row. Reconstruction is always possible because the history is the source of truth.
| Event type | What it captures | When it fires |
|---|---|---|
| earn | +points, transaction id, multiplier (1x / 2x / promo) | Every transaction with loyalty-eligible customer |
| redeem | -points, transaction id, redemption tier | Customer redeems at counter |
| adjust | +/- points, manager id, reason text | Manager-PIN-gated correction |
| migrate-in | +points, source-system label, migration batch id | One-time on cutover from prior POS |
| expire | -points, expiry-policy citation | If/when expiry policy fires |
Migrating from an existing POS
The trick to a clean migration is treating the legacy points balance as a single migrate-in row, not a counter copied verbatim. That makes the cutover reversible (we can re-run the migrate-in if it fails) and auditable (every customer’s ledger has a clear pre-migration boundary).
- Export legacy balances as CSV with customer_email + current_points + tier + signup_date + last_earn_at. One row per customer.
- Validate the export — total points across all customers should match the legacy POS’s aggregate report. If not, find the discrepancy before cutover.
- Insert one migrate-in ledger row per customer — points = legacy balance, source_system = 'dutchie' (or whatever), migration_batch_id = ISO date. The customer’s computed balance now matches.
- Migrate tier-history as separate tier_history table with achieved_at timestamps preserved. Customer tier is derived from history; sign-up date stays original.
- Run a reconciliation report the day after cutover — total points should match within ±5 per customer (rounding tolerance). Anything beyond requires investigation.
- Hold the legacy export read-only for 90 days post-cutover. If a customer disputes a balance, the legacy export is the source of truth for the pre-migration value. After 90 days the dispute window closes.
What customers notice — and what they don’t
- Points balance discrepancy — they always notice. Most-frequent dispute on any migration. Solved by accurate migrate-in + reconciliation.
- Tier loss — they notice if tier was earned + carried perks. Solved by migrating tier_history table separately.
- Sign-up date — they notice on anniversary milestones (5-year, 10-year). Solved by preserving original signup_date as a separate field.
- Last-purchase date — they don’t notice this directly, but the operator-side reorder math depends on it. Migrate as a side-table.
- Earn rate / multiplier history — they don’t notice unless the new POS computes earn differently. If the new system is 1pt/$1 and the legacy was 2pt/$1, that’s a re-anchor moment requiring customer communication, not silent migration.
Cannabis-specific quirks
- WSLCB advertising rules (WAC 314-55-155) — the section carries no loyalty-programme provision and no naming rule. What binds the copy is (2)(a), the content rules that apply to ALL advertising by a licensee: nothing false or misleading, no appeal to under-21s, no curative or therapeutic claim. Auto-generated emails / SMS clear the same bar. Ledger structure doesn’t change this; the customer-facing surfacing does.
- Industry discount + loyalty interaction — industry-discounted purchases may or may not earn loyalty points depending on operator policy. No state rule decides this for you, which is exactly why it has to be written into code and not just the SOP. Audit-log every variance from your own rule.
- Heroes / first-visit / birthday discounts — these stack with loyalty in customer-readable ways. Make sure the ledger captures the discount line-item separately so the customer’s effective earn rate is reconstructable.
- Federal banking + cash-only operations — points-redemption-as-store-credit is a tax-accounting nuance (280E-adjacent). The ledger documenting the redemption is what your CPA needs at year-end; if the redemption was made in cash, capture both the points-out and the cash-equivalent value.
Takeaways
- Counter-only loyalty storage breaks under migration + sync hiccups + tier-history loss. Ledger pattern survives all three.
- Every loyalty event = a row: earn / redeem / adjust / migrate-in / expire. Customer balance is computed from the ledger, never stored on the customer row.
- Migration trick: treat legacy balance as ONE migrate-in row with source_system + migration_batch_id. Reversible + auditable.
- Because the ledger is the source of truth, a balance is always reconstructable — including across a POS migration.
- Hold legacy export read-only for 90 days post-cutover — disputes resolve against it.
Frequently asked
- When we switch POS systems, why do customers keep telling budtenders their points balance is wrong?
- Most cannabis-POS loyalty setups store points as a single counter on the customer row, so on migration you copy that number and trust the math. It breaks when one POS computes points differently, tier-history isn't exported, the sign-up-date field is named differently, or a mid-migration sync leaves rows inconsistent. Once there's no history, reconstruction is impossible after a discrepancy gets reported, and the customer notices before the till opens.
- How do I move an old points balance over without copying it wrong?
- Treat the legacy balance as a single migrate-in ledger row rather than a counter copied verbatim, with points set to the legacy balance plus a source_system label and a migration_batch_id. That makes the cutover reversible, since you can re-run the migrate-in if it fails, and auditable, since every customer's ledger has a clear pre-migration boundary. Validate the export first so total points match the legacy POS's aggregate report, then run a reconciliation report the day after cutover expecting a match within plus or minus 5 points per customer.
- A customer disputes their pre-migration balance weeks later - how do I resolve it?
- Hold the legacy export read-only for 90 days after cutover, and treat it as the source of truth for any pre-migration value in dispute. Because current balance is computed from the append-only ledger rather than stored on the customer row, you can run the SUM, find the discrepancy if one exists, and either confirm the number or credit it with a documented reason. That turns the old 5-minute apology-and-manager-credit conversation into about a 30-second answer.
Related guides
Operational deep-dive
Cannabis loyalty program — earn rates that don’t bankrupt the margin
7 min read
Operations deep-dive
Cannabis customer SMS — what carriers actually filter, what gets you blocked
6 min read
Compliance playbook
Industry / heroes / medical discounts — verification cadence + audit-trail
5 min read
Ready to talk through your migration?
30-minute demo. We end by quoting the cutover from your current setup — fixed scope, no hourly games.