Skip to content

Commit c9677d1

Browse files
committed
fix: throw an error if previous usage is not found in DynamoDB or Stripe
1 parent 8439016 commit c9677d1

File tree

2 files changed

+70
-69
lines changed

2 files changed

+70
-69
lines changed

billing/functions/usage-table.js

Lines changed: 69 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -104,80 +104,76 @@ async function createIdempotencyKey(usage){
104104
* @returns {Promise<bigint>} Previous cumulative usage (0n if not found)
105105
*/
106106
async function getPreviousUsage(currentUsage, ctx) {
107-
try {
108-
// Calculate previous day's date (from - 24 hours)
109-
const previousFrom = new Date(currentUsage.from.getTime() - 24 * 60 * 60 * 1000)
107+
// Calculate previous day's date (from - 24 hours)
108+
const previousFrom = new Date(currentUsage.from.getTime() - 24 * 60 * 60 * 1000)
109+
110+
// Query usage table with: customer PK, sk = previousFrom#provider#space
111+
const result = await ctx.usageStore.get({
112+
customer: currentUsage.customer,
113+
from: previousFrom,
114+
provider: currentUsage.provider,
115+
space: currentUsage.space
116+
})
110117

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+
if (result.ok) {
119+
return result.ok.usage
120+
}
118121

119-
if (result.ok) {
120-
return result.ok.usage
121-
}
122+
if (result.error && result.error.name !== 'RecordNotFound') {
123+
throw result.error
124+
}
122125

123-
if (result.error && result.error.name !== 'RecordNotFound') {
124-
throw result.error
125-
}
126+
console.log(`⚠️ No previous usage found for ${currentUsage.space} on ${previousFrom.toISOString()}.\n Attempting to recover using Stripe meter event summaries...`)
127+
128+
// Query Stripe for summaries (returns in reverse chronological order - newest first)
129+
const summaries = await ctx.stripe.billing.meters.listEventSummaries(STRIPE_BILLING_EVENT.id, {
130+
customer: currentUsage.account.replace('stripe:', ''),
131+
start_time: startOfMonth(currentUsage.from).getTime() / 1000,
132+
end_time: currentUsage.to.getTime() / 1000,
133+
value_grouping_window: 'day',
134+
limit: 1
135+
});
126136

127-
console.log(`⚠️ No previous usage found for ${currentUsage.space} on ${previousFrom.toISOString()}.\n Attempting to recover using Stripe meter event summaries...`)
137+
if (!summaries.data || summaries.data.length === 0) {
138+
console.log(`No Stripe summaries found - treating as first-time customer`)
139+
return 0n
140+
}
128141

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-
});
142+
const latestSummary = summaries.data[0]
143+
const latestSummaryDate = new Date(latestSummary.end_time * 1000);
137144

138-
if (!summaries.data || summaries.data.length === 0) {
139-
console.log(`No Stripe summaries found - treating as first-time customer`)
140-
return 0n
141-
}
145+
console.log(`Found latest Stripe summary: ${latestSummaryDate.toISOString()}`)
146+
147+
// Query DynamoDB for usage at Stripe's latest date
148+
const recoveryResult = await ctx.usageStore.get({
149+
customer: currentUsage.customer,
150+
from: latestSummaryDate,
151+
provider: currentUsage.provider,
152+
space: currentUsage.space
153+
})
142154

143-
const latestSummary = summaries.data[0]
144-
const latestSummaryDate = new Date(latestSummary.end_time * 1000);
155+
if (recoveryResult.ok) {
156+
console.log(`⚠️ WARNING: Space ${currentUsage.space} usage between ${latestSummaryDate.toISOString()} and ${previousFrom.toISOString()} is lost using Stripe summaries for recovery.`)
157+
return recoveryResult.ok.usage
158+
}
145159

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({
160+
if (recoveryResult.error?.name === 'RecordNotFound') {
161+
console.error(`CRITICAL DATA LOSS: Cannot calculate usage delta. Manual investigation and correction required. \n ${JSON.stringify({
162+
previousDay: previousFrom.toISOString(),
163+
latestSummaryDate: latestSummaryDate.toISOString(),
164+
space: currentUsage.space,
150165
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
159-
}
166+
stripeAggregatedValue: latestSummary.aggregated_value,
167+
})}`)
160168

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}`)
169+
throw new Error(
170+
`Critical: Cannot calculate usage delta for space ${currentUsage.space}. ` +
171+
`Both DynamoDB records missing (${previousFrom.toISOString()} and ${latestSummaryDate.toISOString()}). ` +
172+
`This indicates data loss. Manual investigation required.`
173+
)
178174
}
179-
180-
return 0n
175+
176+
throw recoveryResult.error ?? new Error('Unknown error querying usage store during recovery')
181177
}
182178

183179
/**
@@ -231,15 +227,20 @@ export const reportUsage = async (usage, ctx) => {
231227

232228
const isFirstOfMonth = usage.from.getUTCDate() === 1
233229
// NOTE: Since Stripe aggregates per billing period (monthly), each month starts fresh so no need to get previous usage and calculate delta.
234-
const previousCumulativeUsage = isFirstOfMonth
235-
? 0n
236-
: await getPreviousUsage(usage, ctx)
237-
230+
let previousCumulativeUsage
231+
try {
232+
previousCumulativeUsage = isFirstOfMonth ? 0n : await getPreviousUsage(usage, ctx)
233+
} catch (/** @type {any} */ err) {
234+
return { error: err }
235+
}
236+
238237
// Calculate delta: current cumulative - previous cumulative (or 0 if no previous)
239238
// Note: Delta can be negative if users deleted data
240239
const deltaUsage = usage.usage - previousCumulativeUsage
241240

242-
if (previousCumulativeUsage === 0n) {
241+
if (isFirstOfMonth) {
242+
console.log(`First of month reset - reporting full usage as delta (no previous lookup)`, JSON.stringify({customer: usageContext.customer, space: usageContext.space}))
243+
} else if (previousCumulativeUsage === 0n) {
243244
console.log(`No previous usage found - reporting full current usage as delta`, JSON.stringify({customer: usageContext.customer, space: usageContext.space}))
244245
} else {
245246
console.log('Delta calculation:', JSON.stringify({

billing/lib/space-billing-queue.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const calculatePeriodUsage = async (instruction, ctx) => {
6565
if (!snap) console.warn(`!!! Snapshot not found, assuming empty space !!!`)
6666

6767
let size = snap?.size ?? 0n
68-
let usage = size * BigInt(instruction.to.getTime() - instruction.from.getTime())
68+
let usage = size * BigInt(instruction.to.getTime() - instruction.from.getTime()) // initial usage from snapshot
6969

7070
console.log(`Total size of ${instruction.space} is ${size} bytes @ ${instruction.from.toISOString()}`)
7171

0 commit comments

Comments
 (0)