Bounded Context: Rider Management (RDR)

Responsibility: Rider signup and phone verification, payment-method association, profile data (saved addresses and accessibility preferences), rider rating ingestion from completed rides, automated low-rating warnings and ban-proposal generation, operator-driven ban decisions, and a 30-day appeal workflow with SLA-bound resolution.


Context Map

Relation Other Bounded Context Position Pattern Description
RDR ↔ Payment Payment (PAY) downstream Anti-Corruption Layer (ACL) RDR calls PaymentValidationGateway.validatePaymentMethod(...) on AddPaymentMethod; the gateway wraps the Payment context’s API. RDR also consumes the external event PaymentMethodValidated from PAY to confirm the handshake.
RDR ↔ RideManagement Ride Management (RIDE) downstream Conformist (consumed event) + Published Language (bridge reaction) RDR consumes the external event RideCompleted from RIDE and bridges it into a RecordRiderRating command via the reaction recordRatingOnRideCompleted. RIDE is the source of truth for ride completion; RDR is the source of truth for the rider’s average rating.
RDR ↔ DriverManagement Driver Management (DRV) peer Shared Kernel (operator workflow) Trust-and-safety operator identity, reason capture, and audit-trail conventions are shared with driver suspension flows. Ban proposals reference rideId, whose driver record lives in DRV.
RDR → TelephonyProvider external infrastructure downstream Conformist (infrastructure) RDR depends on an external telephony API for SMS verification codes. Wrapped behind the VerificationCodeChannel infrastructure-service.
RDR → NotificationProvider external infrastructure downstream Conformist (infrastructure) RDR depends on an external push/SMS/email provider for rider notifications. Wrapped behind the RiderNotificationService infrastructure-service.

Enums

RiderStatus

  • Values: unverified, active, banned, appealInReview, permanentlyBanned.
  • Realizes: REQ-RDR-005 (ready-to-ride gate), REQ-RDR-024 (operator ban application), REQ-RDR-026 (appeal resolution outcome). The terminal permanentlyBanned value is the final state of the rider lifecycle.

PaymentMethodType

  • Values: creditCard, debitCard, paypal, applePay, googlePay.
  • Realizes: REQ-RDR-006 (typed reference to a Payment-context method).

AppealStatus

  • Values: pending, approved, rejected.
  • Realizes: REQ-RDR-025, REQ-RDR-026.

Value Types

PhoneNumber

  • Fields:
    • countryCode: String
    • number: String
  • Realizes: REQ-RDR-001, REQ-RDR-002 (phone is the minimum identity).

VerificationCode

One-time code dispatched by the telephony channel.

  • Fields:
    • code: String — minLength(4), maxLength(6)
    • issuedAt: DateTime
    • expiresAt: DateTime
  • Invariants:
    • shortLifetime: expiresAt <= issuedAt + 10min — codes have a bounded lifetime.
  • Realizes: REQ-RDR-001, REQ-RDR-002.

VerificationAttempt

Failed-attempt counter and brute-force lockout window.

  • Fields:
    • failedCount: Integer — min(0), max(3)
    • lockedUntil: DateTime optional
  • Invariants:
    • lockoutTriggered :: “Three failures locks the phone for 15 minutes” — enforcement: reject. When failedCount >= 3, lockedUntil must be defined.
  • Realizes: REQ-RDR-003.

SavedAddress

A rider-named place — Home, Work, custom.

  • Fields:
    • label: String — maxLength(50)
    • street: String
    • city: String
    • zipCode: String
    • country: String
    • latitude: Decimal
    • longitude: Decimal
  • Realizes: REQ-RDR-010, REQ-RDR-011, REQ-RDR-012.

PaymentMethodRef

Reference to a payment method whose canonical record lives in the Payment context.

  • Fields:
    • paymentMethodId: UUID
    • type: PaymentMethodType
    • label: String
    • isActive: Boolean — default false
    • addedAt: DateTime
  • Realizes: REQ-RDR-006. The isActive flag flips to true only after the Payment context confirms validation.

RatingScore

  • Fields:
    • value: Integer — min(1), max(5)
  • Realizes: REQ-RDR-021 (rating range invariant carried at the value-type level).

AverageRiderRating

Aggregated rating across completed rides.

  • Fields:
    • value: Decimal — min(1.0), max(5.0)
    • totalRatings: Integer — min(0)
  • Realizes: REQ-RDR-020, REQ-RDR-021.

AccessibilityPreference

Per-rider preference flags propagated to ride requests.

  • Fields:
    • wheelchairAccessible: Boolean — default false
    • quietRide: Boolean — default false
    • largerVehicle: Boolean — default false
  • Realizes: REQ-RDR-013.

Appeal

Captured ban appeal — text, timestamp, current status.

  • Fields:
    • reason: String — maxLength(2000)
    • submittedAt: DateTime
    • status: AppealStatus — default "pending"
  • Realizes: REQ-RDR-025, REQ-RDR-026, REQ-RDR-027.

Entities

Rider

