Allmine API
Live Stream

Live Stream replay rental — backend

Replay kiralama backend kontratı

Live Stream Replay Rental - Backend

Endpointler

  • GET /api/v1/live-stream/:id/replay-status
  • POST /api/v1/live-stream/:id/replay-rentals
  • GET /api/v1/live-stream/my-replay-rentals
  • Auth: @ApiAuth(Role.USER)

Amaç

Bitmiş canlı yayın replay kayıtlarını 24 saatliğine kiralatır. Kiralayan kullanıcı süre boyunca recordingUrl görebilir. Süre dolduğunda kiralama kaydı listede kalabilir, ancak recordingUrl response'ta maskelenir.

Owner, broadcaster ve guest kendi yayın replay'ini ödeme yapmadan izleyebilir.

Kiralanabilirlik Kuralı

Bir replay sadece aşağıdaki koşulların tamamı sağlandığında kiralanabilir:

  • LiveStream.status === ENDED
  • yayın soft delete edilmemiş
  • recordingUrl boş değil
  • isActiveReplayOnCreatorProfile === true
  • replayCreditPrice > 0
  • DUOCROWD / DUOSELF için gelir paylaşımı yapılabilecek guest bulunuyor

Bu kurallar LiveStreamReplayAccessService.resolveUnavailableReason içinde merkezi olarak tutulur.

Dosya Haritası

  • Controller: src/live-stream/live-stream.controller.ts
  • Module wiring: src/live-stream/live-stream.module.ts
  • Rental schema: src/live-stream/schemas/live-stream-replay-rental.schema.ts
  • Billing transaction enum: src/live-stream/schemas/stream-billing-transaction.schema.ts
  • Repository: src/live-stream/repository/live-stream-replay-rental.repository.ts
  • Access resolver: src/live-stream/services/live-stream-replay-access.service.ts
  • Refund service: src/live-stream/services/live-stream-replay-rental-refund.service.ts
  • Status use-case: src/live-stream/use-cases/get-live-stream-replay-status.usecase.ts
  • Rent use-case: src/live-stream/use-cases/rent-live-stream-replay.usecase.ts
  • My rentals use-case: src/live-stream/use-cases/get-my-replay-rentals.usecase.ts
  • Settlement use-case: src/live-stream/use-cases/settle-live-stream-replay-rentals.usecase.ts
  • Soft delete integration: src/live-stream/use-cases/soft-delete-live-stream.usecase.ts
  • Cron: src/live-stream/cron/live-stream-cron.service.ts
  • Mapper masking: src/live-stream/mappers/live-stream.mapper.ts
  • User transaction history: src/users/use-cases/get-my-transaction-history.use-case.ts
  • Stream transaction history: src/live-stream/use-cases/get-live-stream-transactions.usecase.ts

Veri Modeli

Collection: LiveStreamReplayRental

Alanlar:

  • liveStream: kiralanan yayın id
  • user: kiralayan kullanıcı id
  • status: active | expired | refunded
  • rentedAt: kiralama başlangıcı
  • expiresAt: kiralama bitişi
  • pricePaid: kullanıcının ödediği replay kredi tutarı
  • chargePreviousBalance: ücret kesimi öncesi kullanıcı bakiyesi
  • chargeNewBalance: ücret kesimi sonrası kullanıcı bakiyesi
  • refundPreviousBalance: iade öncesi kullanıcı bakiyesi
  • refundNewBalance: iade sonrası kullanıcı bakiyesi
  • settlementStatus: pending | distributed | refunded
  • settledAt: gelir dağıtımı zamanı
  • refundedAt: iade zamanı
  • createdAt, updatedAt

Indexler:

{ liveStream: 1, user: 1 }, unique: true
{ user: 1, status: 1, expiresAt: -1 }
{ liveStream: 1, status: 1, expiresAt: 1 }
{ status: 1, settlementStatus: 1, expiresAt: 1 }

Önemli kural: Aynı user + liveStream için tek rental access kaydı tutulur. Finansal audit, StreamBillingTransaction kayıtlarında kalır.

Status ve Settlement Status

Rental status:

  • active: erişim aktif veya cron tarafından henüz expired yapılmamış pending kayıt
  • expired: 24 saat dolmuş ve gelir dağıtımı yapılmış
  • refunded: kullanıcıya iade yapılmış

