Files
MijnBesluit/docs/Adverteren.md
T
2026-09-27 14:21:31 +00:00

257 lines
13 KiB
Markdown

# Adverteren op MijnBesluit
Een balk op het overzichtsscherm met een korte tekst en een klein logo. Adverteerders boeken
zelf een periode en een aantal vertoningen per uur, betalen via bunq, en na goedkeuring door de
beheerder gaat de campagne draaien.
Dit stuk beschrijft hoe dat werkt en waarom. Lees het voordat je eraan begint te bouwen.
## Waar de advertentie wél en niet komt
**Wel:** op het overzichtsscherm, op de website en in de app. Een brede tegel met een lichte
achtergrond, bóven de vier tegels maar **onder "wat er speelt"** — die regel waarschuwt je dat er
een overlijden op jouw naam is gemeld of dat er iets voor je klaarstaat, en die gaat altijd voor.
**Niet:** in de kluis, bij momenten, bij de nalatenschap, en nergens in de schermen rond een
overlijdensmelding. Iemand die een bericht aan zijn kinderen klaarzet of net heeft gehoord dat
er een overlijden op zijn naam is gemeld, hoort daar geen reclame naast te zien. Dat is niet
preutsheid maar productbehoud: dat is precies het moment waarop mensen weglopen.
De tegel is laag — een afbeelding, een regel of twee tekst — en draagt zichtbaar het woord
**Advertentie**. Een advertentie die zich voordoet als onderdeel van de applicatie kost je meer
vertrouwen dan hij oplevert.
*(Eerder stond hier dat de balk ónder de tegels hoort. Dat is op 16 september 2026 gewijzigd: hij
staat nu bovenaan, maar wel achter "wat er speelt". De reden om hem laag en herkenbaar te houden
blijft staan.)*
## Hoe wordt gekozen wie er getoond wordt
Per keer dat iemand het overzicht opent is er ruimte voor één advertentie. Er kunnen meerdere
campagnes tegelijk lopen, elk met hun eigen aantal per uur. De keuze gaat zo:
1. Zoek alle campagnes die nu mogen draaien: goedgekeurd, betaald, en binnen hun periode.
2. Kijk per campagne hoe vaak hij dit klokuur al is getoond.
3. Laat de campagne zien die het verst achterloopt op zijn eigen aantal - dus de laagste
verhouding *getoond gedeeld door geboekt*.
4. Zijn ze allemaal op hun aantal, dan verschijnt er niets. Geen opvulling, geen herhaling.
5. Tel de vertoning op.
Zo krijgt wie meer boekt ook meer, zonder dat de een de ander verdringt, en zonder dat iemand
over zijn ingekochte aantal heen gaat.
**Let op wat dit betekent voor je voorwaarden.** Het geboekte aantal is een *maximum*, geen
belofte. Zijn er in een uur minder bezoekers dan er vertoningen zijn ingekocht, dan haalt
niemand zijn aantal. De adverteerder moet daarom in zijn eigen scherm kunnen zien wat er
werkelijk is getoond.
## Wat er wordt opgeslagen
### Adverteerder
Bedrijfsnaam, contactpersoon, e-mailadres, telefoonnummer, adres, postcode, plaats, land,
KvK-nummer en btw-nummer. Dit zijn bedrijfsgegevens, geen persoonsgegevens van bezoekers, maar
ze vallen wel onder je administratie: de belastingdienst wil ze zeven jaar kunnen zien.
### Campagne
- Wie hem heeft geboekt
- De tekst, het logo en de link waar hij heen gaat
- Begindatum en einddatum
- Aantal vertoningen per uur
- Het bedrag, het betaalkenmerk, en wanneer de betaling is gezien
- De stand: `concept`, `wacht-op-betaling`, `wacht-op-goedkeuring`, `loopt`, `afgewezen`,
`gestopt` - met bij een afwijzing de reden erbij, zodat de adverteerder weet waarom
### Vertoningen
Eén regel per campagne per klokuur met een teller erin, niet één regel per vertoning. Bij een
paar honderd bezoekers per uur scheelt dat een tabel die anders eindeloos volloopt, en voor het
kiezen én voor de verantwoording is het genoeg.
Kliks worden apart geteld, ook per uur.
## Het betaalpad
Bunq.me is een betaalverzoek, geen kassa. Er komt **geen bericht terug** naar MijnBesluit als er
betaald is. De applicatie kan dus niet zelf vaststellen dat een campagne betaald is.
Daarom loopt het zo:
1. De adverteerder stelt zijn campagne samen en ziet wat het kost.
2. Hij krijgt de bunq.me-link, met het campagnenummer als omschrijving.
3. De campagne komt op `wacht-op-betaling`.
4. De beheerder ziet in zijn bunq dat er betaald is, met dat nummer erbij, en vinkt de campagne
af als betaald.
5. In hetzelfde scherm keurt hij de inhoud goed of af.
6. Pas dan gaat hij draaien, vanaf de begindatum.
### De betaallink
`AdvertentieService.Betaallink` zet hem in elkaar:
```
https://bunq.me/KwikBit?amount=30.00&description=MB-00012
```
Het adres staat in de instellingen onder `Adverteren:BunqMe`; een rekening wisselt eerder dan de
code. Laat je die instelling leeg, dan komt er geen knop en staan het bedrag en het kenmerk er
gewoon om met de hand over te maken - een knop naar de verkeerde rekening is erger dan geen knop.
Twee dingen die makkelijk misgaan en daarom in een zelfcontrole vastliggen:
* **Het bedrag krijgt een punt**, wat de taalinstelling van de server ook is. Zonder dat maakt
een Nederlandse Windows er `30,00` van, en dan leest bunq een ander bedrag of helemaal geen.
* **Het kenmerk gaat gecodeerd mee.** Het is `MB-00012`: kort genoeg om over te typen als iemand
de link niet gebruikt.
Op de knop staat wat je doet en voor hoeveel ("Betaal € 30,00 via bunq"), niet waar de link heen
gaat. Dezelfde link staat in de bevestigingsmail, want niet iedereen komt terug in het scherm.
Stap 4 en 5 zijn twee vinkjes in één scherm. Wil je dit later automatisch, dan kan dat met de
bunq-API in plaats van een bunq.me-link; dat is een aparte verbouwing en vraagt sleutels en een
terugmeldadres.
## Waarom goedkeuring vooraf
Zonder goedkeuring kan iedereen die betaalt zijn advertentie naast jouw tekst over uitvaarten
krijgen. Je zou het achteraf kunnen weghalen, maar dan is het al gezien. De vertraging voor de
adverteerder is klein - je kijkt er dezelfde dag naar - en het verschil in wat er mis kan gaan
is groot.
## De prijs
bedrag = dagen x vertoningen per uur x tarief
met een bodem eronder. Begin- en einddag tellen allebei mee. Het tarief en de bodem staan in de
instellingen (`Adverteren:TariefCenten`, `Adverteren:MinimumCenten`) en niet in de code: het is
het soort getal dat je na drie maanden wilt bijstellen. Standaard 20 cent en een bodem van
10 euro.
Een voorbeeld: 30 dagen, 5 keer per uur = 150 x 20 cent = **30 euro**.
Hoe kom je aan zo'n getal? Reken terug naar wat de adverteerder hoogstens kan krijgen. Vijf keer
per uur, 24 uur, 30 dagen is 3600 vertoningen; 30 euro is dan ruim 8 euro per duizend. Aan de
lage kant voor een klein, goed omschreven publiek — en dat hoort ook, want dat maximum haalt hij
nooit: 's nachts opent niemand het overzicht.
Bij een campagne wordt het gebruikte tarief mee opgeslagen (`rate_cents`). Verandert het tarief
later, dan verandert een al geboekte campagne niet mee — anders valt een oude factuur niet meer
na te rekenen.
## Wat er nog beslist moet worden
- **Facturen.** Moet MijnBesluit ze maken, of doe je dat zelf? Met btw erop hoort er een factuur
met een doorlopend nummer bij.
- **Terugbetalen.** Wat als een campagne door een storing niet heeft gedraaid?
- **Algemene voorwaarden.** Daarin hoort in elk geval te staan dat het aantal een maximum is en
dat je een advertentie mag weigeren.
## Hoe het in de code zit
| Wat | Waar |
|---|---|
| Tabellen | `db/13_adverteren.sql`: `adverteerders`, `campagnes`, `campagne_uren` |
| Gegevens | `MijnBesluit.Domain/Entities/Adverteren.cs` |
| Regels | `MijnBesluit.Application/Services/AdvertentieService.cs` |
| Opslag | `AdvertentieRepository` in Infrastructure |
| Controles | `tests/MijnBesluit.SelfCheck/AdvertentieChecks.cs` |
Een adverteerder is een **eigen tabel** en geen regel in `users`. Hij is een bedrijf, geen persoon
met een kluis, familie of nalatenschap. Hem bij de gebruikers zetten zou betekenen dat elke vraag
over gebruikers hem moet uitzonderen, en één vergeten uitzondering is er één te veel in een
applicatie die over nalatenschappen gaat. Inloggen gaat wel op dezelfde manier: een code van zes
cijfers per e-mail, met een eigen doel-nummer in `login_codes`.
De afbeelding staat als blob bij de campagne en niet bij `vault_files`. Die tabel is voor spullen
van gebruikers en is versleuteld met sleutels per gebruiker; een advertentie is juist openbaar.
Hij wordt bij het opslaan verkleind.
Wat er bewust **niet** in zit: iets wat een advertentie koppelt aan wie hem ziet. Er wordt per
campagne per uur geteld, meer niet. Geen profielen, geen kenmerken, geen bezoeker die door de
applicatie heen te volgen is. Dit is een applicatie waarin mensen hun wachtwoorden en hun laatste
woorden bewaren; daar hoort geen advertentienetwerk in te zitten dat meekijkt.
De beheerder krijgt een e-mail zodra er een campagne is ingediend. Vanaf dat moment ligt het bij
hem: betaling afvinken en de inhoud beoordelen. Goedkeuren kán niet voordat de betaling is
afgevinkt — anders draait er iets waarvoor niet betaald is, en dat merk je pas als het voorbij is.
## In welke volgorde te bouwen
1. ~~Adverteerder en campagne vastleggen, met een beheerscherm om te betalen-af-te-vinken en goed
te keuren.~~ **Klaar.** `/beheer/adverteren`, bereikbaar vanaf `/beheer`.
2. ~~De balk op het overzicht, met de keuzeregels en de tellers.~~ **Klaar.** Op de website
rechtstreeks vanuit `AdvertentieService`; in de app via de socketactie `home.advertentie`.
3. ~~Het scherm waarin een adverteerder zichzelf aanmeldt en een campagne samenstelt, met de
bunq.me-link erin.~~ **Klaar.** `/adverteren` en `/adverteren/nieuw`.
4. ~~Het scherm waarin hij ziet wat er werkelijk is getoond.~~ **Klaar.** `/adverteren/campagnes`.
Zo staat het werkende deel er eerst, en kun je met één handmatig ingevoerde campagne al zien of
die balk doet wat je wilt voordat er een adverteerder bij komt.
## Het adverteerdersgedeelte
Drie pagina's, allemaal **statisch weergegeven** en niet interactief:
* `/adverteren` - wat het is, wat het kost, en inloggen met een code per e-mail. De ingang staat
op het inlogscherm, onder "Ik ben nieuw en wil een account aanmaken".
* `/adverteren/nieuw` - de campagne samenstellen.
* `/adverteren/campagnes` - zijn campagnes, de stand, de betaalregel, en wat er is getoond.
**Een adverteerder logt in met een eigen cookie en een eigen schema** (`Adverteerderslogin`),
los van de gebruikers van MijnBesluit, en dat cookie gaat alleen mee naar `/adverteren`. Dat is
geen netheid. Met één cookie en een claim erin hangt het verschil tussen een adverteerder en
iemand met een kluis af van de vraag of elk scherm aan die ene controle denkt; met twee schema's
kan dat niet misgaan. Om dezelfde reden zijn die schermen niet interactief: het nummer van de
adverteerder komt bij elk verzoek vers uit dat cookie en hoeft dus nergens te blijven staan.
De code die hij per e-mail krijgt heeft een **eigen doel** (`LoginCodePurpose.Adverteerder`).
Een adverteerderscode geeft dus nooit toegang tot een kluis, en een gewone inlogcode werkt niet
in het adverteerdersgedeelte. Dat ligt vast in de zelfcontrole "een code voor adverteerders is
geen inlogcode".
Wie nieuw is vult zijn bedrijfsnaam in vóórdat hij de code aanvraagt. Die naam wordt bij de code
bewaard en pas gebruikt zodra de code klopt - net als de voornaam bij het registreren van een
gebruiker. Zo kan niemand zich een bedrijfsnaam aanmeten zonder eerst bij dat e-mailadres te
kunnen.
De prijs loopt mee terwijl je invult, met een klein stukje javascript in de pagina zelf. Dat is
een gemak en geen bedrag: wat het kost rekent de server bij het indienen zelf uit.
### Een campagne met de hand invoeren om het te proberen
Er is nog geen scherm waarin een adverteerder zichzelf aanmeldt, dus voor een proef zet je er één
met de hand in. Daarna zie je hem op `/beheer/adverteren` staan, vink je de betaling af, keur je
hem goed, en staat hij op het overzicht.
```sql
INSERT INTO adverteerders (company_name, contact_name, email, created_at)
VALUES ('Uitvaartzorg Noord', 'Piet', 'piet@voorbeeld.nl', UTC_TIMESTAMP(6));
INSERT INTO campagnes (advertiser_id, text, link, starts_on, ends_on, per_hour,
amount_cents, rate_cents, payment_reference, state, created_at)
VALUES (LAST_INSERT_ID(), 'Persoonlijk afscheid, dichtbij', 'https://voorbeeld.nl',
CURDATE(), CURDATE() + INTERVAL 30 DAY, 5, 3000, 20, 'MB-HAND', 1, UTC_TIMESTAMP(6));
```
`state` 1 is *wacht op betaling*; de rest gaat via het beheerscherm.
## Het klikpad
| Adres | Wat het doet |
|---|---|
| `/advertentie/{id}/afbeelding` | De afbeelding. Openbaar, en een jaar in de browsercache |
| `/advertentie/{id}/uit` | Telt de klik en stuurt door naar de adverteerder |
Allebei zonder inlog: een advertentie is openbaar, en een plaatje dat alleen met een sessie te
laden is werkt niet in de app.
De tegel linkt naar `/uit` en niet rechtstreeks naar de adverteerder. Met javascript tellen zou
ook kunnen, maar dan telt hij niet mee als er iets misgaat onderweg. Dit is één plek en één
telling. De doorverwijzing is bewust *niet* permanent: die blijft anders in de browser hangen en
dan telt een tweede klik niet meer mee.
In de app opent de tegel in de browser van de telefoon (`IBuitenlink`), niet in de app zelf. Zou
hij binnen de webview openen, dan wordt MijnBesluit een browser en staat iemand op de site van een
adverteerder zonder weg terug naar zijn eigen kluis.