Stripe je odbio zahtev. PayPal je odbio. Google Ads je odbio.
Tri udarca zaredom. Stari sistem sa poenima — upload za poene, troši poene za download — je udario svaku crvenu zastavu.
Nisam mogao da zakrpim stari sistem. Morao sam da dizajniram novi od nule.
Zahtevi koje plugin ne može da ispuni
1. Diminishing returns za upload-ove
Želeo sam da nagradim upload-ovanje, ali ne da dozvoli gaming.
Logika: U klizećem prozoru od 30 dana:
-
- upload: 14 dana PRO + 1 download token
-
- upload: 7 dana PRO
-
- upload: 3 dana PRO
- 4+ upload: 0 dana (ali dokument se i dalje objavljuje)
Maximum: 28 dana mesečno.
MemberPress ne podržava ovo. Nijedan plugin ne podržava.
2. Upload-to-earn + paid stacking
Korisnik koji plati PRO i još uploaduje trebalo bi da dobije produženje.
PRO do 15. februara + upload = PRO do 1. marta (+14 dana).
Plaćanje i zarada se sabiraju. Nagrada za doprinos bez obzira na status plaćanja.
3. Atomic quota consumption
Race condition: Dva taba, isti korisnik, poslednji kredit. Oba klika istovremeno.
Bez zaštite: oba prolaze, korisnik dobija 2 dokumenta za cenu 1.
Potreban: Atomic SQL koji garantuje da samo jedan prolazi.
4. Library permanence
Jednom preuzeto, zauvek dostupno.
PRO istekne, ali biblioteka ostaje. Dokumenti koje si preuzeo su tvoji.
5. Download retry protection
Problem: Korisnik potroši kredit, ali download failuje (network, browser).
Rešenje: Grant sa 10-minutnim prozorom za retry bez dodatne potrošnje.
Šest tabela, jedna svrha
-- Dostupni planovi
wp_studenti_plans
-- Korisnički entitlements (PRO pretplate)
wp_studenti_entitlements
-- Download kvote vezane za entitlements
wp_studenti_quota_buckets
-- Permanentni pristup (preuzeto/uploadovano)
wp_studenti_user_library
-- Audit log za sve događaje
wp_studenti_ledger
-- Tracking za diminishing returns
wp_studenti_pro_grants
Svaka tabela ima jasnu svrhu. Foreign keys održavaju integritet. Indeksi optimizuju česte upite.
Entitlement model
Entitlement nije boolean. To je objekat sa stanjem:
[
'status' => 'active', // active | expired | revoked | scheduled
'starts_at' => '2026-01-01',
'ends_at' => '2026-02-15',
'source' => 'upload_reward', // upload_reward | stripe | admin | migration
'source_ref' => 'job_id_123'
]
Scheduled status je za buduće Stripe pretplate koje počinju nakon trial perioda.
Atomic quota consumption
Ovo je srž zaštite od race conditions:
UPDATE wp_studenti_quota_buckets
SET used = used + 1
WHERE entitlement_id = (
SELECT id FROM wp_studenti_entitlements
WHERE user_id = %d
AND status = 'active'
AND ends_at > NOW()
LIMIT 1
)
AND metric = 'downloads'
AND used < allowance
Ako affected_rows = 0, kvota je iscrpljena ili entitlement ne postoji.
Jedan SQL. Atomičan. Bez race conditions.
Two-step download flow
Nikada ne troši kredit bez potvrde.
Korak 1: Document page
Korisnik vidi "Preuzmi dokument" dugme. Klik vodi na /download stranicu.
Korak 2: Download page
Korisnik vidi potvrdu: "Imaš još X preuzimanja nakon ovog."
Tek kada klikne "Potvrdi preuzimanje", kredit se troši.
Zašto? Jer slučajni klikovi, browser prefetch, ili dupli tap ne smeju da troše kredite.
Download grant system
Problem koji rešava: "Potrošio sam kredit ali download nije uspeo."
Rešenje:
- Korisnik potvrdi → kredit potrošen → grant kreiran → redirect na signed URL
- Ako download failuje, korisnik klikne "Preuzmi ponovo"
- Grant check pronađe validan grant → preskoči potrošnju → novi signed URL
- Korisnik dobija fajl bez gubitka kredita
Grant properties:
- TTL: 10 minuta
- Storage: WordPress transients (auto-expiry)
- Idempotency: Key sprečava double-spend na form refresh
CTA state machine
Jedna funkcija za sve UI plasmane:
$state = studenti_get_cta_state($user_id, $document_id);
Vraća:
[
'primaryText' => 'Preuzmi dokument',
'primaryHref' => '/download?document=123',
'primaryKind' => 'download',
'secondaryText' => null,
'secondaryHref' => null,
'note' => 'Imaš još 3 preuzimanja',
'costsCredit' => false,
'badge' => null,
'reason' => 'has_quota',
'quotaRemaining' => 3,
]
7 mogućih stanja, 4 UI plasmana, 1 funkcija.
myCRED migracija
5.873 korisnika. 7.900 library entries. 12 abuse naloga.
Migracija je mapirala stare poene na:
- Entitlement (ako ima dovoljno za PRO)
- Library entries (preuzeti dokumenti)
- Flagged status (abuse detection)
Jedan korisnik je imao 887 transakcija. 887.
Helper funkcije
Za temu i plugin, globalne funkcije:
studenti_user_has_pro($user_id) // Check PRO status
studenti_can_download($user_id, $post_id) // Check download permission
studenti_consume_download($user_id, $post_id) // Consume credit
studenti_get_cta_state($user_id, $post_id) // Get CTA copy
studenti_has_in_library($user_id, $post_id) // Check library
studenti_is_author($user_id, $post_id) // Check authorship
studenti_get_downloads_remaining($user_id) // Remaining credits
Clean API za ostatak codebase-a. Implementacija skrivena.
Šta plugin ne bi mogao
- Diminishing returns — Nijedan plugin nema rolling window logiku
- Atomic consumption — Većina koristi PHP check-then-update (race-prone)
- Grant system — Retry protection nije standard
- Stacking — Paid + earned stacking zahteva custom logiku
- Library permanence — Plugins obično vezuju pristup za aktivan status
Buduća Stripe integracija
Webhook handler je spreman:
customer.subscription.created → grant_entitlement()
customer.subscription.updated → extend_entitlement()
customer.subscription.deleted → revoke_entitlement()
Price-to-plan mapping preko STUDENTI_STRIPE_PRICE_MAP konstante.
Ovo je v1. Očekujem iteracije kako korisnici počnu da koriste sistem.
Custom membership sistem daje potpunu kontrolu. Ali ta kontrola dolazi sa odgovornošću da razumete svaki edge case.
Race conditions. Retry failures. Abuse detection. Stacking logic. Download grants.
MemberPress bi pokrio 80% use case-a. Ali tih 20% je razlika između "članstvo" i "članstvo koje Stripe odobrava."