fix(server): rcat event parse (#13781)
This commit is contained in:
@@ -3,6 +3,8 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { Config, EventBus } from '../../../base';
|
import { Config, EventBus } from '../../../base';
|
||||||
import { Public } from '../../../core/auth';
|
import { Public } from '../../../core/auth';
|
||||||
|
import { FeatureService } from '../../../core/features';
|
||||||
|
import { Models } from '../../../models';
|
||||||
|
|
||||||
const RcEventSchema = z
|
const RcEventSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -52,7 +54,9 @@ export class RevenueCatWebhookController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly event: EventBus
|
private readonly event: EventBus,
|
||||||
|
private readonly models: Models,
|
||||||
|
private readonly feature: FeatureService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@@ -70,28 +74,49 @@ export class RevenueCatWebhookController {
|
|||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
const event = parsed.data.event;
|
const event = parsed.data.event;
|
||||||
const { id, app_user_id: appUserId, type } = event;
|
const { id, app_user_id: appUserId, type } = event;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.environment.toLowerCase() === environment?.toLowerCase()
|
event.environment.toLowerCase() === environment?.toLowerCase()
|
||||||
) {
|
) {
|
||||||
|
const logParams = {
|
||||||
|
appUserId,
|
||||||
|
familyShare: event.is_family_share,
|
||||||
|
environment: event.environment,
|
||||||
|
};
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[${id}] RevenueCat Webhook {${type}} received for appUserId=${appUserId}.`
|
`[${id}] RevenueCat Webhook {${type}} received for appUserId=${appUserId}.`
|
||||||
);
|
);
|
||||||
|
if (appUserId) {
|
||||||
if (
|
const user = await this.models.user.get(appUserId);
|
||||||
appUserId &&
|
if (user) {
|
||||||
(typeof event.is_family_share !== 'boolean' ||
|
if (
|
||||||
!event.is_family_share)
|
(typeof event.is_family_share !== 'boolean' ||
|
||||||
) {
|
!event.is_family_share) &&
|
||||||
// immediately ack and process asynchronously
|
(environment.toLowerCase() === 'production' ||
|
||||||
this.event
|
this.feature.isStaff(user.email))
|
||||||
.emitAsync('revenuecat.webhook', { appUserId, event })
|
) {
|
||||||
.catch((e: Error) => {
|
// immediately ack and process asynchronously
|
||||||
this.logger.error(
|
this.event
|
||||||
'Failed to handle RevenueCat Webhook event.',
|
.emitAsync('revenuecat.webhook', { appUserId, event })
|
||||||
e
|
.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 {
|
} else {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Config } from '../../../base';
|
import { Config } from '../../../base';
|
||||||
@@ -34,7 +34,7 @@ const zRcV2RawEntitlementItem = z
|
|||||||
products: z
|
products: z
|
||||||
.object({ items: z.array(zRcV2RawProduct).default([]) })
|
.object({ items: z.array(zRcV2RawProduct).default([]) })
|
||||||
.partial()
|
.partial()
|
||||||
.nullable(),
|
.nullish(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ const zRcV2RawEnvelope = z
|
|||||||
.object({
|
.object({
|
||||||
app_user_id: z.string().optional(),
|
app_user_id: z.string().optional(),
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
subscriptions: z.array(zRcV2RawSubscription).default([]),
|
items: z.array(zRcV2RawSubscription).default([]),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -96,6 +96,8 @@ export type Subscription = z.infer<typeof Subscription>;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RevenueCatService {
|
export class RevenueCatService {
|
||||||
|
private readonly logger = new Logger(RevenueCatService.name);
|
||||||
|
|
||||||
constructor(private readonly config: Config) {}
|
constructor(private readonly config: Config) {}
|
||||||
|
|
||||||
private get apiKey(): string {
|
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) {
|
if (envParsed.success) {
|
||||||
return envParsed.data.subscriptions
|
return envParsed.data.items
|
||||||
.flatMap(sub => {
|
.flatMap(sub => {
|
||||||
const items = sub.entitlements.items ?? [];
|
const items = sub.entitlements.items ?? [];
|
||||||
return items.map(ent => {
|
return items.map(ent => {
|
||||||
const product = ent.products?.items?.[0];
|
const product = ent.products?.items?.[0];
|
||||||
if (!product) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
identifier: ent.lookup_key,
|
identifier: ent.lookup_key,
|
||||||
isTrial: sub.status === 'trialing',
|
isTrial: sub.status === 'trialing',
|
||||||
@@ -151,20 +151,25 @@ export class RevenueCatService {
|
|||||||
sub.status === 'active' ||
|
sub.status === 'active' ||
|
||||||
sub.status === 'trialing',
|
sub.status === 'trialing',
|
||||||
latestPurchaseDate: sub.starts_at
|
latestPurchaseDate: sub.starts_at
|
||||||
? new Date(sub.starts_at * 1000)
|
? new Date(sub.starts_at)
|
||||||
: null,
|
: null,
|
||||||
expirationDate: sub.current_period_ends_at
|
expirationDate: sub.current_period_ends_at
|
||||||
? new Date(sub.current_period_ends_at * 1000)
|
? new Date(sub.current_period_ends_at)
|
||||||
: null,
|
: null,
|
||||||
productId: product.store_identifier,
|
productId: product?.store_identifier,
|
||||||
store: sub.store ?? product.app.type,
|
store: sub.store ?? product?.app.type,
|
||||||
willRenew: sub.auto_renewal_status === 'will_renew',
|
willRenew: sub.auto_renewal_status === 'will_renew',
|
||||||
duration: product.subscription?.duration ?? null,
|
duration: product?.subscription?.duration ?? null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.filter((s): s is Subscription => s !== null);
|
.filter((s): s is Subscription => s !== null);
|
||||||
}
|
}
|
||||||
|
this.logger.error(
|
||||||
|
`RevenueCat subscription parse failed: ${JSON.stringify(
|
||||||
|
envParsed.error.format()
|
||||||
|
)}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user