Rider lifecycle root — signup, profile, ratings, bans.

  • Identifier: riderId : UUID
  • Fields:
    • phoneNumber: PhoneNumber
    • phoneVerified: Boolean — default false
    • firstName: String optional
    • lastName: String optional
    • email: String optional
    • status: RiderStatus — default "unverified"
    • verificationCode: VerificationCode optional
    • verificationAttempt: VerificationAttempt
    • savedAddresses: list<SavedAddress>
    • paymentMethods: list<PaymentMethodRef>
    • accessibility: AccessibilityPreference
    • averageRating: AverageRiderRating optional
    • totalRides: Integer — min(0), default 0
    • appeal: Appeal optional
    • createdAt: DateTime
    • updatedAt: DateTime
  • Invariants:
    • readyToRideRequiresVerifiedPhoneAndPayment :: “Active rider must have verified phone + active payment” — enforcement: reject. When status = active, phoneVerified = true and there exists a PaymentMethodRef with isActive = true.
    • noGovernmentIdAtSignup :: “Government ID is not collected at signup” — enforcement: reject. When status = unverified, firstName and lastName are null.
  • Realizes: REQ-RDR-001 through REQ-RDR-006, REQ-RDR-010 through REQ-RDR-013, REQ-RDR-020 through REQ-RDR-027.

Operations on Rider

  • Sign up rider on command RegisterRider

    • Realizes: REQ-RDR-001, REQ-RDR-004.
    • State changes (sets): phoneNumber from the command; status = unverified; verificationCode = generateCode(); verificationAttempt = VerificationAttempt(failedCount = 0); createdAt = now(); updatedAt = now().
    • Emits: RiderRegistered with riderId, phoneNumber, code, registeredAt.
  • Verify phone on command VerifyPhone

    • Realizes: REQ-RDR-002.
    • Preconditions:
      • codeOutstanding: Rider.verificationCode is defined.
      • codeNotExpired: Rider.verificationCode.expiresAt > now().
      • notLocked: Rider.verificationAttempt.lockedUntil is null or already past.
      • codeMatches: submitted code equals stored code.
    • State changes: phoneVerified = true; verificationCode = null; verificationAttempt = VerificationAttempt(failedCount = 0).
    • Emits: RiderPhoneVerified with riderId, verifiedAt = now().
  • Submit invalid verification code on command SubmitInvalidVerificationCode

    • Realizes: REQ-RDR-003.
    • State changes: increment verificationAttempt.failedCount by one; if the new count is >= 3, set lockedUntil = now() + 15min, otherwise null.
    • Emits: VerificationFailed with riderId, failedCount, lockedUntil, failedAt.
  • Add payment method on command AddPaymentMethod

    • Realizes: REQ-RDR-006.
    • Precondition phoneVerified: Rider.phoneVerified = true.
    • State changes: append the method to paymentMethods; if Rider.status = unverified, advance to active.
    • Emits: PaymentMethodAdded with riderId, method, addedAt.
  • Save address on command SaveAddress

    • Realizes: REQ-RDR-010.
    • State change: append the address to savedAddresses.
    • Emits: AddressSaved with riderId, address, savedAt.
  • Set accessibility preference on command SetAccessibilityPreference

    • Realizes: REQ-RDR-013.
    • State change: replace accessibility with the value from the command.
    • Emits: AccessibilityPreferenceUpdated with riderId, preference, updatedAt.
  • Record rider rating on command RecordRiderRating

    • Realizes: REQ-RDR-020, REQ-RDR-021.
    • Precondition validRange: score.value in [1, 5].
    • State changes: averageRating = recompute(prior, newScore); totalRides = totalRides + 1.
    • Postcondition (implicit): the new average reflects the previous total plus one.
    • Emits: RiderRated with riderId, rideId, score, comment, newAverage, totalRides, submittedAt.
  • Issue low-rating warning on command WarnLowRating

    • Realizes: REQ-RDR-022.
    • Preconditions:
      • matureRider: Rider.totalRides > 10.
      • belowWarningThreshold: Rider.averageRating.value < 4.0.
    • Emits: LowRatingWarningIssued with riderId, averageRating, issuedAt.
  • Propose ban on command ProposeRiderBan

    • Realizes: REQ-RDR-023.
    • Preconditions:
      • matureRider: Rider.totalRides > 10.
      • belowBanThreshold: Rider.averageRating.value < 3.5.
    • Emits: RiderBanProposalCreated with riderId, averageRating, proposedAt.
  • Apply rider ban on command BanRider

    • Realizes: REQ-RDR-024, REQ-RDR-NFR-004.
    • Precondition operatorApproved: banRider.operatorId is defined.
    • State change: status = banned.
    • Emits: RiderBanned with riderId, operatorId, reason, bannedAt.
  • Submit ban appeal on command SubmitBanAppeal

    • Realizes: REQ-RDR-025.
    • Preconditions:
      • isBanned: Rider.status = banned.
      • withinAppealWindow: now() - banAppealWindowStart(Rider) <= 30days.
    • State changes: status = appealInReview; appeal = Appeal(reason, submittedAt = now(), status = pending).
    • Emits: BanAppealSubmitted with riderId, submittedAt.
  • Reject late appeal on command RejectLateAppeal

    • Realizes: REQ-RDR-027.
    • Preconditions:
      • isBanned: Rider.status = banned.
      • outsideWindow: now() - banAppealWindowStart(Rider) > 30days.
    • Emits: RiderBanAppealRejectedAsLate with riderId, rejectedAt.
  • Resolve ban appeal on command ResolveBanAppeal

    • Realizes: REQ-RDR-026, REQ-RDR-NFR-004.
    • Precondition appealUnderReview: Rider.status = appealInReview.
    • State changes: when outcome is approved, status = active and appeal.status = approved; otherwise status = permanentlyBanned and appeal.status = rejected.
    • Emits: BanAppealResolved with riderId, operatorId, outcome, resolvedAt.

