Product · Domain · Users

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

PersonaWhere they liveWhat they doKey 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.

SHOP_SPACE

High-street retail, malls.

SHOWROOM_SPACE

Auto, electronics, brand showrooms.

OFFICE_SPACE

Corporate & small-business offices.

CO_WORKING

Per-seat & team-room offerings.

HOTEL_RESORT

Hospitality assets & investments.

WAREHOUSE

Storage, fulfilment, cold-chain.

INDUSTRIAL

Manufacturing, BOI zones.

LAND

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

stateDiagram-v2 [*] --> DRAFT : agent saves DRAFT --> PENDING_REVIEW : submit for approval PENDING_REVIEW --> ACTIVE : approver approves PENDING_REVIEW --> DRAFT : approver requests changes PENDING_REVIEW --> REFUND_REJECTED : permanently rejected ACTIVE --> SOLD : agent marks sold ACTIVE --> LEASED : agent marks leased ACTIVE --> INACTIVE : unpublish / expire INACTIVE --> ACTIVE : republish SOLD --> [*] LEASED --> [*] REFUND_REJECTED --> [*]
Encoded in PropertyStateMachineConfig.java; transitions are role-gated and emit audit + notification events.

Article / blog lifecycle

stateDiagram-v2 [*] --> DRAFT DRAFT --> PENDING_REVIEW : editor submits PENDING_REVIEW --> PUBLISHED : super admin approves PENDING_REVIEW --> REJECTED : super admin rejects (reason captured) REJECTED --> DRAFT : editor revises PUBLISHED --> DRAFT : editor opens draft revision (hasPendingDraft=true) PUBLISHED --> ARCHIVED ARCHIVED --> [*]
Articles and blogs share the same workflow shape (ArticleStateMachineConfig, BlogStateMachineConfig). Draft revisions on a published article live alongside the published content until approved.

Subscription lifecycle

stateDiagram-v2 [*] --> PENDING : checkout initiated PENDING --> ACTIVE : PayHere SUCCESS PENDING --> FAILED : PayHere FAIL / 3DS abandoned ACTIVE --> GRACE_PERIOD : period_end reached, charge fails GRACE_PERIOD --> ACTIVE : retry succeeds GRACE_PERIOD --> EXPIRED : 30-day grace lapses ACTIVE --> EXPIRED : auto-renew off & period_end EXPIRED --> [*] FAILED --> [*]
Driven by SubscriptionLifecycleService and a nightly PayHereRecurringService scheduler. Properties auto-inactivate when the agency hits EXPIRED.

Inquiry / lead lifecycle

stateDiagram-v2 [*] --> NEW : visitor submits NEW --> CONTACTED : agent replies (InquiryMessage) CONTACTED --> CLOSED : marked closed / no response CONTACTED --> CONVERTED : deal closed offline CLOSED --> [*] CONVERTED --> [*]

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

  1. Owner signs up → user created with no agency, onboarding_step populated.
  2. Owner completes agency details → Agency row + Keycloak organisation provisioned (keycloakOrgId).
  3. Owner selects a starter package → free or paid checkout (PayHere preapproval if paid).
  4. Subscription becomes ACTIVE; owner invites team; each invite mints a Keycloak user + assigns realm role.

Property publish

  1. Agent runs the wizard (portal/properties/new) — required fields gated by property type.
  2. Images uploaded to R2; ImageProcessingService generates thumbnail + medium variants async.
  3. Property submitted → state machine fires SUBMIT → state PENDING_REVIEW.
  4. Property Approver reviews → approves (ACTIVE) or rejects (back to DRAFT).
  5. On approval, property surfaces in public search + sitemap; SEO metadata generated server-side.

Subscription renewal

  1. Nightly scheduler scans subscriptions WHERE status='ACTIVE' AND auto_renew=true AND period_end < today.
  2. PayHereRecurringService.chargeViaToken calls PayHere with stored customer_token.
  3. SUCCESS → new Payment row, FinancialAuditLog(PAYMENT_SUCCESS), credits refresh, invoice generated async, user notified.
  4. FAIL → subscription moves to GRACE_PERIOD with 30-day window; user receives PAYMENT_FAILED notification.

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, currency LKR, timezone Asia/Colombo.
Where this comes from in code: 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).