Settlement status:

  • pending: ücret kesilmiş, gelir dağıtımı/iade bekliyor
  • distributed: gelir creator/guest'e dağıtılmış
  • refunded: kullanıcıya iade yapılmış

Access response status:

  • owner
  • active
  • expired
  • refunded
  • not_rented
  • unavailable

Controller Sözleşmesi

@Get('my-replay-rentals')
@ApiAuth(Role.USER)
async getMyReplayRentals(
  @CurrentUser() user: JwtPayload,
  @Query() query: ListMyReplayRentalsQueryDto,
): Promise<PaginatedResponseDto<LiveStreamReplayRentalResponseDto>>

@Get(':id/replay-status')
@ApiAuth(Role.USER)
async getLiveStreamReplayStatus(
  @CurrentUser() user: JwtPayload,
  @Param('id') id: string,
): Promise<LiveStreamReplayStatusResponseDto>

@Post(':id/replay-rentals')
@ApiAuth(Role.USER)
async rentLiveStreamReplay(
  @CurrentUser() user: JwtPayload,
  @Param('id') id: string,
): Promise<LiveStreamReplayStatusResponseDto>

POST /:id/replay-rentals body almaz.

Response DTO

LiveStreamReplayStatusResponseDto:

type LiveStreamReplayStatusResponseDto = {
  streamId: string;
  streamStatus: LiveStreamStatus;
  isDeleted: boolean;
  isReplayAvailable: boolean;
  unavailableReason: ReplayUnavailableReason | null;
  price: number | null;
  rentalDurationHours: 24;
  rentalStatus: 'owner' | 'active' | 'expired' | 'refunded' | 'not_rented' | 'unavailable';
  canRent: boolean;
  canViewRecording: boolean;
  rentedAt: string | null;
  expiresAt: string | null;
  remainingSeconds: number | null;
  title: string;
  thumbnailUrl: string | null;
  creator: UserSummaryDto | null;
  liveStreamType: LiveStreamType;
  endedAt: string | null;
  recordingUrl: string | null;
};

recordingUrl sadece canViewRecording === true ise dolu gelir.

LiveStreamReplayRentalResponseDto:

type LiveStreamReplayRentalResponseDto = {
  id: string;
  streamId: string;
  replayAccess: LiveStreamReplayAccessDto;
  pricePaid: number;
  stream: LiveStreamResponseDto;
  createdAt: string;
  updatedAt: string;
};

stream alanı custom kısa replay objesi değildir. Doğrudan LiveStreamResponseDto shape'i ile döner. Bu response içinde de replayAccess alanı bulunur ve recordingUrl aynı access resolver sonucuna göre maskelenir.

Unavailable reason değerleri:

  • stream_deleted
  • stream_not_ended
  • recording_unavailable
  • replay_disabled
  • replay_price_missing
  • duo_guest_missing

Access Resolver

LiveStreamReplayAccessService replay erişim kararının tek kaynağıdır.

Sorumlulukları:

  • Kiralama uygunluğunu belirlemek
  • Owner/broadcaster/guest ücretsiz erişimini belirlemek
  • Aktif/expired/refunded rental status normalize etmek
  • recordingUrl gösterilip gösterilmeyeceğine karar vermek
  • LiveStreamResponseDto.replayAccess alanını üretmek
  • DUO gelir alıcısı guest'i çözmek

LiveStreamMapper.mapToResponseDto artık default olarak recordingUrl döndürmez. URL döndürmek isteyen akışlar includeRecordingUrl: true opsiyonu ile mapper'a gider. Bu karar LiveStreamReplayAccessService.mapToResponseDto içinde verilir.

Kiralama Akışı

Use-case: RentLiveStreamReplayUseCase

Akış:

  1. streamId ve userId ObjectId validasyonu yapılır.
  2. Yayın populate edilmiş şekilde okunur.
  3. LiveStreamReplayAccessService.buildStatus ile mevcut erişim hesaplanır.
  4. Kullanıcı owner veya active ise ücret kesilmeden mevcut status döner.
  5. canRent === false ise 400 döner.
  6. replayCreditPrice okunur.
  7. Mongo transaction başlar.
  8. Aynı user/stream rental kaydı transaction içinde tekrar okunur.
  9. Transaction sırasında aktif ve süresi devam eden rental varsa ücret kesilmez.
  10. Eski rental active + pending ama süresi dolmuşsa rerent öncesi eski pencerenin geliri dağıtılır.
  11. Kullanıcı bakiyesi atomik olarak replayCreditPrice kadar düşülür.
  12. Rental yoksa yeni kayıt oluşturulur.
  13. Rental varsa aynı kayıt tekrar active yapılır.
  14. StreamBillingTransaction içine replay_rental_charge yazılır.
  15. Transaction commit sonrası kullanıcıya balance socket event gönderilir.
  16. Yeni replay status döner.

