Enhance payment processing and rental features

- Updated the BTCPay service to support internal Lightning invoices with private route hints, improving payment routing for users with private channels.
- Added reconciliation methods for pending rents and subscriptions to ensure missed payments are processed on startup.
- Enhanced the rental and subscription services to handle payments in satoshis, aligning with Lightning Network standards.
- Improved the rental modal and content detail components to display rental status and pricing more clearly, including a countdown for rental expiration.
- Refactored various components to streamline user experience and ensure accurate rental access checks.
This commit is contained in:
Dorian
2026-02-12 23:24:25 +00:00
parent cdd24a5def
commit 0da83f461c
39 changed files with 1182 additions and 270 deletions

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { CreateSubscriptionDTO } from './dto/create-subscription.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Subscription } from './entities/subscription.entity';
import { In, IsNull, Not, Repository } from 'typeorm';
import { In, IsNull, MoreThanOrEqual, Not, Repository } from 'typeorm';
import { UsersService } from 'src/users/users.service';
import { User } from 'src/users/entities/user.entity';
import { randomUUID } from 'node:crypto';
@@ -13,7 +13,7 @@ import { AdminCreateSubscriptionDTO } from './dto/admin-create-subscription.dto'
import { BTCPayService } from 'src/payment/providers/services/btcpay.service';
/**
* Subscription pricing in USD.
* Subscription pricing in satoshis.
* Since Lightning doesn't support recurring billing,
* each period is a one-time payment that activates the subscription.
*/
@@ -21,11 +21,11 @@ const SUBSCRIPTION_PRICES: Record<
string,
{ monthly: number; yearly: number }
> = {
enthusiast: { monthly: 9.99, yearly: 99.99 },
'film-buff': { monthly: 19.99, yearly: 199.99 },
cinephile: { monthly: 29.99, yearly: 299.99 },
'rss-addon': { monthly: 4.99, yearly: 49.99 },
'verification-addon': { monthly: 2.99, yearly: 29.99 },
enthusiast: { monthly: 10_000, yearly: 100_000 },
'film-buff': { monthly: 21_000, yearly: 210_000 },
cinephile: { monthly: 42_000, yearly: 420_000 },
'rss-addon': { monthly: 5_000, yearly: 50_000 },
'verification-addon': { monthly: 3_000, yearly: 30_000 },
};
@Injectable()
@@ -142,6 +142,49 @@ export class SubscriptionsService {
return now;
}
/**
* Reconcile pending subscriptions against BTCPay on startup.
*/
async reconcilePendingPayments(): Promise<number> {
const cutoff = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
const pendingSubs = await this.subscriptionsRepository.find({
where: {
status: 'created' as any,
stripeId: Not(IsNull()),
createdAt: MoreThanOrEqual(cutoff),
},
});
if (pendingSubs.length === 0) return 0;
Logger.log(
`Reconciling ${pendingSubs.length} pending subscription(s)…`,
'SubscriptionsService',
);
let settled = 0;
for (const sub of pendingSubs) {
try {
const invoice = await this.btcpayService.getInvoice(sub.stripeId);
if (invoice.state === 'PAID') {
await this.activateSubscription(sub.stripeId);
settled++;
Logger.log(
`Reconciled subscription ${sub.id} (invoice ${sub.stripeId}) — now active`,
'SubscriptionsService',
);
}
} catch (err) {
Logger.warn(
`Reconciliation failed for subscription ${sub.id}: ${err.message}`,
'SubscriptionsService',
);
}
}
return settled;
}
async getActiveSubscriptions(userId: string) {
return this.subscriptionsRepository.find({
where: {