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:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user