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) — loyalty programs are permitted; what you can call them in customer-facing copy is regulated. Auto-generated emails / SMS need to clear the rules. Ledger structure doesn’t change this; the customer-facing surfacing does.
- Industry discount + loyalty interaction (WAC 314-55-095) — industry-discounted purchases may or may not earn loyalty points depending on operator policy. Document the rule in code, not just the SOP. Audit-log every variance from the 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.