The business CPLK serves, and how the software reflects it.
CPLK exists because commercial real-estate supply in Sri Lanka is fragmented across agencies and individual property owners, with no shared marketplace that handles identity, tenancy, billing and SEO together. This page is the bridge between the product and the code — so engineers know why the system is shaped the way it is before they read how.
Product mission
Build a single, trustworthy commercial-property marketplace for Sri Lanka where agencies can run their listing business end-to-end (team, listings, leads, billing) and the public can discover and inquire about properties with strong SEO surfacing.
Supply side
Agencies and individual owners list, price, and promote properties; pay only when they want reach (subscription + boosts).
Demand side
Buyers, lessees and investors search by city, type and transaction; submit inquiries that become tenant-scoped leads.
Platform
CPLK staff approve listings, edit blogs, verify payment slips, process refunds, run support — all under audit.
User personas
| Persona | Where they live | What they do | Key constraints |
|---|---|---|---|
| SUPER_ADMIN platform | Owns CPLK. Single org. | Configure system, approve agencies, see everything cross-tenant. | Bypasses tenant filter via @BypassTenantFilter on specific paths. |
| AGENCY_SUPER_ADMIN tenant | One agency. | Billing, subscriptions, team invites, agency profile, approves agency content. | Cannot see other agencies' data. |
| AGENCY_ADMIN tenant | One agency. | Manages listings and inquiries within agency. | No billing access. |
| AGENT tenant | One agency. | Creates and edits property listings; handles assigned leads. | Cannot delete other agents' listings or touch billing. |
| PROPERTY_OWNER individual | Individual (CPLK individual agency). | Lists own properties, pays per listing via PayHere. | Pay-as-you-go billing model. |
| PROPERTY_APPROVER platform | CPLK staff. | Reviews listings across all agencies; approves, rejects, permanently rejects. | Cross-tenant by role; no billing. |
| FINANCIAL_OFFICER platform | CPLK staff. | Verifies payment slips, processes refunds, audits financial trail. | Read & act on financial events only. |
| BLOG_EDITOR platform | CPLK staff. | Authors blogs and articles; submits drafts for SUPER_ADMIN approval. | Cannot publish without approval. |
| CUSTOMER_SUPPORT_AGENT platform | CPLK staff. | Manages support tickets across the platform. | — |
| PROPERTY_VIEWER public | Public. | Browses, favourites, inquires. | Anonymous or registered, no listing rights. |
Property types
CPLK is intentionally commercial-only. The eight property types are encoded as a Java enum and gate the listing wizard, search filters and SEO sitemap.
High-street retail, malls.
Auto, electronics, brand showrooms.
Corporate & small-business offices.
Per-seat & team-room offerings.
Hospitality assets & investments.
Storage, fulfilment, cold-chain.
Manufacturing, BOI zones.
Plots, agricultural & commercial land.
Each type can be listed under three transaction types (SALE LEASE BOTH), and supports four price units (fixed, negotiable, POA, per seat).
Lifecycles
Property listing lifecycle
PropertyStateMachineConfig.java; transitions are role-gated and emit audit + notification events.Article / blog lifecycle
ArticleStateMachineConfig, BlogStateMachineConfig). Draft revisions on a published article live alongside the published content until approved.Subscription lifecycle
SubscriptionLifecycleService and a nightly PayHereRecurringService scheduler. Properties auto-inactivate when the agency hits EXPIRED.Inquiry / lead lifecycle
Monetisation
Subscription packages
Each agency has at most one ACTIVE subscription, scoped per billing cycle:
- MONTHLY — ad slots reset every renewal; credits refresh monthly.
- YEARLY — 12-month commitment (
annual_commitment_end); credits refresh monthly via scheduler. - PAY_AS_YOU_GO — used by
PROPERTY_OWNER; per-listing charge.
Packages have a fixed targetAudience
(AGENCY_ONLY, PROPERTY_OWNER_ONLY, HIDDEN),
so the catalogue API only ever returns plans appropriate for the caller.
Boosts & credits
On top of subscriptions, agencies buy boost packs (one-off point bundles) and apply them to specific listings.
- Boost types: FEATURED, HIGHLIGHTED.
- Time-windowed (
start_date,end_date), expire automatically. - Credit ledger is immutable (
PointsTransaction· EARN/SPEND). - Boost purchases & spends are double-logged:
AuditLog+FinancialAuditLog.
Headline user journeys
Agency onboarding
- Owner signs up → user created with no agency,
onboarding_steppopulated. - Owner completes agency details → Agency row + Keycloak organisation provisioned (
keycloakOrgId). - Owner selects a starter package → free or paid checkout (PayHere preapproval if paid).
- Subscription becomes
ACTIVE; owner invites team; each invite mints a Keycloak user + assigns realm role.
Property publish
- Agent runs the wizard (
portal/properties/new) — required fields gated by property type. - Images uploaded to R2;
ImageProcessingServicegenerates thumbnail + medium variants async. - Property submitted → state machine fires
SUBMIT→ statePENDING_REVIEW. - Property Approver reviews → approves (
ACTIVE) or rejects (back toDRAFT). - On approval, property surfaces in public search + sitemap; SEO metadata generated server-side.
Subscription renewal
- Nightly scheduler scans
subscriptions WHERE status='ACTIVE' AND auto_renew=true AND period_end < today. PayHereRecurringService.chargeViaTokencalls PayHere with storedcustomer_token.- SUCCESS → new
Paymentrow,FinancialAuditLog(PAYMENT_SUCCESS), credits refresh, invoice generated async, user notified. - FAIL → subscription moves to
GRACE_PERIODwith 30-day window; user receivesPAYMENT_FAILEDnotification.
Constraints that shape the code
- Multi-tenancy is non-negotiable — accidental cross-tenant reads are a Sev-1.
- PayHere webhooks may fire multiple times; idempotency must be guaranteed at the database.
- Email PII (agency & user phone, address) is encrypted at rest via
EncryptedStringConverter. - Soft-delete is the rule for properties, articles, blogs — slug uniqueness uses a partial index
WHERE deleted = false. - Audit logs are immutable (
ImmutableEntity); financial events live in a separate ledger. - SEO is critical; public pages are SSR with ISR, sitemap covers 1,000+ filter combinations.
- Sri Lanka is the only market; locale is
en_LK, currencyLKR, timezoneAsia/Colombo.
backend/.../user/Role.java (roles),
property/Property.java (types & transactions),
config/PropertyStateMachineConfig.java (workflow),
subscription/Package.java (plans),
payment/PayHereRecurringService.java (renewals),
tenant/TenantFilter.java + TenantAwareEntity.java (isolation).