# Loyalty-programma — uitwerking en besluit

Doel: klanten sparen punten op bestellingen en wisselen die in voor korting of een
tegoedbon. Hoe meer iemand uitgeeft, hoe hoger de tier en hoe groter het voordeel.
Alles wat de shop owner moet kunnen aanpassen (drempels, spaarfactor, waarde per
punt, vervaldatum) staat in WP-admin.

## Wat er al staat

| Onderdeel | Status |
| --- | --- |
| Punten sparen per bestelling | Werkt. `earnPointsForSpend()` past de spaarfactor en de tier-multiplier toe zodra een betaling definitief geslaagd is. |
| Tiers | Werkt, stuurt de multiplier en gratis verzending aan. |
| Inwisselen in de checkout | Werkt: reserveren → verbruiken bij slagen, terugstorten bij mislukken/annuleren (idempotent via `loyalty_events`). |
| Punten → coupon | Werkt: er wordt een eenmalige WooCommerce-coupon aangemaakt en na gebruik/annulering weer verwijderd. |
| Saldo, tier, voortgang, historiek | Werkt; de geschiedenis komt uit de klant-meta, het dashboard leest `config` uit de server. |
| Terugdraaien bij annulatie/terugbetaling | Werkt via `releasePoints()`. |
| Bonussen (registratie, review, verwijzing, verjaardag) | Werkt en is instelbaar. |
| Drempels, spaarfactor, waarde per punt, vervaldatum instelbaar | Instelbaar in WP-admin (`Peptidewinkel → Loyalty`). De vervaldatum is *nog niet gehandhaafd* — zie hieronder. |
| Handmatig bij-/afboeken | Nog niet. Zie hieronder voor de voorgestelde route. |

## Besluit: geen loyalty-plugin, wel een eigen grootboek

Drie opties zijn afgewogen:

1. **Een WooCommerce loyalty-plugin** (YITH Points, Points & Rewards, …).
   Snel, maar de regels staan dan in een plugin-UI los van de storefront, de
   tiers/voortgang zijn niet zonder plugin-updates aan te passen, en de headless
   frontend moet opnieuw op plugin-specifieke meta bouwen. De ervaring met
   SliceWP leert dat dit prima werkt zolang het standaardgedrag volstaat.

2. **SaaS (LoyaltyLion/Smile.io).** Krachtig, maar kosten per klant, en
   klantgegevens gaan naar een derde partij. Botst met "WooCommerce blijft de
   bron van waarheid".

3. **Eigen punten-grootboek (gekozen).** De regels en de berekeningen staan in de
   eigen code, de punten staan in WooCommerce-klantmeta, en `loyalty_events` in de
   Laravel-database is het grootboek.

Waarom 3 het beste past:

* WooCommerce blijft de bron van waarheid voor klanten en orders; de storefront
  leest de puntwaarden uit de klantmeta die er al staat.
* Alle regels zijn al instelbaar in WP-admin en de storefront volgt ze direct —
  geen plugin-update nodig.
* Idempotentie zit al in `loyalty_events` (uniek per klant/type/sleutel), precies
  wat je wilt bij webhooks, retries en refunds.
* Het grootboek bevat alleen `customer_id` (WooCommerce-id), punten, order-id en
  metadata — technische records, geen klantgegevens. Dat is de lijn uit het
  datamodel: WooCommerce is de system of record.

## Nog te bouwen (in deze volgorde)

**1. Vervaldatum handhaven.** `expiry_days` is instelbaar maar doet nog niets.
Voorstel: FIFO — de oudste punten verlopen eerst. Dat kan volledig uit het
grootboek worden berekend (alle `loyalty_events` met een puntwaarde, plus de
redemptions), dus zonder extra boekhouding in WooCommerce-meta. Een scheduled
command (`loyalty:expire-points`, dagelijks om 03:30 naast de bestaande
opruimtaken) schrijft een `expiry`-event en verlaagt het saldo. Bij `expiry_days = 0`
gebeurt er niets, dus de functie is veilig te activeren.

**2. Tier op bestedingsbedrag in plaats van op punten.** De wens is "hoger tier op
basis van totaal bestedingsbedrag". Nu rekent de tier met levenslange punten; bij
een spaarfactor anders dan 1 lopen die uit elkaar. Voorstel: een tweede meta-veld
`loyalty_spend_lifetime` bijhouden (elke bestelling, ook als punten verlopen) en de
tier daarop baseren. De config krijgt dan `min_spend` naast `min_points`
(`min_points` blijft voor achterwaartse compatibiliteit met bestaande klanten).

**3. Handmatig bij-/afboeken.** Voorstel: `POST /api/internal/loyalty/adjust`
(backend, achter de bestaande interne sleutel) met `customer_id`, `points`, `note`.
Dat schrijft een `manual`-event in het grootboek en werkt de klantmeta bij. De
companion-plugin krijgt op het WP-gebruikersprofiel een klein formulier met
"+/- punten" en een notitie, dat die endpoint aanroept — zo blijft het bijboeken
dicht bij de klant waar de medewerker toch al kijkt.

**4. Puntenhistoriek uit het grootboek.** De historiek staat nu als JSON in de
klantmeta (max. 100 regels). Het grootboek is compleet; de historiek kan daaruit
worden gelezen, met de meta alleen als cache. Dat maakt "waarom heb ik deze
punten?"-vragen exact te beantwoorden.

**5. E-mails.** Punten bijgeschreven / bijna een tier / punten verlopen: een
geplande mail (WP-cron of de Laravel-scheduler) op basis van dezelfde events.

## Instellingen (WP-admin → Peptidewinkel → Loyalty)

```json
{
  "points_per_euro": 1,
  "points_to_euro": 0.01,
  "min_redemption": 500,
  "expiry_days": 0,
  "bonuses": { "registration": 50, "review": 25, "referral_referrer": 200, "referral_new": 100, "birthday": 100 },
  "tiers": [
    { "name": "Bronze",  "min_points": 0,    "multiplier": 1,    "free_shipping": false },
    { "name": "Silver",  "min_points": 1000, "multiplier": 1.25, "free_shipping": false },
    { "name": "Gold",    "min_points": 5000, "multiplier": 1.5,  "free_shipping": true }
  ]
}
```

`multiplier` = extra punten per euro in die tier (spaarfactor × multiplier).
`expiry_days` = 0 betekent: punten verlopen niet.