Aggregates

RiderAggregate

  • Root: Rider. The aggregate enforces all rider-level invariants — the ready-to-ride gate, the no-government-ID rule at signup, and the consistency of verificationAttempt and averageRating recomputation.

State Machines

RiderLifecycle on Rider

  • Start state: unverified.
  • States:
    • unverified — rider profile created, code dispatched, no active payment method yet.
    • active — state-scoped invariant phoneVerified: Rider.phoneVerified = true. The rider can request rides.
    • banned — operator has applied a ban; the rider is in the appeal window.
    • appealInReview — the rider has submitted an appeal within 30 days; an operator decision is pending.
    • permanentlyBanned — the appeal was rejected (or no appeal was filed and the window closed). Final state.
  • Transitions:
    • unverifiedactive on command AddPaymentMethod (the first active payment method completes the profile).
    • activebanned on command BanRider.
    • bannedappealInReview on command SubmitBanAppeal.
    • appealInReviewactive on command ResolveBanAppeal (outcome = approved).
    • appealInReviewpermanentlyBanned on command ResolveBanAppeal (outcome = rejected).
  • Final state: permanentlyBanned.

The phoneVerified invariant on active works in concert with the entity-level readyToRideRequiresVerifiedPhoneAndPayment invariant — together they guarantee that no rider can hold the active status without a verified phone and at least one active payment method.


Domain Services

RiderRatingCalculator

Recomputes the rider average rating when a new score arrives.

  • Operations:
    • recompute(prior: AverageRiderRating, newScore: RatingScore) : AverageRiderRating — increments totalRatings, blends the new score into the running average.
  • Realizes: REQ-RDR-020.

Infrastructure Services

VerificationCodeChannel

Telephony provider for SMS verification codes — infrastructure boundary.

  • Operations:
    • sendCode(phoneNumber: PhoneNumber, code: string) : void — wraps an external SMS API.
  • Realizes: REQ-RDR-001, REQ-RDR-NFR-003 (signup-success-rate budget excludes telephony failures).

PaymentValidationGateway

Anti-Corruption Layer to the Payment context for method validation — infrastructure boundary.

  • Operations:
    • validatePaymentMethod(paymentMethodId: uuid) : boolean — calls the Payment context’s validation API and returns the boolean outcome. RDR persists the method as isActive = true only when validation succeeds.
  • Realizes: REQ-RDR-006.

RiderNotificationService

Push, SMS, and email channel for rider-facing notifications — infrastructure boundary.

  • Operations:
    • notifyRider(riderId: uuid, subject: string, message: string) : void — wraps an external push/SMS/email provider.
  • Realizes: REQ-RDR-022, REQ-RDR-024, REQ-RDR-026.

Commands

Command Fields Realizes
RegisterRider phoneNumber, correlationId REQ-RDR-001, REQ-RDR-004
VerifyPhone riderId, submittedCode, correlationId REQ-RDR-002
SubmitInvalidVerificationCode riderId, correlationId REQ-RDR-003
AddPaymentMethod riderId, method, correlationId REQ-RDR-006
SaveAddress riderId, address, correlationId REQ-RDR-010
SetAccessibilityPreference riderId, accessibility, correlationId REQ-RDR-013
RecordRiderRating riderId, rideId, score, comment?, correlationId REQ-RDR-020, REQ-RDR-021
WarnLowRating riderId, correlationId REQ-RDR-022
ProposeRiderBan riderId, correlationId REQ-RDR-023
BanRider riderId, operatorId, reason, correlationId REQ-RDR-024
SubmitBanAppeal riderId, reason, correlationId REQ-RDR-025
RejectLateAppeal riderId, correlationId REQ-RDR-027
ResolveBanAppeal riderId, operatorId, outcome, correlationId REQ-RDR-026

Events

Internal events

Event Fields Realizes
RiderRegistered riderId, phoneNumber, code, registeredAt REQ-RDR-001
RiderPhoneVerified riderId, verifiedAt REQ-RDR-002
VerificationFailed riderId, failedCount, lockedUntil?, failedAt REQ-RDR-003
PaymentMethodAdded riderId, method, addedAt REQ-RDR-006
AddressSaved riderId, address, savedAt REQ-RDR-010
AccessibilityPreferenceUpdated riderId, preference, updatedAt REQ-RDR-013
RiderRated riderId, rideId, score, comment?, newAverage, totalRides, submittedAt REQ-RDR-020
LowRatingWarningIssued riderId, averageRating, issuedAt REQ-RDR-022
RiderBanProposalCreated riderId, averageRating, proposedAt REQ-RDR-023
RiderBanned riderId, operatorId, reason, bannedAt REQ-RDR-024, REQ-RDR-NFR-004
BanAppealSubmitted riderId, submittedAt REQ-RDR-025
RiderBanAppealRejectedAsLate riderId, rejectedAt REQ-RDR-027
BanAppealResolved riderId, operatorId, outcome, resolvedAt REQ-RDR-026, REQ-RDR-NFR-004

