(
params: {
userId: string
creditsToConsume: number
grants: (typeof schema.creditLedger.$inferSelect)[]
logger: Logger
} & ParamsExcluding<
typeof updateGrantBalance,
'grant' | 'consumed' | 'newBalance'
>,
)
| 211 | * new credits are added. This function only deepens debt on overflow. |
| 212 | */ |
| 213 | export async function consumeFromOrderedGrants( |
| 214 | params: { |
| 215 | userId: string |
| 216 | creditsToConsume: number |
| 217 | grants: (typeof schema.creditLedger.$inferSelect)[] |
| 218 | logger: Logger |
| 219 | } & ParamsExcluding< |
| 220 | typeof updateGrantBalance, |
| 221 | 'grant' | 'consumed' | 'newBalance' |
| 222 | >, |
| 223 | ): Promise<CreditConsumptionResult> { |
| 224 | const { userId, creditsToConsume, grants, logger } = params |
| 225 | |
| 226 | let remainingToConsume = creditsToConsume |
| 227 | let consumed = 0 |
| 228 | let fromPurchased = 0 |
| 229 | |
| 230 | // Consume from positive balances in priority order. |
| 231 | // NOTE: debt grants (balance < 0) are skipped. Consumption never repays |
| 232 | // debt; that only happens via grant-credits.ts when new credits arrive. |
| 233 | for (const grant of grants) { |
| 234 | if (remainingToConsume <= 0) break |
| 235 | if (grant.balance <= 0) continue |
| 236 | |
| 237 | const consumeFromThisGrant = Math.min(remainingToConsume, grant.balance) |
| 238 | const newBalance = grant.balance - consumeFromThisGrant |
| 239 | remainingToConsume -= consumeFromThisGrant |
| 240 | consumed += consumeFromThisGrant |
| 241 | |
| 242 | // Track consumption from purchased credits |
| 243 | if (grant.type === 'purchase') { |
| 244 | fromPurchased += consumeFromThisGrant |
| 245 | } |
| 246 | |
| 247 | await updateGrantBalance({ |
| 248 | ...params, |
| 249 | grant, |
| 250 | consumed: consumeFromThisGrant, |
| 251 | newBalance, |
| 252 | }) |
| 253 | |
| 254 | // Mutate in-memory balance so the overflow check below sees |
| 255 | // post-consumption state (not the stale original value). |
| 256 | grant.balance = newBalance |
| 257 | } |
| 258 | |
| 259 | // If we still have remaining to consume, create or extend debt on the |
| 260 | // last grant. After the loop above all positive-balance grants are drained. |
| 261 | // The "last grant" (lowest consumption priority, typically a subscription |
| 262 | // grant that renews monthly) absorbs the overflow as debt. |
| 263 | if (remainingToConsume > 0 && grants.length > 0) { |
| 264 | const lastGrant = grants[grants.length - 1] |
| 265 | const newBalance = lastGrant.balance - remainingToConsume |
| 266 | |
| 267 | await updateGrantBalance({ |
| 268 | ...params, |
| 269 | grant: lastGrant, |
| 270 | consumed: remainingToConsume, |
no test coverage detected