Talent Unlock and Quota Enforcement
In the YouTeacher profile service, teacher (talent) profiles are searchable but their contact details are gated. A hiring-side viewer — a verified employer or an approved recruiter — spends unlock quota to reveal one teacher's contact details. This node describes that gate: the domain rules that decide whether an unlock is allowed, and the flow that records it.
The three domain rules
The decision lives in a single domain service, UnlockService, which validates a viewer against a talent in a fixed order and returns the first failing rule (as a coded result, not an exception). The two viewer types run parallel checks:
- Standing — an employer must be verified; a recruiter must be approved. A viewer who is neither cannot unlock anyone (
employer_not_verified/recruiter_not_approved). - Visibility — the talent must be visible to that audience. Employers are checked against school-facing visibility; recruiters against recruiter-facing visibility. A hidden talent cannot be unlocked (
talent_not_visible). - Quota — the viewer must have remaining unlock quota. When exhausted, the unlock is refused (
quota_exceeded).
Only when all three pass does the service report canUnlock: true.
Quota as a value object
Quota is modelled as an immutable value object holding a total and a used count, with remaining derived as total − used (it is never stored separately — a single source of truth). Its invariants are enforced in the constructor: neither count may be negative, and used may never exceed total. Consuming quota does not mutate the object; it returns a new Quota with used incremented, and refuses when there is nothing left to consume. A separate adjust operation resets the total or used count. Because remaining is always computed, the object can never drift into an inconsistent "remaining" value.
The unlock flow
The employer and recruiter application handlers are near-identical. Each execute(userId, talentId) runs the same sequence:
- Resolve the viewer's profile from the user id; if there is none, the request fails as not-found.
- Resolve the talent by id; if there is none, not-found.
- Idempotency check — look up an existing unlock for this (talent, viewer, viewer-type). If one already exists, return it unchanged. A viewer is never charged twice for the same teacher.
- Run the three domain rules. A failure surfaces as a forbidden response carrying the rule's message.
- Decrement the viewer's quota.
- Record the unlock and return it.
An unlock record is a small fact: an id, the talent id, the viewer id, the viewer type (employer or recruiter), and a timestamp.
Persistence and race safety
Unlock records live in their own table, keyed uniquely by the (talent, viewer, viewer-type) triple. The write uses an upsert against that unique key, so two concurrent unlock requests for the same pairing converge on one record rather than creating duplicates. The repository also supports transferring a viewer's unlocks to another viewer — skipping any the target already holds before removing them from the source.
Notes and seams
- The idempotency check and the quota decrement mean a repeat unlock of an already-revealed teacher is free; the quota is spent once, at first reveal.
- The quota decrement and the unlock record are two separate persistence steps in the handler; the upsert protects the record itself from duplication, and the idempotency check guards against re-charging.
- Standing (verification / approval) is decided upstream — the profile service only reads the viewer's current status when applying the rule.
Inside this component
- dual-auth-service-jwt-user-session — how an unlock request is authenticated as a real user
- architecture-ddd-cqrs-wiring — how the domain service, handlers, and Prisma repositories are wired