Source: src/data/entitlement/
Service
createEntitlementService / EntitlementService
Factory: createEntitlementService(deps: EntitlementServiceDeps) — returns EntitlementService.
Manages Discord entitlements and digital item grants for orders. Supports three Discord fulfillment
types — role grants, emoji-role grants, and private channel permission overrides. Calls the Discord
REST API to grant or revoke each type. Use DISCORD_API_BASE_URL env var to override the base URL
(for testing).
| Method | Parameters | Returns | Description |
|---|
| generateEntitlementsForOrder | (ctx: RequestContext, orderId: string) | Promise<EntitlementRecord[]> | Creates PENDING entitlement records for each product line item; idempotent |
| processEntitlements | (ctx: RequestContext, orderId: string) | Promise<void> | Processes PENDING entitlements — grants Discord roles via REST; marks GRANTED or FAILED |
| revokeEntitlementsForOrder | (ctx: RequestContext, orderId: string) | Promise<void> | Revokes GRANTED entitlements; removes Discord roles best-effort; marks REVOKED |
| retryEntitlement | (ctx: RequestContext, entitlementId: string) | Promise<EntitlementRecord> | Retries a single FAILED entitlement; no-op if not FAILED |
EntitlementServiceDeps
| Field | Type | Description |
|---|
| entitlementRepository | EntitlementRepository | Persistence layer |
| getOrder | (ctx, orderId) => Promise<EntitlementOrder | null> | Order lookup |
| getProduct | (ctx, productId) => Promise<EntitlementProduct | null> | Product lookup (reads grantedEntitlements; falls back to grantedRoleIds) |
| guildId | string | Discord guild ID for role management |
| botToken | string | Discord bot token |
Event Subscriptions (not yet wired)
payment.confirmed → generateEntitlementsForOrder + processEntitlements
order.refunded → revokeEntitlementsForOrder
Repository
EntitlementRepository
| Method | Parameters | Returns |
|---|
| getById | (ctx: RequestContext, id: string) | Promise<EntitlementRecord | null> |
| listByTenant | (ctx: RequestContext) | Promise<EntitlementRecord[]> |
| listByOrder | (ctx: RequestContext, orderId: string) | Promise<EntitlementRecord[]> |
| create | (ctx: RequestContext, record: Omit<EntitlementRecord, 'createdAt' | 'updatedAt'>) | Promise<EntitlementRecord> |
| update | (ctx: RequestContext, id: string, patch: Partial<...>) | Promise<EntitlementRecord> |
| delete | (ctx: RequestContext, id: string) | Promise<void> |
EntitlementRecord
| Field | Type |
|---|
| id | string |
| tenantId | string |
| customerId | string |
| orderId | string |
| productId | string |
| type | 'DISCORD_ROLE' | 'DISCORD_EMOJI' | 'CHANNEL_ACCESS' | 'DIGITAL_ITEM' |
| targetId | string (optional — Discord role ID, channel ID, or item key) |
| status | 'PENDING' | 'GRANTED' | 'REVOKED' | 'FAILED' |
| grantedAt | Date (optional) |
| revokedAt | Date (optional) |
| failedReason | string (optional) |
| createdAt | Date |
| updatedAt | Date |
EntitlementType
| Value | Discord REST call | Admin label |
|---|
DISCORD_ROLE | PUT /guilds/{guildId}/members/{userId}/roles/{targetId} | Discord Role |
DISCORD_EMOJI | Same as DISCORD_ROLE — emoji access is role-gated | Custom Emoji Access |
CHANNEL_ACCESS | PUT /channels/{targetId}/permissions/{userId} with allow: "3072" (VIEW + SEND) | Private Channel Access |
DIGITAL_ITEM | No Discord REST call | Digital Item |
Validators
entitlementTypeSchema
Enum: DISCORD_ROLE, DISCORD_EMOJI, CHANNEL_ACCESS, DIGITAL_ITEM
entitlementStatusSchema
Enum: PENDING, GRANTED, REVOKED, FAILED
entitlementSchema
| Field | Type | Required | Default |
|---|
| tenantId | string | Yes | — |
| userId | string | Yes | — |
| orderId | string | Yes | — |
| productId | string | Yes | — |
| type | entitlementTypeSchema | Yes | — |
| roleId | string | No | — |
| status | entitlementStatusSchema | No | 'PENDING' |
| metadata | Record<string, unknown> | No | {} |
| grantedAt | Date (coerced) | No | — |
| createdAt | Date (coerced) | Yes | — |
| updatedAt | Date (coerced) | Yes | — |
Inferred Types
Entitlement — z.infer<typeof entitlementSchema>
EntitlementType — z.infer<typeof entitlementTypeSchema>
EntitlementStatus — z.infer<typeof entitlementStatusSchema>