-
Notifications
You must be signed in to change notification settings - Fork 245
Expand file tree
/
Copy pathhandleRevenucatRenewal.ts
More file actions
135 lines (114 loc) · 3.99 KB
/
Copy pathhandleRevenucatRenewal.ts
File metadata and controls
135 lines (114 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import type { WebhookRenewal } from "@puzzmo/revenue-cat-webhook-types";
import { CusProductStatus } from "@shared/index";
import {
getRevenueCatCustomerEmail,
getRevenueCatCustomerFingerprint,
getRevenueCatOverrideCustomerId,
} from "@/external/revenueCat/misc/getRevenueCatOverrideCustomerId";
import { provisionRevenueCatCusProduct } from "@/external/revenueCat/misc/provisionRevenueCatCusProduct";
import { resolveRevenuecatResources } from "@/external/revenueCat/misc/resolveRevenuecatResources";
import { recordRevenueCatInvoice } from "@/external/revenueCat/utils/recordRevenueCatInvoice";
import type { RevenueCatWebhookContext } from "@/external/revenueCat/webhookMiddlewares/revenuecatWebhookContext";
import { customerProductActions } from "@/internal/customers/cusProducts/actions";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts";
export const handleRenewal = async ({
event,
ctx,
}: {
event: WebhookRenewal;
ctx: RevenueCatWebhookContext;
}) => {
const { logger } = ctx;
const { product_id, app_user_id, original_app_user_id } = event;
const {
ctx: customerCtx,
product,
customer,
cusProducts,
} = await resolveRevenuecatResources({
ctx,
revenuecatProductId: product_id,
customerId: app_user_id,
originalAppUserId: original_app_user_id,
overrideCustomerId: getRevenueCatOverrideCustomerId(event),
customerEmail: getRevenueCatCustomerEmail(event),
customerFingerprint: getRevenueCatCustomerFingerprint(event),
// RevenueCat sends RENEWAL (not INITIAL_PURCHASE) when a lapsed user
// resubscribes, so the customer may legitimately not exist yet.
autoCreateCustomer: true,
});
const { curSameProduct } = getExistingCusProducts({
product,
cusProducts,
});
// Same active product: pure side-effect (webhook + invoice record). No DB
// mutation on the cusProduct; the cycle anchor is owned by the app store.
// Active only — past-due must fall through to the recovery branch below.
if (curSameProduct && curSameProduct.status === CusProductStatus.Active) {
logger.info(
`Renewal for existing active product ${product.id}, sending webhook`,
);
await customerProductActions.renew({
ctx: customerCtx,
customerProduct: curSameProduct,
fullCustomer: customer,
});
await recordRevenueCatInvoice({
ctx: customerCtx,
event,
customer,
product,
});
return { success: true };
}
// Past-due → active recovery. A product cancelled before it went past due
// needs uncancel, since markActive leaves the cancellation fields set.
if (curSameProduct && curSameProduct.status === CusProductStatus.PastDue) {
logger.info(
`Renewal for existing past due product ${product.id}, marking as active`,
);
const recoverPastDueProduct = curSameProduct.canceled
? customerProductActions.uncancel
: customerProductActions.markActive;
await recoverPastDueProduct({
ctx: customerCtx,
customerProduct: curSameProduct,
fullCustomer: customer,
});
logger.info(`Marked past due product as active: ${curSameProduct.id}`);
await recordRevenueCatInvoice({
ctx: customerCtx,
event,
customer,
product,
});
return { success: true };
}
// Reactivate same product (expired/canceled → active).
if (curSameProduct) {
await customerProductActions.uncancel({
ctx: customerCtx,
customerProduct: curSameProduct,
fullCustomer: customer,
});
logger.info(`Reactivated cus_product: ${curSameProduct.id}`);
await recordRevenueCatInvoice({
ctx: customerCtx,
event,
customer,
product,
});
return { success: true };
}
// Different product (upgrade or downgrade). V2 attach handles expiring the
// outgoing cusProduct via computeAttachPlan's transition logic.
await provisionRevenueCatCusProduct({
ctx: customerCtx,
customer,
product,
appUserId: app_user_id,
});
logger.info(`Created RC cus_product for ${product.id} (renewal transition)`);
await recordRevenueCatInvoice({ ctx: customerCtx, event, customer, product });
return { success: true };
};