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

@@ -24,7 +24,7 @@ export class SeasonRent {
userId: string;
@Column('decimal', {
precision: 5,
precision: 15,
scale: 2,
transformer: new ColumnNumericTransformer(),
})

View File

@@ -2,11 +2,12 @@ import {
BadRequestException,
Inject,
Injectable,
Logger,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { IsNull, MoreThanOrEqual, Not, Repository } from 'typeorm';
import { SeasonRent } from './entities/season-rents.entity';
import { SeasonService } from './season.service';
import { BTCPayService } from 'src/payment/providers/services/btcpay.service';
@@ -215,6 +216,49 @@ export class SeasonRentsService {
}
}
/**
* Reconcile pending season rents against BTCPay on startup.
*/
async reconcilePendingPayments(): Promise<number> {
const cutoff = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
const pendingRents = await this.seasonRentRepository.find({
where: {
status: 'pending',
providerId: Not(IsNull()),
createdAt: MoreThanOrEqual(cutoff),
},
});
if (pendingRents.length === 0) return 0;
Logger.log(
`Reconciling ${pendingRents.length} pending season rent(s)…`,
'SeasonRentsService',
);
let settled = 0;
for (const rent of pendingRents) {
try {
const invoice = await this.btcpayService.getInvoice(rent.providerId);
if (invoice.state === 'PAID') {
await this.lightningPaid(rent.providerId, invoice);
settled++;
Logger.log(
`Reconciled season rent ${rent.id} (invoice ${rent.providerId}) — now paid`,
'SeasonRentsService',
);
}
} catch (err) {
Logger.warn(
`Reconciliation failed for season rent ${rent.id}: ${err.message}`,
'SeasonRentsService',
);
}
}
return settled;
}
private getExpiringDate() {
return new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
}