@@ -11,6 +11,86 @@ import {
1111} from "@/lib/utils/slackAlerts" ;
1212import Stripe from "stripe" ;
1313
14+ interface PreviousAttributes {
15+ // Billing period dates (change during automated renewals)
16+ current_period_end ?: number ;
17+ current_period_start ?: number ;
18+
19+ // Subscription items and pricing (change during manual updates)
20+ items ?: {
21+ data ?: Partial < Stripe . SubscriptionItem > [ ] ;
22+ } ;
23+
24+ // Other subscription properties that can change manually
25+ plan ?: Stripe . Plan | null ;
26+ quantity ?: number ;
27+ discount ?: Stripe . Discount | null ;
28+ cancel_at_period_end ?: boolean ;
29+ collection_method ?: string ;
30+ latest_invoice ?: string | Stripe . Invoice | null ;
31+ }
32+
33+ function isAutomatedBillingRenewal (
34+ sub : Stripe . Subscription ,
35+ previousAttributes : PreviousAttributes | undefined ,
36+ ) : boolean {
37+ // Treat as automated renewal when:
38+ // 1. subscription status is active
39+ // 2. previousAttributes exists
40+ // 3. Only contains billing period changes (current_period_start, current_period_end) and optionally items/latest_invoice
41+ // 4. If items changed, only the period dates within items actually changed (not price/plan/quantity)
42+ // 5. cancel_at_period_end and collection_method are not present among keys
43+
44+ if ( sub . status !== "active" || ! previousAttributes ) {
45+ return false ;
46+ }
47+
48+ // Get all keys that changed in previousAttributes
49+ const changedKeys = Object . keys ( previousAttributes ) ;
50+
51+ // Define keys that indicate manual changes (not automated renewals)
52+ const manualChangeKeys = [
53+ "cancel_at_period_end" ,
54+ "collection_method" ,
55+ "plan" ,
56+ "quantity" ,
57+ "discount" ,
58+ ] ;
59+
60+ // If any manual change keys are present, this is not an automated renewal
61+ if ( manualChangeKeys . some ( ( key ) => changedKeys . includes ( key ) ) ) {
62+ return false ;
63+ }
64+
65+ // Check if items changed and verify only period dates changed
66+ if ( changedKeys . includes ( "items" ) ) {
67+ const itemsChange = previousAttributes . items ;
68+ if ( ! itemsChange || ! itemsChange . data || ! itemsChange . data [ 0 ] || ! sub . items ?. data ?. [ 0 ] ) {
69+ return false ;
70+ }
71+
72+ const previousItem = itemsChange . data [ 0 ] ;
73+ const currentItem = sub . items . data [ 0 ] ;
74+
75+ // Check if price, plan, or quantity actually changed by comparing current vs previous
76+ if (
77+ previousItem . price ?. id !== currentItem . price ?. id ||
78+ previousItem . plan ?. id !== currentItem . plan ?. id ||
79+ previousItem . quantity !== currentItem . quantity
80+ ) {
81+ return false ;
82+ }
83+ }
84+
85+ // Define expected keys for automated renewal (period dates + optional items/latest_invoice)
86+ const allowedKeys = [ "current_period_start" , "current_period_end" , "items" , "latest_invoice" ] ;
87+
88+ // Check if all changed keys are allowed for automated renewals
89+ const hasOnlyAllowedKeys = changedKeys . every ( ( key ) => allowedKeys . includes ( key ) ) ;
90+
91+ return hasOnlyAllowedKeys ;
92+ }
93+
1494function validateAndParseQuotas ( product : Stripe . Product ) : {
1595 valid : boolean ;
1696 requestsPerMonth ?: number ;
@@ -100,6 +180,11 @@ export const POST = async (req: Request): Promise<Response> => {
100180
101181 const previousAttributes = event . data . previous_attributes ;
102182
183+ // Skip database updates and notifications for automated billing renewals
184+ if ( isAutomatedBillingRenewal ( sub , previousAttributes ) ) {
185+ return new Response ( "Skip" , { status : 201 } ) ;
186+ }
187+
103188 if ( ! sub . items ?. data ?. [ 0 ] ?. price ?. id || ! sub . customer ) {
104189 return new Response ( "OK" ) ;
105190 }
@@ -145,6 +230,7 @@ export const POST = async (req: Request): Promise<Response> => {
145230
146231 const { requestsPerMonth, logsRetentionDays, auditLogsRetentionDays } = quotas ;
147232
233+ // Update quotas and workspace tier
148234 await db . transaction ( async ( tx ) => {
149235 await tx
150236 . update ( schema . workspaces )
@@ -226,10 +312,10 @@ export const POST = async (req: Request): Promise<Response> => {
226312 }
227313 }
228314
315+ // Send notification for subscription update
229316 if ( customer && ! customer . deleted && customer . email ) {
230317 const formattedPrice = formatPrice ( price . unit_amount ) ;
231318
232- // Send notification for any subscription update
233319 await alertSubscriptionUpdate (
234320 product . name ,
235321 formattedPrice ,
0 commit comments