feat: sync rcat data (#13628)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* RevenueCat support: public webhook endpoint, webhook handler/service,
nightly reconciliation and per-user sync; subscriptions now expose
provider and iapStore; new user-facing error for App Store/Play-managed
subscriptions.
* **Chores**
* Multi-provider subscription schema (Provider, IapStore); Stripe
credentials moved into payment.stripe (top-level apiKey/webhookKey
deprecated); new payment.revenuecat config and defaults added.
* **Tests**
  * Comprehensive RevenueCat integration test suite and snapshots.
* **Documentation**
  * Admin config descriptions updated with deprecation guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-09-23 16:52:23 +08:00
committed by GitHub
parent 75a6c79b2c
commit 762b702e46
31 changed files with 2059 additions and 33 deletions
@@ -30,6 +30,9 @@ export interface Subscription {
trialEnd: Date | null;
nextBillAt: Date | null;
canceledAt: Date | null;
// read-only metadata for IAP integration
provider?: string | null;
iapStore?: string | null;
}
export interface Invoice {
@@ -1,8 +1,8 @@
import { randomUUID } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { PrismaClient, UserStripeCustomer } from '@prisma/client';
import { pick } from 'lodash-es';
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
import { omit, pick } from 'lodash-es';
import { z } from 'zod';
import { SubscriptionPlanNotFound, URLHelper } from '../../../base';
@@ -132,8 +132,9 @@ export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
const [subscription] = await this.db.$transaction([
this.db.subscription.create({
data: {
provider: Provider.stripe,
targetId: key,
...subscriptionData,
...omit(subscriptionData, 'provider', 'iapStore'),
},
}),
this.db.license.create({
@@ -9,6 +9,7 @@ import {
EventBus,
InternalServerError,
InvalidCheckoutParameters,
ManagedByAppStoreOrPlay,
Mutex,
OnEvent,
SubscriptionAlreadyExists,
@@ -103,6 +104,14 @@ export class UserSubscriptionManager extends SubscriptionManager {
throw new InvalidCheckoutParameters();
}
const active = await this.getActiveSubscription({
plan: lookupKey.plan,
userId: user.id,
});
if (active?.provider === 'revenuecat') {
throw new ManagedByAppStoreOrPlay();
}
const subscription = await this.getSubscription({
plan: lookupKey.plan,
userId: user.id,
@@ -256,7 +265,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
]),
create: {
targetId: userId,
...subscriptionData,
...omit(subscriptionData, ['provider', 'iapStore']),
},
});
}
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient, UserStripeCustomer } from '@prisma/client';
import { PrismaClient, Provider, UserStripeCustomer } from '@prisma/client';
import { omit, pick } from 'lodash-es';
import { z } from 'zod';
@@ -157,6 +157,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
return this.db.subscription.upsert({
where: {
provider: Provider.stripe,
stripeSubscriptionId: stripeSubscription.id,
},
update: {
@@ -171,7 +172,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager {
},
create: {
targetId: workspaceId,
...subscriptionData,
...omit(subscriptionData, 'provider', 'iapStore'),
},
});
}