Skip to content

Commit c022df6

Browse files
committed
fix: previous usage report
1 parent eff7d39 commit c022df6

File tree

1 file changed

+82
-67
lines changed

1 file changed

+82
-67
lines changed

billing/functions/usage-table.js

Lines changed: 82 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -104,78 +104,80 @@ async function createIdempotencyKey(usage){
104104
* @returns {Promise<bigint>} Previous cumulative usage (0n if not found)
105105
*/
106106
async function getPreviousUsage(currentUsage, ctx) {
107-
// Calculate previous day's date (from - 24 hours)
108-
// This works even on the 1st of the month (queries last day of previous month)
109-
const previousFrom = new Date(currentUsage.from.getTime() - 24 * 60 * 60 * 1000)
110-
111-
// Query usage table with: customer PK, sk = previousFrom#provider#space
112-
const result = await ctx.usageStore.get({
113-
customer: currentUsage.customer,
114-
from: previousFrom,
115-
provider: currentUsage.provider,
116-
space: currentUsage.space
117-
})
118-
119-
if (result.ok) {
120-
return result.ok.usage
121-
}
122-
123-
if (result.error && result.error.name !== 'RecordNotFound') {
124-
throw result.error
125-
}
126-
127-
console.log(`⚠️ No previous usage found for ${currentUsage.space} on ${previousFrom.toISOString()}.\n Attempting to recover using Stripe meter event summaries...`)
128-
129-
// Query Stripe for summaries (returns in reverse chronological order - newest first)
130-
const summaries = await ctx.stripe.billing.meters.listEventSummaries(STRIPE_BILLING_EVENT.id, {
131-
customer: currentUsage.account.replace('stripe:', ''),
132-
start_time: startOfMonth(currentUsage.from).getTime() / 1000,
133-
end_time: currentUsage.to.getTime() / 1000,
134-
value_grouping_window: 'day',
135-
limit: 1
136-
});
107+
try {
108+
// Calculate previous day's date (from - 24 hours)
109+
const previousFrom = new Date(currentUsage.from.getTime() - 24 * 60 * 60 * 1000)
110+
111+
// Query usage table with: customer PK, sk = previousFrom#provider#space
112+
const result = await ctx.usageStore.get({
113+
customer: currentUsage.customer,
114+
from: previousFrom,
115+
provider: currentUsage.provider,
116+
space: currentUsage.space
117+
})
118+
119+
if (result.ok) {
120+
return result.ok.usage
121+
}
137122

138-
if (!summaries.data || summaries.data.length === 0) {
139-
console.log(`No Stripe summaries found - treating as first-time customer`)
140-
return 0n
141-
}
123+
if (result.error && result.error.name !== 'RecordNotFound') {
124+
throw result.error
125+
}
142126

143-
const latestSummary = summaries.data[0]
144-
const latestSummaryDate = new Date(latestSummary.end_time * 1000);
127+
console.log(`⚠️ No previous usage found for ${currentUsage.space} on ${previousFrom.toISOString()}.\n Attempting to recover using Stripe meter event summaries...`)
145128

146-
console.log(`Found latest Stripe summary: ${latestSummaryDate.toISOString()}`)
147-
148-
// Query DynamoDB for usage at Stripe's latest date
149-
const recoveryResult = await ctx.usageStore.get({
150-
customer: currentUsage.customer,
151-
from: latestSummaryDate,
152-
provider: currentUsage.provider,
153-
space: currentUsage.space
154-
})
129+
// Query Stripe for summaries (returns in reverse chronological order - newest first)
130+
const summaries = await ctx.stripe.billing.meters.listEventSummaries(STRIPE_BILLING_EVENT.id, {
131+
customer: currentUsage.account.replace('stripe:', ''),
132+
start_time: startOfMonth(currentUsage.from).getTime() / 1000,
133+
end_time: currentUsage.to.getTime() / 1000,
134+
value_grouping_window: 'day',
135+
limit: 1
136+
});
155137

156-
if (recoveryResult.ok) {
157-
console.log(`⚠️ WARNING: Space ${currentUsage.space} usage between ${latestSummaryDate.toISOString()} and ${previousFrom.toISOString()} is lost using Stripe summaries for recovery.`)
158-
return recoveryResult.ok.usage
159-
}
138+
if (!summaries.data || summaries.data.length === 0) {
139+
console.log(`No Stripe summaries found - treating as first-time customer`)
140+
return 0n
141+
}
160142

161-
if (recoveryResult.error?.name === 'RecordNotFound') {
162-
console.error(`CRITICAL DATA LOSS: Cannot calculate usage delta. Manual investigation and correction required.' \n ${JSON.stringify({
163-
previousDay: previousFrom.toISOString(),
164-
latestSummaryDate: latestSummaryDate.toISOString(),
165-
space: currentUsage.space,
166-
customer: currentUsage.customer,
167-
stripeAggregatedValue: latestSummary.aggregated_value,
168-
})}`)
169-
170-
throw new Error(
171-
`Critical: Cannot calculate usage delta for space ${currentUsage.space}. ` +
172-
`Both DynamoDB records missing (${previousFrom.toISOString()} and ${latestSummaryDate.toISOString()}). ` +
173-
`This indicates data loss. Manual investigation required.`
174-
)
143+
const latestSummary = summaries.data[0]
144+
const latestSummaryDate = new Date(latestSummary.end_time * 1000);
145+
146+
console.log(`Found latest Stripe summary: ${latestSummaryDate.toISOString()}`)
147+
148+
// Query DynamoDB for usage at Stripe's latest date
149+
const recoveryResult = await ctx.usageStore.get({
150+
customer: currentUsage.customer,
151+
from: latestSummaryDate,
152+
provider: currentUsage.provider,
153+
space: currentUsage.space
154+
})
155+
156+
if (recoveryResult.ok) {
157+
console.log(`⚠️ WARNING: Space ${currentUsage.space} usage between ${latestSummaryDate.toISOString()} and ${previousFrom.toISOString()} is lost using Stripe summaries for recovery.`)
158+
return recoveryResult.ok.usage
175159
}
176160

161+
if (recoveryResult.error?.name === 'RecordNotFound') {
162+
console.error(`CRITICAL DATA LOSS: Cannot calculate usage delta. Manual investigation and correction required.' \n ${JSON.stringify({
163+
previousDay: previousFrom.toISOString(),
164+
latestSummaryDate: latestSummaryDate.toISOString(),
165+
space: currentUsage.space,
166+
customer: currentUsage.customer,
167+
stripeAggregatedValue: latestSummary.aggregated_value,
168+
})}`)
169+
170+
throw new Error(
171+
`Critical: Cannot calculate usage delta for space ${currentUsage.space}. ` +
172+
`Both DynamoDB records missing (${previousFrom.toISOString()} and ${latestSummaryDate.toISOString()}). ` +
173+
`This indicates data loss. Manual investigation required.`
174+
)
175+
}
176+
} catch (error) {
177+
console.error(`Failed to recover previous usage for space ${currentUsage.space}. Returning 0 to minimize losses. Error: ${error.message}`)
178+
}
177179

178-
throw recoveryResult.error
180+
return 0n
179181
}
180182

181183
/**
@@ -227,9 +229,16 @@ export const reportUsage = async (usage, ctx) => {
227229
// Calculate cumulative byte quantity (for logging)
228230
const cumulativeByteQuantity = Math.floor(new Big(usage.usage.toString()).div(duration).toNumber())
229231

230-
// Query previous day's usage to calculate delta
231-
const previousCumulativeUsage = await getPreviousUsage(usage, ctx)
232-
232+
let previousCumulativeUsage
233+
const isFirstOfMonth = usage.from.getUTCDate() === 1
234+
if (isFirstOfMonth) {
235+
// NOTE: Since Stripe aggregates per billing period (monthly), each month starts fresh so no need to get previous usage and calculate delta.
236+
previousCumulativeUsage = 0n
237+
} else {
238+
// Query previous day's usage to calculate delta
239+
previousCumulativeUsage = await getPreviousUsage(usage, ctx)
240+
}
241+
233242
// Calculate delta: current cumulative - previous cumulative (or 0 if no previous)
234243
// Note: Delta can be negative if users deleted data
235244
const deltaUsage = usage.usage - previousCumulativeUsage
@@ -277,6 +286,12 @@ export const reportUsage = async (usage, ctx) => {
277286
timestamp: referenceDate.toISOString(),
278287
idempotencyKey
279288
}
289+
290+
if (deltaByteQuantity == 0 ) {
291+
console.log(`No usage delta to report to Stripe. Skipping.\n${JSON.stringify(stripeRequest)}`)
292+
return { ok: {} }
293+
}
294+
280295
console.log(`sending Stripe request:\n${JSON.stringify(stripeRequest)}`)
281296

282297
const meterEvent = await ctx.stripe.billing.meterEvents.create({

0 commit comments

Comments
 (0)