Reactivation alanları:

  • status = active
  • settlementStatus = pending
  • rentedAt = now
  • expiresAt = now + 24 saat
  • pricePaid = replayCreditPrice
  • settledAt = null
  • refundedAt = null

My Replay Rentals Akışı

Use-case: GetMyReplayRentalsUseCase

Query:

  • page: default 1, max use-case içinde normalize edilir
  • limit: default 20, max 100
  • status: active | expired | refunded

Repository status filtresi:

  • active: status = active ve expiresAt > now
  • expired: status = expired veya status = active ve expiresAt <= now
  • refunded: status = refunded

Her item için:

  • rental row bilgisi döner
  • populated liveStream, LiveStreamResponseDto olarak map edilir
  • replayAccess hesaplanır
  • aktif erişim yoksa nested stream.recordingUrl null kalır

Mapping yapısı:

const liveStream = rental.liveStream as LiveStreamDocument;

const replayStatus = replayAccessService.buildStatusFromRental(
  liveStream,
  rental,
  userId,
);

const stream = await replayAccessService.mapToResponseDto(
  liveStream,
  userId,
);

return {
  id: rental._id.toString(),
  streamId: liveStream._id.toString(),
  replayAccess: replayAccessService.toAccessDto(replayStatus),
  pricePaid: Number(rental.pricePaid ?? 0),
  stream,
  createdAt: rental.createdAt.toISOString(),
  updatedAt: rental.updatedAt.toISOString(),
};

replayAccessService.mapToResponseDto iç mapping:

const replayStatus = await replayAccessService.buildStatus(liveStream, userId);

return LiveStreamMapper.mapToResponseDto(liveStream, userId, {
  includeRecordingUrl: replayStatus.canViewRecording,
  replayAccess: replayAccessService.toAccessDto(replayStatus),
});

Bu nedenle LiveStreamReplayRentalResponseDto.stream içinde beklenen shape, mevcut LiveStreamResponseDto ile aynıdır:

type LiveStreamResponseDto = {
  id: string;
  title: string;
  liveStreamType: LiveStreamType;
  channelName: string;
  broadcasters: LiveStreamBroadcasterResponseDto[];
  guests: UserSummaryDto[];
  creator: UserSummaryDto | null;
  thumbnailUrl: string | null;
  recording: boolean;
  recordingUrl: string | null;
  replayAccess: LiveStreamReplayAccessDto | null;
  status: LiveStreamStatus;
  accessType: LiveStreamAccessType;
  price: number;
  interest: string | null;
  durationGoal: number | null;
  motivation: string | null;
  isActiveReplayOnCreatorProfile: boolean | null;
  replayCreditPrice: number | null;
  startedAt: string | null;
  plannedStartDate: string | null;
  endedAt: string | null;
  plannedEndDate: string | null;
  createdAt: string;
  updatedAt: string;
  role: LiveStreamUserRole | null;
  fundingGoal: number | null;
  collectedFunding: number | null;
  fundingPercentage: number | null;
  miniCrowdFundings: MiniCrowdFundingResponseDto[];
};

Settlement Cron

Cron: LiveStreamCronService.handleReplayRentalSettlements

Schedule:

@Cron('0 * * * * *') // her dakika

Use-case: SettleLiveStreamReplayRentalsUseCase

Batch:

  • status = active
  • settlementStatus = pending
  • expiresAt <= now
  • limit = 100

Akış:

  1. Settlement bekleyen expired rental kayıtları alınır.
  2. Her rental ayrı transaction içinde tekrar okunur.
  3. Kayıt hala active + pending değilse skip edilir.
  4. Yayın findByIdWithDeleted ile okunur.
  5. Yayın rental window içinde silinmişse refund yapılır.
  6. Yayın silinmemişse gelir dağıtılır.
  7. Rental status = expired, settlementStatus = distributed, settledAt = now yapılır.
  8. Commit sonrası balance socket event'leri gönderilir.