External-bound events (Published Language)

RiderBanned and BanAppealResolved are append-only audit-trail entries (REQ-RDR-NFR-004) and are made available to the Trust & Safety operator workflow shared with Driver Management. RiderRegistered and RiderPhoneVerified are available to other contexts that care about identity provenance (e.g., Payment may key risk scores on the verified-phone signal).

External-consumed events

Event Source context Consumed via
RideCompleted Ride Management (RIDE) external-event RideCompleted from RideManagement — bridged into RecordRiderRating by the reaction recordRatingOnRideCompleted when the carried riderRating is defined
PaymentMethodValidated Payment (PAY) external-event PaymentMethodValidated from Payment — confirms the validation handshake initiated by AddPaymentMethodPaymentValidationGateway.validatePaymentMethod(...)

Reactions

sendCodeOnRegistration

“Dispatch SMS code immediately on signup.”

  • Trigger: event RiderRegistered.
  • Effect: VerificationCodeChannel.sendCode(phoneNumber = event.phoneNumber, code = event.code.code).
  • Realizes: REQ-RDR-001.

warnRiderOnLowRating

“Warn at avg < 4.0 with 10+ rides.”

  • Trigger: event RiderRated.
  • Guard: event.totalRides > 10 and event.newAverage.value < 4.0 and event.newAverage.value >= 3.5.
  • Effect: issue command WarnLowRating(riderId = event.riderId).
  • Realizes: REQ-RDR-022.

proposeBanOnVeryLowRating

“Queue ban proposal at avg < 3.5 with 10+ rides.”

  • Trigger: event RiderRated.
  • Guard: event.totalRides > 10 and event.newAverage.value < 3.5.
  • Effect: issue command ProposeRiderBan(riderId = event.riderId).
  • Realizes: REQ-RDR-023.

recordRatingOnRideCompleted

“Bridge from Ride Management — record driver rating of rider.”

  • Trigger: external event RideCompleted (from RideManagement).
  • Guard: event.riderRating is defined.
  • Effect: issue command RecordRiderRating(riderId = event.riderId, rideId = event.rideId, score = event.riderRating.score, comment = event.riderRating.comment).
  • Realizes: REQ-RDR-020. This is the one cross-context bridge reaction in RDR — RIDE owns ride completion and carries the optional riderRating payload; RDR owns rating ingestion and the average computation.

notifyOnBan

“Notify rider of ban with reason and appeal path.”

  • Trigger: event RiderBanned.
  • Effect: RiderNotificationService.notifyRider(riderId = event.riderId, subject = "Account banned", message = event.reason + " You may appeal within 30 days.").
  • Realizes: REQ-RDR-024.

notifyOnAppealResolution

“Notify rider of appeal outcome.”

  • Trigger: event BanAppealResolved.
  • Effect: RiderNotificationService.notifyRider(riderId = event.riderId, subject = "Appeal resolved", message = "Outcome: " + event.outcome).
  • Realizes: REQ-RDR-026.

Traceability Matrix

