Eerste commit
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user