Skip to content

Commit 7fe6a18

Browse files
committed
feat: use latest snapshot fallback for daily billing migration
1 parent c9677d1 commit 7fe6a18

File tree

6 files changed

+50
-10
lines changed

6 files changed

+50
-10
lines changed

billing/data/space-snapshot.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,13 @@ export const decode = input => {
6565
}
6666
}
6767
}
68+
69+
export const lister = {
70+
/** @type {import('../lib/api.js').Encoder<Pick<SpaceSnapshotKey, 'provider'|'space'>, Pick<SpaceSnapshotKeyStoreRecord, 'pk'>>} */
71+
encodeKey: (input) => ({
72+
ok: {
73+
pk: `${input.provider}#${input.space}`
74+
}
75+
})
76+
}
77+

billing/functions/usage-table.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,11 @@ export const reportUsage = async (usage, ctx) => {
226226
const cumulativeByteQuantity = Math.floor(new Big(usage.usage.toString()).div(duration).toNumber())
227227

228228
const isFirstOfMonth = usage.from.getUTCDate() === 1
229+
const isFirstDailyRun = usage.from <= new Date('2026-02-25T00:00:00.000Z') // Hardcoded migration cutoff. TODO: remove it after
229230
// NOTE: Since Stripe aggregates per billing period (monthly), each month starts fresh so no need to get previous usage and calculate delta.
230231
let previousCumulativeUsage
231232
try {
232-
previousCumulativeUsage = isFirstOfMonth ? 0n : await getPreviousUsage(usage, ctx)
233+
previousCumulativeUsage = (isFirstOfMonth || isFirstDailyRun) ? 0n : await getPreviousUsage(usage, ctx)
233234
} catch (/** @type {any} */ err) {
234235
return { error: err }
235236
}

billing/lib/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export interface SpaceSnapshotKey { provider: ProviderDID, space: ConsumerDID, r
6666
export type SpaceSnapshotStore =
6767
& StorePutter<SpaceSnapshot>
6868
& StoreGetter<SpaceSnapshotKey, SpaceSnapshot>
69+
& StoreLister<Pick<SpaceSnapshotKey, 'provider'|'space'>, SpaceSnapshot>
6970

7071
/**
7172
* Captures information about a customer of the service that may need to be
@@ -433,7 +434,7 @@ export interface StoreGetter<K extends {}, V> {
433434
/** StoreLister allows items in the store to be listed page by page. */
434435
export interface StoreLister<K extends {}, V> {
435436
/** Lists items in the store. */
436-
list: (key: K, options?: Pageable) => Promise<Result<ListSuccess<V>, EncodeFailure|DecodeFailure|StoreOperationFailure>>
437+
list: (key: K, options?: Pageable & { scanIndexForward?: boolean }) => Promise<Result<ListSuccess<V>, EncodeFailure|DecodeFailure|StoreOperationFailure>>
437438
}
438439

439440
/** QueueAdder allows messages to be added to the end of the queue. */

billing/lib/space-billing-queue.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,47 @@ export const calculatePeriodUsage = async (instruction, ctx) => {
5656
console.log(`Customer: ${instruction.customer}`)
5757
console.log(`Period: ${instruction.from.toISOString()} - ${instruction.to.toISOString()}`)
5858

59+
// Try to get snapshot at exact 'from' date first
5960
const { ok: snap, error } = await ctx.spaceSnapshotStore.get({
6061
space: instruction.space,
6162
provider: instruction.provider,
6263
recordedAt: instruction.from
6364
})
6465
if (error && error.name !== 'RecordNotFound') return { error }
65-
if (!snap) console.warn(`!!! Snapshot not found, assuming empty space !!!`)
6666

67-
let size = snap?.size ?? 0n
68-
let usage = size * BigInt(instruction.to.getTime() - instruction.from.getTime()) // initial usage from snapshot
67+
let snapshotToUse = snap
68+
let snapshotDate = instruction.from
69+
70+
// If no snapshot at exact 'from' date, list snapshots in descending order (newest first)
71+
// and check if the most recent one is before 'from'.
72+
if (!snap) {
73+
console.warn(`No snapshot found at ${instruction.from.toISOString()}, querying for most recent snapshot before this date...`)
74+
75+
const listResult = await ctx.spaceSnapshotStore.list({
76+
space: instruction.space,
77+
provider: instruction.provider
78+
}, { size: 1, scanIndexForward: false }) // get newest snapshot
79+
80+
if (listResult.error) return listResult
81+
82+
// Check if the newest snapshot is at or before 'from'
83+
const newestSnapshot = listResult.ok.results[0]
84+
if (newestSnapshot && newestSnapshot.recordedAt.getTime() <= instruction.from.getTime()) {
85+
snapshotToUse = newestSnapshot
86+
snapshotDate = newestSnapshot.recordedAt
87+
console.log(`Found snapshot @ ${snapshotDate.toISOString()}: ${newestSnapshot.size} bytes`)
88+
} else {
89+
console.warn(`!!! No snapshot found before ${instruction.from.toISOString()}, assuming empty space !!!`)
90+
}
91+
}
92+
93+
let size = snapshotToUse ? snapshotToUse.size : 0n
94+
let usage = size * BigInt(instruction.to.getTime() - snapshotDate.getTime()) // initial usage from snapshot
6995

70-
console.log(`Total size of ${instruction.space} is ${size} bytes @ ${instruction.from.toISOString()}`)
96+
console.log(`Starting calculation from ${snapshotDate.toISOString()}: ${size} bytes for ${instruction.space}`)
7197

7298
let totalDiffs = 0
73-
for await (const page of iterateSpaceDiffs(instruction, ctx)) {
99+
for await (const page of iterateSpaceDiffs({...instruction, from: snapshotDate}, ctx)) {
74100
if (page.error) return page
75101
totalDiffs += page.ok.length
76102
for (const diff of page.ok) {

billing/tables/client.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export const createStoreListerClient = (conf, context) => {
182182
IndexName: context.indexName,
183183
Limit: options?.size ?? 100,
184184
KeyConditions: conditions,
185+
ScanIndexForward: options?.scanIndexForward,
185186
ExclusiveStartKey: options?.cursor
186187
? marshall(JSON.parse(options.cursor))
187188
: undefined

billing/tables/space-snapshot.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { createStoreGetterClient, createStorePutterClient } from './client.js'
2-
import { validate, encode, decode, encodeKey } from '../data/space-snapshot.js'
1+
import { createStoreGetterClient, createStoreListerClient, createStorePutterClient } from './client.js'
2+
import { validate, encode, decode, encodeKey, lister } from '../data/space-snapshot.js'
33

44
/**
55
* Stores snapshots of total space size at a given time.
@@ -35,5 +35,6 @@ export const spaceSnapshotTableProps = {
3535
*/
3636
export const createSpaceSnapshotStore = (conf, { tableName }) => ({
3737
...createStorePutterClient(conf, { tableName, validate, encode }),
38-
...createStoreGetterClient(conf, { tableName, encodeKey, decode })
38+
...createStoreGetterClient(conf, { tableName, encodeKey, decode }),
39+
...createStoreListerClient(conf, { tableName, encodeKey: lister.encodeKey, decode })
3940
})

0 commit comments

Comments
 (0)