fix(server): rcat event parse (#13781)

This commit is contained in:
DarkSky
2025-10-20 17:20:41 +08:00
committed by GitHub
parent 5c0e3b8a7f
commit 01c164a78a
2 changed files with 58 additions and 28 deletions

View File

@@ -3,6 +3,8 @@ import { z } from 'zod';
import { Config, EventBus } from '../../../base';
import { Public } from '../../../core/auth';
import { FeatureService } from '../../../core/features';
import { Models } from '../../../models';
const RcEventSchema = z
.object({
@@ -52,7 +54,9 @@ export class RevenueCatWebhookController {
constructor(
private readonly config: Config,
private readonly event: EventBus
private readonly event: EventBus,
private readonly models: Models,
private readonly feature: FeatureService
) {}
@Public()
@@ -70,28 +74,49 @@ export class RevenueCatWebhookController {
if (parsed.success) {
const event = parsed.data.event;
const { id, app_user_id: appUserId, type } = event;
if (
event.environment.toLowerCase() === environment?.toLowerCase()
) {
const logParams = {
appUserId,
familyShare: event.is_family_share,
environment: event.environment,
};
this.logger.log(
`[${id}] RevenueCat Webhook {${type}} received for appUserId=${appUserId}.`
);
if (
appUserId &&
(typeof event.is_family_share !== 'boolean' ||
!event.is_family_share)
) {
// immediately ack and process asynchronously
this.event
.emitAsync('revenuecat.webhook', { appUserId, event })
.catch((e: Error) => {
this.logger.error(
'Failed to handle RevenueCat Webhook event.',
e
if (appUserId) {
const user = await this.models.user.get(appUserId);
if (user) {
if (
(typeof event.is_family_share !== 'boolean' ||
!event.is_family_share) &&
(environment.toLowerCase() === 'production' ||
this.feature.isStaff(user.email))
) {
// immediately ack and process asynchronously
this.event
.emitAsync('revenuecat.webhook', { appUserId, event })
.catch((e: Error) => {
this.logger.error(
'Failed to handle RevenueCat Webhook event.',
e
);
});
return;
} else {
this.logger.warn(
`[${id}] RevenueCat Webhook received for non-acceptable params.`,
logParams
);
});
}
}
}
this.logger.warn(
`RevenueCat Webhook received for unknown user`,
logParams
);
}
} else {
this.logger.warn(

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { z } from 'zod';
import { Config } from '../../../base';
@@ -34,7 +34,7 @@ const zRcV2RawEntitlementItem = z
products: z
.object({ items: z.array(zRcV2RawProduct).default([]) })
.partial()
.nullable(),
.nullish(),
})
.passthrough();
@@ -75,7 +75,7 @@ const zRcV2RawEnvelope = z
.object({
app_user_id: z.string().optional(),
id: z.string().optional(),
subscriptions: z.array(zRcV2RawSubscription).default([]),
items: z.array(zRcV2RawSubscription).default([]),
})
.passthrough();
@@ -96,6 +96,8 @@ export type Subscription = z.infer<typeof Subscription>;
@Injectable()
export class RevenueCatService {
private readonly logger = new Logger(RevenueCatService.name);
constructor(private readonly config: Config) {}
private get apiKey(): string {
@@ -132,17 +134,15 @@ export class RevenueCatService {
);
}
const envParsed = zRcV2RawEnvelope.safeParse(await res.json());
const json = await res.json();
const envParsed = zRcV2RawEnvelope.safeParse(json);
if (envParsed.success) {
return envParsed.data.subscriptions
return envParsed.data.items
.flatMap(sub => {
const items = sub.entitlements.items ?? [];
return items.map(ent => {
const product = ent.products?.items?.[0];
if (!product) {
return null;
}
return {
identifier: ent.lookup_key,
isTrial: sub.status === 'trialing',
@@ -151,20 +151,25 @@ export class RevenueCatService {
sub.status === 'active' ||
sub.status === 'trialing',
latestPurchaseDate: sub.starts_at
? new Date(sub.starts_at * 1000)
? new Date(sub.starts_at)
: null,
expirationDate: sub.current_period_ends_at
? new Date(sub.current_period_ends_at * 1000)
? new Date(sub.current_period_ends_at)
: null,
productId: product.store_identifier,
store: sub.store ?? product.app.type,
productId: product?.store_identifier,
store: sub.store ?? product?.app.type,
willRenew: sub.auto_renewal_status === 'will_renew',
duration: product.subscription?.duration ?? null,
duration: product?.subscription?.duration ?? null,
};
});
})
.filter((s): s is Subscription => s !== null);
}
this.logger.error(
`RevenueCat subscription parse failed: ${JSON.stringify(
envParsed.error.format()
)}`
);
return null;
}
}