Gelir dağıtımı:

  • SOLO: creator pricePaid * 0.8
  • DUOCROWD: creator pricePaid * 0.4, guest pricePaid * 0.4
  • DUOSELF: creator pricePaid * 0.4, guest pricePaid * 0.4

Platform %20 pay için kullanıcı bakiyesi artırılmaz.

Soft Delete Refund Akışı

Use-case: SoftDeleteLiveStreamUseCase

Yayın soft delete edilirken aynı transaction içinde aktif replay kiralamaları iade edilir.

Refund kapsamı:

  • liveStream = deleted stream
  • status = active
  • settlementStatus = pending
  • expiresAt > now

Refund davranışı:

  • Kiralayan kullanıcıya pricePaid kadar kredi iade edilir.
  • Rental status = refunded yapılır.
  • Rental settlementStatus = refunded yapılır.
  • refundedAt, refundPreviousBalance, refundNewBalance yazılır.
  • StreamBillingTransaction içine replay_rental_refund yazılır.
  • Commit sonrası balance socket event gönderilir.

Süresi dolmuş ve dağıtılmış kiralamalar iade edilmez.

Billing Transaction Tipleri

BillingTransactionType yeni değerleri:

REPLAY_RENTAL_CHARGE = 'replay_rental_charge'
REPLAY_RENTAL_REVENUE = 'replay_rental_revenue'
REPLAY_RENTAL_REFUND = 'replay_rental_refund'

Kullanım:

  • replay_rental_charge: kiralayan kullanıcıdan kredi düşümü
  • replay_rental_revenue: creator/guest gelir yazımı
  • replay_rental_refund: kiralayan kullanıcıya iade

Bu tipler user transaction history ve stream transaction history aggregation'larına eklendi.

Balance Socket Event Tipleri

BalanceTransactionType yeni değerleri:

REPLAY_RENTAL_CHARGE
REPLAY_RENTAL_REVENUE
REPLAY_RENTAL_REFUND

Event metadata:

{
  streamId: string;
  rentalId: string | null;
  amount: number;
}

Etkilenen Liste/Detail Endpointleri

Aşağıdaki use-case'ler LiveStreamReplayAccessService.mapToResponseDto kullanacak şekilde güncellendi:

  • GetLiveStreamByIdUseCase
  • GetPastStreamsUseCase
  • GetPastStreamsV2UseCase
  • GetUserPastLiveStreamsUseCase
  • GetUserPastLiveStreamsV2UseCase
  • GetMyPastLiveStreamsUseCase
  • GetMyPastLiveStreamsV2UseCase
  • GetMyBroadcastedLiveStreamsUseCase
  • GetMyWatchedLiveStreamsUseCase

Amaç:

  • response içinde replayAccess dönmek
  • recordingUrl sızıntısını engellemek
  • kiralama süresi dolduğunda URL'yi otomatik null yapmak

Hata Durumları

Status endpoint:

  • invalid ObjectId: 400
  • yayın yok: 404

Rent endpoint:

  • invalid ObjectId: 400
  • yayın yok: 404
  • replay uygun değil: 400
  • fiyat yok: 400
  • bakiye yetersiz: 400

My rentals endpoint:

  • invalid user id: 400
  • invalid status query: validation error

Test Kapsamı

Eklenen test dosyaları:

  • src/live-stream/services/live-stream-replay-access.service.spec.ts
  • src/live-stream/use-cases/rent-live-stream-replay.usecase.spec.ts
  • src/live-stream/use-cases/settle-live-stream-replay-rentals.usecase.spec.ts

Kapsanan ana senaryolar:

  • kiralamayan kullanıcı için recordingUrl = null
  • aktif rental için recordingUrl döner
  • owner ödeme yapmadan replay görebilir
  • aktif rental varken tekrar ücret kesilmez
  • expired rental aynı kayıt üzerinden tekrar active yapılır
  • replay uygun değilse rent reddedilir
  • rerent öncesi eski pending pencere settlement yapılır
  • SOLO revenue %80 creator'a gider
  • DUO revenue %40 + %40 creator/guest'e gider
  • stream rental window içinde silinmişse refund yapılır

Çalıştırılan doğrulama:

npm run build
npm test -- --runInBand src/live-stream/services/live-stream-replay-access.service.spec.ts src/live-stream/use-cases/rent-live-stream-replay.usecase.spec.ts src/live-stream/use-cases/settle-live-stream-replay-rentals.usecase.spec.ts

On this page