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
permanentlyBannedvalue 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: Stringnumber: 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: DateTimeexpiresAt: 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. WhenfailedCount >= 3,lockedUntilmust be defined.
- Realizes: REQ-RDR-003.
SavedAddress
A rider-named place — Home, Work, custom.
- Fields:
label: String —maxLength(50)street: Stringcity: StringzipCode: Stringcountry: Stringlatitude: Decimallongitude: 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: UUIDtype:PaymentMethodTypelabel: StringisActive: Boolean — defaultfalseaddedAt: DateTime
- Realizes: REQ-RDR-006. The
isActiveflag flips totrueonly 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 — defaultfalsequietRide: Boolean — defaultfalselargerVehicle: Boolean — defaultfalse
- Realizes: REQ-RDR-013.
Appeal
Captured ban appeal — text, timestamp, current status.
- Fields:
reason: String —maxLength(2000)submittedAt: DateTimestatus: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:PhoneNumberphoneVerified: Boolean — defaultfalsefirstName: String optionallastName: String optionalemail: String optionalstatus:RiderStatus— default"unverified"verificationCode:VerificationCodeoptionalverificationAttempt:VerificationAttemptsavedAddresses:list<SavedAddress>paymentMethods:list<PaymentMethodRef>accessibility:AccessibilityPreferenceaverageRating:AverageRiderRatingoptionaltotalRides: Integer —min(0), default0appeal:AppealoptionalcreatedAt: DateTimeupdatedAt: DateTime
- Invariants:
readyToRideRequiresVerifiedPhoneAndPayment:: “Active rider must have verified phone + active payment” — enforcement: reject. Whenstatus = active,phoneVerified = trueand there exists aPaymentMethodRefwithisActive = true.noGovernmentIdAtSignup:: “Government ID is not collected at signup” — enforcement: reject. Whenstatus = unverified,firstNameandlastNameare 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 rideron commandRegisterRider- Realizes: REQ-RDR-001, REQ-RDR-004.
- State changes (
sets):phoneNumberfrom the command;status = unverified;verificationCode = generateCode();verificationAttempt = VerificationAttempt(failedCount = 0);createdAt = now();updatedAt = now(). - Emits:
RiderRegisteredwithriderId,phoneNumber,code,registeredAt.
-
Verify phoneon commandVerifyPhone- Realizes: REQ-RDR-002.
- Preconditions:
codeOutstanding:Rider.verificationCode is defined.codeNotExpired:Rider.verificationCode.expiresAt > now().notLocked:Rider.verificationAttempt.lockedUntilis null or already past.codeMatches: submitted code equals stored code.
- State changes:
phoneVerified = true;verificationCode = null;verificationAttempt = VerificationAttempt(failedCount = 0). - Emits:
RiderPhoneVerifiedwithriderId,verifiedAt = now().
-
Submit invalid verification codeon commandSubmitInvalidVerificationCode- Realizes: REQ-RDR-003.
- State changes: increment
verificationAttempt.failedCountby one; if the new count is>= 3, setlockedUntil = now() + 15min, otherwisenull. - Emits:
VerificationFailedwithriderId,failedCount,lockedUntil,failedAt.
-
Add payment methodon commandAddPaymentMethod- Realizes: REQ-RDR-006.
- Precondition
phoneVerified:Rider.phoneVerified = true. - State changes: append the method to
paymentMethods; ifRider.status = unverified, advance toactive. - Emits:
PaymentMethodAddedwithriderId,method,addedAt.
-
Save addresson commandSaveAddress- Realizes: REQ-RDR-010.
- State change: append the address to
savedAddresses. - Emits:
AddressSavedwithriderId,address,savedAt.
-
Set accessibility preferenceon commandSetAccessibilityPreference- Realizes: REQ-RDR-013.
- State change: replace
accessibilitywith the value from the command. - Emits:
AccessibilityPreferenceUpdatedwithriderId,preference,updatedAt.
-
Record rider ratingon commandRecordRiderRating- Realizes: REQ-RDR-020, REQ-RDR-021.
- Precondition
validRange:score.valuein[1, 5]. - State changes:
averageRating = recompute(prior, newScore);totalRides = totalRides + 1. - Postcondition (implicit): the new average reflects the previous total plus one.
- Emits:
RiderRatedwithriderId,rideId,score,comment,newAverage,totalRides,submittedAt.
-
Issue low-rating warningon commandWarnLowRating- Realizes: REQ-RDR-022.
- Preconditions:
matureRider:Rider.totalRides > 10.belowWarningThreshold:Rider.averageRating.value < 4.0.
- Emits:
LowRatingWarningIssuedwithriderId,averageRating,issuedAt.
-
Propose banon commandProposeRiderBan- Realizes: REQ-RDR-023.
- Preconditions:
matureRider:Rider.totalRides > 10.belowBanThreshold:Rider.averageRating.value < 3.5.
- Emits:
RiderBanProposalCreatedwithriderId,averageRating,proposedAt.
-
Apply rider banon commandBanRider- Realizes: REQ-RDR-024, REQ-RDR-NFR-004.
- Precondition
operatorApproved:banRider.operatorId is defined. - State change:
status = banned. - Emits:
RiderBannedwithriderId,operatorId,reason,bannedAt.
-
Submit ban appealon commandSubmitBanAppeal- 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:
BanAppealSubmittedwithriderId,submittedAt.
-
Reject late appealon commandRejectLateAppeal- Realizes: REQ-RDR-027.
- Preconditions:
isBanned:Rider.status = banned.outsideWindow:now() - banAppealWindowStart(Rider) > 30days.
- Emits:
RiderBanAppealRejectedAsLatewithriderId,rejectedAt.
-
Resolve ban appealon commandResolveBanAppeal- Realizes: REQ-RDR-026, REQ-RDR-NFR-004.
- Precondition
appealUnderReview:Rider.status = appealInReview. - State changes: when outcome is
approved,status = activeandappeal.status = approved; otherwisestatus = permanentlyBannedandappeal.status = rejected. - Emits:
BanAppealResolvedwithriderId,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 ofverificationAttemptandaverageRatingrecomputation.
State Machines
RiderLifecycle on Rider
- Start state:
unverified. - States:
unverified— rider profile created, code dispatched, no active payment method yet.active— state-scoped invariantphoneVerified: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:
unverified→activeon commandAddPaymentMethod(the first active payment method completes the profile).active→bannedon commandBanRider.banned→appealInReviewon commandSubmitBanAppeal.appealInReview→activeon commandResolveBanAppeal(outcome = approved).appealInReview→permanentlyBannedon commandResolveBanAppeal(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— incrementstotalRatings, 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 asisActive = trueonly 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 AddPaymentMethod → PaymentValidationGateway.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(fromRideManagement). - 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
riderRatingpayload; 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
)
}
}