Requirement Realized by
REQ-RDR-001 PhoneNumber, VerificationCode, Rider, command RegisterRider, op Sign up rider, event RiderRegistered, VerificationCodeChannel.sendCode, reaction sendCodeOnRegistration
REQ-RDR-002 VerificationCode, Rider, command VerifyPhone, op Verify phone (preconditions codeOutstanding, codeNotExpired, notLocked, codeMatches), event RiderPhoneVerified
REQ-RDR-003 VerificationAttempt (invariant lockoutTriggered), command SubmitInvalidVerificationCode, op Submit invalid verification code, event VerificationFailed
REQ-RDR-004 Rider (invariant noGovernmentIdAtSignup), op Sign up rider
REQ-RDR-005 Rider (invariant readyToRideRequiresVerifiedPhoneAndPayment), RiderStatus.active, statemachine RiderLifecycle (state active)
REQ-RDR-006 PaymentMethodRef, Rider, command AddPaymentMethod, op Add payment method, event PaymentMethodAdded, PaymentValidationGateway.validatePaymentMethod, external-event PaymentMethodValidated
REQ-RDR-010 SavedAddress, Rider, command SaveAddress, op Save address, event AddressSaved
REQ-RDR-011 SavedAddress, Rider (edit/delete operations on savedAddresses list)
REQ-RDR-012 SavedAddress (consumed by the rider app for one-tap selection)
REQ-RDR-013 AccessibilityPreference, command SetAccessibilityPreference, op Set accessibility preference, event AccessibilityPreferenceUpdated
REQ-RDR-020 RatingScore, AverageRiderRating, RiderRatingCalculator.recompute, command RecordRiderRating, op Record rider rating, event RiderRated, reaction recordRatingOnRideCompleted, external-event RideCompleted
REQ-RDR-021 RatingScore (value min(1) max(5)), AverageRiderRating, op Record rider rating (precondition validRange)
REQ-RDR-022 command WarnLowRating, op Issue low-rating warning (preconditions matureRider, belowWarningThreshold), event LowRatingWarningIssued, reaction warnRiderOnLowRating, RiderNotificationService.notifyRider
REQ-RDR-023 command ProposeRiderBan, op Propose ban (preconditions matureRider, belowBanThreshold), event RiderBanProposalCreated, reaction proposeBanOnVeryLowRating
REQ-RDR-024 RiderStatus.banned, command BanRider, op Apply rider ban, event RiderBanned, statemachine transition active -> banned, reaction notifyOnBan, RiderNotificationService.notifyRider
REQ-RDR-025 Appeal, RiderStatus.appealInReview, command SubmitBanAppeal, op Submit ban appeal (precondition withinAppealWindow), event BanAppealSubmitted, statemachine transition banned -> appealInReview
REQ-RDR-026 AppealStatus, command ResolveBanAppeal, op Resolve ban appeal, event BanAppealResolved, statemachine transitions appealInReview -> active and appealInReview -> permanentlyBanned, reaction notifyOnAppealResolution
REQ-RDR-027 Appeal, command RejectLateAppeal, op Reject late appeal (precondition outsideWindow), event RiderBanAppealRejectedAsLate
REQ-RDR-NFR-001 (operational policy — implementation concern: AES-256 encryption at rest for Rider, PhoneNumber, SavedAddress)
REQ-RDR-NFR-002 (operational policy — implementation concern: 30-day erasure pipeline coordinated across contexts)
REQ-RDR-NFR-003 VerificationCodeChannel (telephony-failure exclusion clause), op Sign up rider
REQ-RDR-NFR-004 event RiderBanned, event BanAppealResolved, op Apply rider ban, op Resolve ban appeal (append-only audit log retained ≥ 7 years)

Concrete syntax (.domain DSL)

The full .domain source that this page narrates. Entity declarations, value types, commands, events, invariants, preconditions, services, state machine, external events, and reactions appear in parse order.

context RiderManagement :: "Rider signup, phone verification, profile, ratings, bans, appeals" {
    requirements-source "rider-management.sysreq"

    enum RiderStatus { unverified, active, banned, appealInReview, permanentlyBanned }
    enum PaymentMethodType { creditCard, debitCard, paypal, applePay, googlePay }
    enum AppealStatus { pending, approved, rejected }

    // -----------------------------------------------------------------
    // Value types
    // -----------------------------------------------------------------

    value PhoneNumber {
        fields { countryCode : string; number : string }
    }

    value VerificationCode {
        satisfies [REQ-RDR-001, REQ-RDR-002]
        fields {
            code : string minLength(4) maxLength(6)
            issuedAt : datetime
            expiresAt : datetime
        }
        invariants {
            shortLifetime { expiresAt <= issuedAt + 10min }
        }
    }

    value VerificationAttempt {
        satisfies [REQ-RDR-003]
        fields {
            failedCount : int min(0) max(3)
            lockedUntil : datetime optional
        }
        invariants {
            lockoutTriggered :: "Three failures locks the phone for 15 minutes" enforcement reject {
                if failedCount >= 3 { lockedUntil is defined }
            }
        }
    }

    value SavedAddress {
        satisfies [REQ-RDR-010, REQ-RDR-011, REQ-RDR-012]
        fields {
            label : string maxLength(50)
            street : string
            city : string
            zipCode : string
            country : string
            latitude : decimal
            longitude : decimal
        }
    }

    value PaymentMethodRef {
        satisfies [REQ-RDR-006]
        fields {
            paymentMethodId : uuid
            type : PaymentMethodType
            label : string
            isActive : boolean default(false)
            addedAt : datetime
        }
    }

    value RatingScore {
        fields { value : int min(1) max(5) }
    }

    value AverageRiderRating {
        satisfies [REQ-RDR-020, REQ-RDR-021]
        fields {
            value : decimal min(1.0) max(5.0)
            totalRatings : int min(0)
        }
    }

    value AccessibilityPreference {
        satisfies [REQ-RDR-013]
        fields {
            wheelchairAccessible : boolean default(false)
            quietRide : boolean default(false)
            largerVehicle : boolean default(false)
        }
    }

    value Appeal {
        satisfies [REQ-RDR-025, REQ-RDR-026, REQ-RDR-027]
        fields {
            reason : string maxLength(2000)
            submittedAt : datetime
            status : AppealStatus default("pending")
        }
    }

    // -----------------------------------------------------------------
    // Entities
    // -----------------------------------------------------------------

    entity Rider :: "Rider lifecycle root — signup, profile, ratings, bans" {
        satisfies [REQ-RDR-001, REQ-RDR-002, REQ-RDR-003, REQ-RDR-004, REQ-RDR-005,
                   REQ-RDR-006, REQ-RDR-010, REQ-RDR-011, REQ-RDR-012, REQ-RDR-013,
                   REQ-RDR-020, REQ-RDR-021, REQ-RDR-022, REQ-RDR-023, REQ-RDR-024,
                   REQ-RDR-025, REQ-RDR-026, REQ-RDR-027]

        identifier riderId : UUID

        fields {
            phoneNumber : PhoneNumber
            phoneVerified : boolean default(false)
            firstName : string optional
            lastName : string optional
            email : string optional
            status : RiderStatus default("unverified")
            verificationCode : VerificationCode optional
            verificationAttempt : VerificationAttempt
            savedAddresses : list<SavedAddress>
            paymentMethods : list<PaymentMethodRef>
            accessibility : AccessibilityPreference
            averageRating : AverageRiderRating optional
            totalRides : int min(0) default(0)
            appeal : Appeal optional
            createdAt : datetime
            updatedAt : datetime
        }

        invariants {
            readyToRideRequiresVerifiedPhoneAndPayment :: "Active rider must have verified phone + active payment" enforcement reject {
                if status = active {
                    phoneVerified = true
                    and exists pm in paymentMethods where pm.isActive = true
                }
            }
            noGovernmentIdAtSignup :: "Government ID is not collected at signup" enforcement reject {
                if status = unverified { firstName is null and lastName is null }
            }
        }

        operations {
            "Sign up rider" on RegisterRider {
                satisfies [REQ-RDR-001, REQ-RDR-004]
                sets Rider {
                    phoneNumber = registerRider.phoneNumber
                    status = unverified
                    verificationCode = generateCode()
                    verificationAttempt = VerificationAttempt(failedCount = 0)
                    createdAt = now()
                    updatedAt = now()
                }
                emits RiderRegistered {
                    riderId = Rider.riderId
                    phoneNumber = Rider.phoneNumber
                    code = Rider.verificationCode
                    registeredAt = Rider.createdAt
                }
            }

            "Verify phone" on VerifyPhone {
                satisfies [REQ-RDR-002]
                precondition codeOutstanding { Rider.verificationCode is defined }
                precondition codeNotExpired { Rider.verificationCode.expiresAt > now() }
                precondition notLocked {
                    Rider.verificationAttempt.lockedUntil is null
                    or Rider.verificationAttempt.lockedUntil <= now()
                }
                precondition codeMatches { verifyPhone.submittedCode = Rider.verificationCode.code }

                sets Rider {
                    phoneVerified = true
                    verificationCode = null
                    verificationAttempt = VerificationAttempt(failedCount = 0)
                }
                emits RiderPhoneVerified {
                    riderId = Rider.riderId
                    verifiedAt = now()
                }
            }

            "Submit invalid verification code" on SubmitInvalidVerificationCode {
                satisfies [REQ-RDR-003]
                sets Rider {
                    verificationAttempt = VerificationAttempt(
                        failedCount = Rider.verificationAttempt.failedCount + 1,
                        lockedUntil = if Rider.verificationAttempt.failedCount + 1 >= 3 then now() + 15min else null
                    )
                }
                emits VerificationFailed {
                    riderId = Rider.riderId
                    failedCount = Rider.verificationAttempt.failedCount
                    lockedUntil = Rider.verificationAttempt.lockedUntil
                    failedAt = now()
                }
            }

            "Add payment method" on AddPaymentMethod {
                satisfies [REQ-RDR-006]
                precondition phoneVerified { Rider.phoneVerified = true }
                sets Rider {
                    paymentMethods = Rider.paymentMethods + addPaymentMethod.method
                    status = if Rider.status = unverified then active else Rider.status
                }
                emits PaymentMethodAdded {
                    riderId = Rider.riderId
                    method = addPaymentMethod.method
                    addedAt = now()
                }
            }

            "Save address" on SaveAddress {
                satisfies [REQ-RDR-010]
                sets Rider {
                    savedAddresses = Rider.savedAddresses + saveAddress.address
                }
                emits AddressSaved {
                    riderId = Rider.riderId
                    address = saveAddress.address
                    savedAt = now()
                }
            }

            "Set accessibility preference" on SetAccessibilityPreference {
                satisfies [REQ-RDR-013]
                sets Rider { accessibility = setAccessibilityPreference.accessibility }
                emits AccessibilityPreferenceUpdated {
                    riderId = Rider.riderId
                    preference = Rider.accessibility
                    updatedAt = now()
                }
            }

            "Record rider rating" on RecordRiderRating {
                satisfies [REQ-RDR-020, REQ-RDR-021]
                precondition validRange {
                    recordRiderRating.score.value >= 1 and recordRiderRating.score.value <= 5
                }
                sets Rider {
                    averageRating = recompute(Rider.averageRating, recordRiderRating.score)
                    totalRides = Rider.totalRides + 1
                }
                emits RiderRated {
                    riderId = Rider.riderId
                    rideId = recordRiderRating.rideId
                    score = recordRiderRating.score
                    comment = recordRiderRating.comment
                    newAverage = Rider.averageRating
                    totalRides = Rider.totalRides
                    submittedAt = now()
                }
            }

            "Issue low-rating warning" on WarnLowRating {
                satisfies [REQ-RDR-022]
                precondition matureRider { Rider.totalRides > 10 }
                precondition belowWarningThreshold { Rider.averageRating.value < 4.0 }
                emits LowRatingWarningIssued {
                    riderId = Rider.riderId
                    averageRating = Rider.averageRating
                    issuedAt = now()
                }
            }

            "Propose ban" on ProposeRiderBan {
                satisfies [REQ-RDR-023]
                precondition matureRider { Rider.totalRides > 10 }
                precondition belowBanThreshold { Rider.averageRating.value < 3.5 }
                emits RiderBanProposalCreated {
                    riderId = Rider.riderId
                    averageRating = Rider.averageRating
                    proposedAt = now()
                }
            }

            "Apply rider ban" on BanRider {
                satisfies [REQ-RDR-024, REQ-RDR-NFR-004]
                precondition operatorApproved { banRider.operatorId is defined }
                sets Rider { status = banned }
                emits RiderBanned {
                    riderId = Rider.riderId
                    operatorId = banRider.operatorId
                    reason = banRider.reason
                    bannedAt = now()
                }
            }

            "Submit ban appeal" on SubmitBanAppeal {
                satisfies [REQ-RDR-025]
                precondition isBanned { Rider.status = banned }
                precondition withinAppealWindow {
                    now() - banAppealWindowStart(Rider) <= 30days
                }
                sets Rider {
                    status = appealInReview
                    appeal = Appeal(reason = submitBanAppeal.reason, submittedAt = now(), status = pending)
                }
                emits BanAppealSubmitted {
                    riderId = Rider.riderId
                    submittedAt = now()
                }
            }

            "Reject late appeal" on RejectLateAppeal {
                satisfies [REQ-RDR-027]
                precondition isBanned { Rider.status = banned }
                precondition outsideWindow {
                    now() - banAppealWindowStart(Rider) > 30days
                }
                emits RiderBanAppealRejectedAsLate {
                    riderId = Rider.riderId
                    rejectedAt = now()
                }
            }

            "Resolve ban appeal" on ResolveBanAppeal {
                satisfies [REQ-RDR-026, REQ-RDR-NFR-004]
                precondition appealUnderReview { Rider.status = appealInReview }
                sets Rider {
                    status = if resolveBanAppeal.outcome = approved then active else permanentlyBanned
                    appeal = Appeal(
                        reason = Rider.appeal.reason,
                        submittedAt = Rider.appeal.submittedAt,
                        status = if resolveBanAppeal.outcome = approved then approved else rejected
                    )
                }
                emits BanAppealResolved {
                    riderId = Rider.riderId
                    operatorId = resolveBanAppeal.operatorId
                    outcome = resolveBanAppeal.outcome
                    resolvedAt = now()
                }
            }
        }
    }

    aggregate RiderAggregate root Rider

    statemachine RiderLifecycle on Rider {
        start unverified
        state unverified {}
        state active {
            invariant phoneVerified { Rider.phoneVerified = true }
        }
        state banned {}
        state appealInReview {}
        state permanentlyBanned {}

        transition unverified -> active on AddPaymentMethod                // first active payment method completes profile
        transition active -> banned on BanRider
        transition banned -> appealInReview on SubmitBanAppeal
        transition appealInReview -> active on ResolveBanAppeal             // outcome = approved
        transition appealInReview -> permanentlyBanned on ResolveBanAppeal  // outcome = rejected

        final permanentlyBanned
    }

    // -----------------------------------------------------------------
    // Domain services
    // -----------------------------------------------------------------

    service RiderRatingCalculator :: "Recomputes rider average rating" {
        satisfies [REQ-RDR-020]
        operations {
            recompute(prior: AverageRiderRating, newScore: RatingScore) : AverageRiderRating
        }
    }

    // -----------------------------------------------------------------
    // Infrastructure services
    // -----------------------------------------------------------------

    infrastructure-service VerificationCodeChannel :: "Telephony provider for SMS verification codes" {
        satisfies [REQ-RDR-001, REQ-RDR-NFR-003]
        operations {
            sendCode(phoneNumber: PhoneNumber, code: string) : void
        }
    }

    infrastructure-service PaymentValidationGateway :: "ACL to Payment for method validation" {
        satisfies [REQ-RDR-006]
        operations {
            validatePaymentMethod(paymentMethodId: uuid) : boolean
        }
    }

    infrastructure-service RiderNotificationService :: "Push, SMS, email to riders" {
        satisfies [REQ-RDR-022, REQ-RDR-024, REQ-RDR-026]
        operations {
            notifyRider(riderId: uuid, subject: string, message: string) : void
        }
    }

    // -----------------------------------------------------------------
    // Commands
    // -----------------------------------------------------------------

    command RegisterRider { fields { phoneNumber: PhoneNumber; correlationId: uuid } }
    command VerifyPhone { fields { riderId: uuid; submittedCode: string; correlationId: uuid } }
    command SubmitInvalidVerificationCode { fields { riderId: uuid; correlationId: uuid } }
    command AddPaymentMethod { fields { riderId: uuid; method: PaymentMethodRef; correlationId: uuid } }
    command SaveAddress { fields { riderId: uuid; address: SavedAddress; correlationId: uuid } }
    command SetAccessibilityPreference { fields { riderId: uuid; accessibility: AccessibilityPreference; correlationId: uuid } }
    command RecordRiderRating { fields { riderId: uuid; rideId: uuid; score: RatingScore; comment: string optional; correlationId: uuid } }
    command WarnLowRating { fields { riderId: uuid; correlationId: uuid } }
    command ProposeRiderBan { fields { riderId: uuid; correlationId: uuid } }
    command BanRider { fields { riderId: uuid; operatorId: uuid; reason: string; correlationId: uuid } }
    command SubmitBanAppeal { fields { riderId: uuid; reason: string; correlationId: uuid } }
    command RejectLateAppeal { fields { riderId: uuid; correlationId: uuid } }
    command ResolveBanAppeal { fields { riderId: uuid; operatorId: uuid; outcome: AppealStatus; correlationId: uuid } }

    // -----------------------------------------------------------------
    // Events
    // -----------------------------------------------------------------

    event RiderRegistered { satisfies [REQ-RDR-001] fields { riderId: uuid; phoneNumber: PhoneNumber; code: VerificationCode; registeredAt: datetime } }
    event RiderPhoneVerified { satisfies [REQ-RDR-002] fields { riderId: uuid; verifiedAt: datetime } }
    event VerificationFailed { satisfies [REQ-RDR-003] fields { riderId: uuid; failedCount: int; lockedUntil: datetime optional; failedAt: datetime } }
    event PaymentMethodAdded { satisfies [REQ-RDR-006] fields { riderId: uuid; method: PaymentMethodRef; addedAt: datetime } }
    event AddressSaved { satisfies [REQ-RDR-010] fields { riderId: uuid; address: SavedAddress; savedAt: datetime } }
    event AccessibilityPreferenceUpdated { satisfies [REQ-RDR-013] fields { riderId: uuid; preference: AccessibilityPreference; updatedAt: datetime } }
    event RiderRated { satisfies [REQ-RDR-020] fields { riderId: uuid; rideId: uuid; score: RatingScore; comment: string optional; newAverage: AverageRiderRating; totalRides: int; submittedAt: datetime } }
    event LowRatingWarningIssued { satisfies [REQ-RDR-022] fields { riderId: uuid; averageRating: AverageRiderRating; issuedAt: datetime } }
    event RiderBanProposalCreated { satisfies [REQ-RDR-023] fields { riderId: uuid; averageRating: AverageRiderRating; proposedAt: datetime } }
    event RiderBanned { satisfies [REQ-RDR-024, REQ-RDR-NFR-004] fields { riderId: uuid; operatorId: uuid; reason: string; bannedAt: datetime } }
    event BanAppealSubmitted { satisfies [REQ-RDR-025] fields { riderId: uuid; submittedAt: datetime } }
    event RiderBanAppealRejectedAsLate { satisfies [REQ-RDR-027] fields { riderId: uuid; rejectedAt: datetime } }
    event BanAppealResolved { satisfies [REQ-RDR-026, REQ-RDR-NFR-004] fields { riderId: uuid; operatorId: uuid; outcome: AppealStatus; resolvedAt: datetime } }

    // -----------------------------------------------------------------
    // External events (consumed)
    // -----------------------------------------------------------------

    external-event RideCompleted from RideManagement
    external-event PaymentMethodValidated from Payment

    // -----------------------------------------------------------------
    // Reactions
    // -----------------------------------------------------------------

    reaction sendCodeOnRegistration :: "Dispatch SMS code immediately on signup" {
        satisfies [REQ-RDR-001]
        trigger RiderRegistered
        effect VerificationCodeChannel.sendCode(phoneNumber = event.phoneNumber, code = event.code.code)
    }

    reaction warnRiderOnLowRating :: "Warn at avg < 4.0 with 10+ rides" {
        satisfies [REQ-RDR-022]
        trigger RiderRated
        guard event.totalRides > 10 and event.newAverage.value < 4.0 and event.newAverage.value >= 3.5
        effect WarnLowRating(riderId = event.riderId)
    }

    reaction proposeBanOnVeryLowRating :: "Queue ban proposal at avg < 3.5 with 10+ rides" {
        satisfies [REQ-RDR-023]
        trigger RiderRated
        guard event.totalRides > 10 and event.newAverage.value < 3.5
        effect ProposeRiderBan(riderId = event.riderId)
    }

    reaction recordRatingOnRideCompleted :: "Bridge from RideManagement — record driver rating of rider" {
        satisfies [REQ-RDR-020]
        trigger RideCompleted
        guard event.riderRating is defined
        effect RecordRiderRating(
            riderId = event.riderId,
            rideId = event.rideId,
            score = event.riderRating.score,
            comment = event.riderRating.comment
        )
    }

    reaction notifyOnBan :: "Notify rider of ban with reason and appeal path" {
        satisfies [REQ-RDR-024]
        trigger RiderBanned
        effect RiderNotificationService.notifyRider(
            riderId = event.riderId,
            subject = "Account banned",
            message = event.reason + " You may appeal within 30 days."
        )
    }

    reaction notifyOnAppealResolution :: "Notify rider of appeal outcome" {
        satisfies [REQ-RDR-026]
        trigger BanAppealResolved
        effect RiderNotificationService.notifyRider(
            riderId = event.riderId,
            subject = "Appeal resolved",
            message = "Outcome: " + event.outcome
        )
    }
}